127 lines
3.1 KiB
Go
127 lines
3.1 KiB
Go
package campaigns
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"crypto/rand"
|
||
|
|
"crypto/sha256"
|
||
|
|
"encoding/hex"
|
||
|
|
"net/mail"
|
||
|
|
"regexp"
|
||
|
|
"strings"
|
||
|
|
"unicode"
|
||
|
|
|
||
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/security"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
MaxPromptLen = security.MaxCampaignPromptRunes
|
||
|
|
MaxSubjectLen = 200
|
||
|
|
MaxHTMLBodyLen = security.MaxEmailHTMLRunes
|
||
|
|
unsubscribeMark = "data-descrybe-unsubscribe"
|
||
|
|
)
|
||
|
|
|
||
|
|
var emailLoose = regexp.MustCompile(`(?i)^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$`)
|
||
|
|
|
||
|
|
// NormalizeEmail lowercases and trims; returns ErrInvalidEmail when invalid.
|
||
|
|
func NormalizeEmail(raw string) (string, error) {
|
||
|
|
raw = strings.TrimSpace(strings.ToLower(raw))
|
||
|
|
if raw == "" || len(raw) > 254 {
|
||
|
|
return "", ErrInvalidEmail
|
||
|
|
}
|
||
|
|
addr, err := mail.ParseAddress(raw)
|
||
|
|
if err != nil {
|
||
|
|
return "", ErrInvalidEmail
|
||
|
|
}
|
||
|
|
e := strings.TrimSpace(strings.ToLower(addr.Address))
|
||
|
|
if !emailLoose.MatchString(e) {
|
||
|
|
return "", ErrInvalidEmail
|
||
|
|
}
|
||
|
|
return e, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func EmailHash(email string) string {
|
||
|
|
sum := sha256.Sum256([]byte(strings.ToLower(strings.TrimSpace(email))))
|
||
|
|
return hex.EncodeToString(sum[:])
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewToken() (string, error) {
|
||
|
|
b := make([]byte, 24)
|
||
|
|
if _, err := rand.Read(b); err != nil {
|
||
|
|
return "", err
|
||
|
|
}
|
||
|
|
return hex.EncodeToString(b), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func ValidatePrompt(prompt string) error {
|
||
|
|
if security.CapPromptLength(prompt, MaxPromptLen) {
|
||
|
|
return ErrPromptTooLong
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// SanitizePrompt bounds and soft-filters campaign prompts before AI / storage.
|
||
|
|
func SanitizePrompt(prompt string) string {
|
||
|
|
return security.SanitizePrompt(prompt, MaxPromptLen)
|
||
|
|
}
|
||
|
|
|
||
|
|
// SanitizeHTMLBody strips dangerous markup from generated/stored campaign HTML.
|
||
|
|
func SanitizeHTMLBody(html string) string {
|
||
|
|
return security.SanitizeEmailHTML(html)
|
||
|
|
}
|
||
|
|
|
||
|
|
func HasUnsubscribeFooter(html string) bool {
|
||
|
|
lower := strings.ToLower(html)
|
||
|
|
if strings.Contains(lower, unsubscribeMark) {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
if strings.Contains(lower, "unsubscribe") && (strings.Contains(lower, "href=") || strings.Contains(lower, "/unsubscribe")) {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
func EnsureUnsubscribeFooter(html, plain, unsubscribeURL string) (string, string) {
|
||
|
|
if HasUnsubscribeFooter(html) {
|
||
|
|
if plain == "" {
|
||
|
|
plain = stripTags(html)
|
||
|
|
}
|
||
|
|
return html, plain
|
||
|
|
}
|
||
|
|
footerHTML := `<hr style="border:none;border-top:1px solid #ddd;margin:24px 0"/>` +
|
||
|
|
`<p style="font-size:12px;color:#666" ` + unsubscribeMark + `="1">` +
|
||
|
|
`You are receiving this because you opted in to marketing emails. ` +
|
||
|
|
`<a href="` + unsubscribeURL + `">Unsubscribe</a>.</p>`
|
||
|
|
footerPlain := "\n\n---\nUnsubscribe: " + unsubscribeURL + "\n"
|
||
|
|
if strings.TrimSpace(html) == "" {
|
||
|
|
html = "<div></div>"
|
||
|
|
}
|
||
|
|
html = html + footerHTML
|
||
|
|
if plain == "" {
|
||
|
|
plain = stripTags(html)
|
||
|
|
} else {
|
||
|
|
plain = plain + footerPlain
|
||
|
|
}
|
||
|
|
return html, plain
|
||
|
|
}
|
||
|
|
|
||
|
|
func stripTags(s string) string {
|
||
|
|
var b strings.Builder
|
||
|
|
inTag := false
|
||
|
|
for _, r := range s {
|
||
|
|
switch {
|
||
|
|
case r == '<':
|
||
|
|
inTag = true
|
||
|
|
case r == '>':
|
||
|
|
inTag = false
|
||
|
|
case !inTag:
|
||
|
|
if unicode.IsSpace(r) {
|
||
|
|
if b.Len() > 0 && b.String()[b.Len()-1] != ' ' {
|
||
|
|
b.WriteByte(' ')
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
b.WriteRune(r)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return strings.TrimSpace(b.String())
|
||
|
|
}
|