Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
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())
|
||||
}
|
||||
Reference in New Issue
Block a user