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,125 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func (s *Service) IsUnsubscribed(ctx context.Context, companyID uuid.UUID, emailAddr string) (bool, error) {
|
||||
emailAddr = normalizeEmail(emailAddr)
|
||||
var at *time.Time
|
||||
err := s.Pool.QueryRow(ctx, `
|
||||
SELECT unsubscribed_at FROM email_unsubscribes
|
||||
WHERE company_id = $1 AND email_hash = $2`, companyID, hashEmail(emailAddr)).Scan(&at)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return at != nil, nil
|
||||
}
|
||||
|
||||
func (s *Service) ensureUnsubscribeToken(ctx context.Context, companyID uuid.UUID, emailAddr string) (token, pageURL, apiURL string, err error) {
|
||||
emailAddr = normalizeEmail(emailAddr)
|
||||
h := hashEmail(emailAddr)
|
||||
err = s.Pool.QueryRow(ctx, `
|
||||
SELECT token FROM email_unsubscribes WHERE company_id = $1 AND email_hash = $2`,
|
||||
companyID, h).Scan(&token)
|
||||
if err == nil {
|
||||
pageURL = UnsubscribePageURL(s.Env.WebOrigin, token)
|
||||
apiURL = UnsubscribeURL(s.Env.PublicAPIURL, token)
|
||||
return token, pageURL, apiURL, nil
|
||||
}
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return "", "", "", err
|
||||
}
|
||||
token, err = newUnsubscribeToken()
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
_, err = s.Pool.Exec(ctx, `
|
||||
INSERT INTO email_unsubscribes (company_id, email, email_hash, token, unsubscribed_at)
|
||||
VALUES ($1, $2, $3, $4, NULL)
|
||||
ON CONFLICT (company_id, email_hash) DO NOTHING`, companyID, emailAddr, h, token)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
err = s.Pool.QueryRow(ctx, `
|
||||
SELECT token FROM email_unsubscribes WHERE company_id = $1 AND email_hash = $2`,
|
||||
companyID, h).Scan(&token)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
pageURL = UnsubscribePageURL(s.Env.WebOrigin, token)
|
||||
apiURL = UnsubscribeURL(s.Env.PublicAPIURL, token)
|
||||
return token, pageURL, apiURL, nil
|
||||
}
|
||||
|
||||
const maxUnsubscribeReasonLen = 500
|
||||
|
||||
// UnsubscribeInfo is the public unsubscribe API response. It must not leak
|
||||
// tenant identifiers or recipient emails (even masked) to unauthenticated callers.
|
||||
type UnsubscribeInfo struct {
|
||||
AlreadyDone bool `json:"already_unsubscribed"`
|
||||
OK bool `json:"ok"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (s *Service) LookupUnsubscribeToken(ctx context.Context, token string) (companyID uuid.UUID, emailAddr string, unsubscribed bool, err error) {
|
||||
token = strings.TrimSpace(token)
|
||||
if token == "" {
|
||||
return uuid.Nil, "", false, pgx.ErrNoRows
|
||||
}
|
||||
var at *time.Time
|
||||
err = s.Pool.QueryRow(ctx, `
|
||||
SELECT company_id, email, unsubscribed_at FROM email_unsubscribes WHERE token = $1`, token).Scan(
|
||||
&companyID, &emailAddr, &at)
|
||||
if err != nil {
|
||||
return uuid.Nil, "", false, err
|
||||
}
|
||||
return companyID, emailAddr, at != nil, nil
|
||||
}
|
||||
|
||||
func (s *Service) UnsubscribeByToken(ctx context.Context, token, reason string) (UnsubscribeInfo, error) {
|
||||
_, _, already, err := s.LookupUnsubscribeToken(ctx, token)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return UnsubscribeInfo{OK: false, Message: "invalid or expired unsubscribe link"}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return UnsubscribeInfo{}, err
|
||||
}
|
||||
if already {
|
||||
return UnsubscribeInfo{
|
||||
OK: true,
|
||||
AlreadyDone: true,
|
||||
Message: "already unsubscribed",
|
||||
}, nil
|
||||
}
|
||||
reason = clampUnsubscribeReason(reason)
|
||||
now := time.Now().UTC()
|
||||
_, err = s.Pool.Exec(ctx, `
|
||||
UPDATE email_unsubscribes SET unsubscribed_at = $2, reason = $3
|
||||
WHERE token = $1 AND unsubscribed_at IS NULL`,
|
||||
token, now, reason)
|
||||
if err != nil {
|
||||
return UnsubscribeInfo{}, err
|
||||
}
|
||||
return UnsubscribeInfo{
|
||||
OK: true,
|
||||
Message: "unsubscribed",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func clampUnsubscribeReason(reason string) string {
|
||||
reason = strings.TrimSpace(reason)
|
||||
if len(reason) > maxUnsubscribeReasonLen {
|
||||
return reason[:maxUnsubscribeReasonLen]
|
||||
}
|
||||
return reason
|
||||
}
|
||||
Reference in New Issue
Block a user