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 := `
` + `You are receiving this because you opted in to marketing emails. ` + `Unsubscribe.
` footerPlain := "\n\n---\nUnsubscribe: " + unsubscribeURL + "\n" if strings.TrimSpace(html) == "" { html = "" } 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()) }