package mail import ( "context" "encoding/json" "errors" "fmt" "github.com/jackc/pgx/v5" "github.com/ultisuite/ulti-backend/internal/mail/listunsubscribe" "github.com/ultisuite/ulti-backend/internal/mail/smtp" ) // MailSender sends immediately without outbox persistence. type MailSender interface { Send(ctx context.Context, req *smtp.SendRequest) error } var ( ErrListUnsubscribeUnavailable = errors.New("list-unsubscribe mailto not available") ErrListUnsubscribeNoMailto = errors.New("list-unsubscribe has no mailto target") ) type messageAuthInfo struct { ListUnsubscribe string `json:"list_unsubscribe"` } // SendMailtoListUnsubscribe sends the RFC 2369 mailto unsubscribe without outbox or sent copy. func (s *Service) SendMailtoListUnsubscribe( ctx context.Context, externalID, messageID string, sender MailSender, ) (*listunsubscribe.Mailto, error) { if sender == nil { return nil, errors.New("mail sender not configured") } var accountID string var authRaw []byte err := s.db.QueryRow(ctx, ` SELECT m.account_id, m.auth_info FROM messages m JOIN mail_accounts ma ON m.account_id = ma.id WHERE m.id = $1 AND ma.user_id = (SELECT id FROM users WHERE external_id = $2) `, messageID, externalID).Scan(&accountID, &authRaw) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, ErrNotFound } return nil, err } var auth messageAuthInfo if len(authRaw) > 0 { _ = json.Unmarshal(authRaw, &auth) } parsed := listunsubscribe.Parse(auth.ListUnsubscribe) if parsed.Mailto == nil { if parsed.HTTP != "" { return nil, fmt.Errorf("%w: use http url", ErrListUnsubscribeNoMailto) } return nil, ErrListUnsubscribeUnavailable } fromEmail, err := s.resolveAccountFromEmail(ctx, accountID) if err != nil { return nil, err } m := parsed.Mailto req := &smtp.SendRequest{ AccountID: accountID, From: fromEmail, To: []string{m.Address}, Subject: m.Subject, BodyText: m.Body, } if err := sender.Send(ctx, req); err != nil { return nil, err } return m, nil } func (s *Service) resolveAccountFromEmail(ctx context.Context, accountID string) (string, error) { var fromEmail string err := s.db.QueryRow(ctx, ` SELECT mi.email FROM mail_identities mi JOIN mail_accounts ma ON mi.account_id = ma.id WHERE ma.id = $1 AND mi.is_default = true LIMIT 1 `, accountID).Scan(&fromEmail) if err == nil && fromEmail != "" { return fromEmail, nil } if err := s.db.QueryRow(ctx, `SELECT email FROM mail_accounts WHERE id = $1`, accountID).Scan(&fromEmail); err != nil { return "", err } if fromEmail == "" { return "", errors.New("account has no from address") } return fromEmail, nil }