// Package mailrelay is the hub's app-email passthrough: it re-emits a raw MIME message // from a customer box to Resend over SMTP, UNCHANGED. This is deliberately separate from // internal/notify (the hub's own structured-alert path, which uses the Resend HTTP API): // the spike proved parse-then-API silently drops inline CID images, so app mail must be // raw-SMTP passthrough (SPIKE-smtp-app-relay-2026-06-28.md §4). The Resend key lives only // here, hub-side — never on a box. package mailrelay import ( "bytes" "context" "crypto/tls" "fmt" "net" "net/mail" "net/smtp" "strings" "time" ) // Sender delivers a raw message to the upstream mail provider. Resend applies DKIM; the // bytes are passed through unchanged (no parse/re-encode). The seam keeps the API handler // unit-testable with a fake (no real Resend in unit tests). type Sender interface { Send(ctx context.Context, raw []byte, mailFrom string, rcptTo []string) error } // ResendSMTP is the production passthrough sender: STARTTLS to smtp.resend.com:587, // AUTH LOGIN resend/, then raw MAIL/RCPT/DATA. type ResendSMTP struct { apiKey string addr string // host:port host string // host (for TLS ServerName + auth) helo string // EHLO name } // NewResendSMTP builds a sender for the live Resend SMTP endpoint. func NewResendSMTP(apiKey string) *ResendSMTP { return &ResendSMTP{ apiKey: apiKey, addr: "smtp.resend.com:587", host: "smtp.resend.com", helo: "felhom.eu", } } // Send re-emits the raw message to Resend. A connection/auth/transport failure or an SMTP // refusal is returned as an error (the API handler maps it to an HTTP status); on success // it returns nil. func (s *ResendSMTP) Send(ctx context.Context, raw []byte, mailFrom string, rcptTo []string) error { if len(rcptTo) == 0 { return fmt.Errorf("no recipients") } d := net.Dialer{Timeout: 15 * time.Second} conn, err := d.DialContext(ctx, "tcp", s.addr) if err != nil { return fmt.Errorf("dialing resend: %w", err) } c, err := smtp.NewClient(conn, s.host) if err != nil { conn.Close() return fmt.Errorf("smtp client: %w", err) } defer c.Close() if err := c.Hello(s.helo); err != nil { return fmt.Errorf("EHLO: %w", err) } if ok, _ := c.Extension("STARTTLS"); !ok { return fmt.Errorf("resend did not offer STARTTLS") } if err := c.StartTLS(&tls.Config{ServerName: s.host, MinVersion: tls.VersionTLS12}); err != nil { return fmt.Errorf("STARTTLS: %w", err) } if err := c.Auth(&loginAuth{username: "resend", password: s.apiKey}); err != nil { return fmt.Errorf("AUTH LOGIN: %w", err) } if err := c.Mail(mailFrom); err != nil { return fmt.Errorf("MAIL FROM: %w", err) } for _, rcpt := range rcptTo { if err := c.Rcpt(rcpt); err != nil { return fmt.Errorf("RCPT TO <%s>: %w", rcpt, err) } } wc, err := c.Data() if err != nil { return fmt.Errorf("DATA: %w", err) } if _, err := wc.Write(raw); err != nil { wc.Close() return fmt.Errorf("writing message: %w", err) } if err := wc.Close(); err != nil { // The 250/5xx verdict lands here (Resend returns the queued-id on 250, or e.g. a // 550 for an unverified From domain). return fmt.Errorf("delivery refused: %w", err) } return c.Quit() } // loginAuth implements the non-standard SMTP AUTH LOGIN exchange as a net/smtp.Auth // (stdlib ships PlainAuth only). Resend's SMTP accepts LOGIN with username "resend" and // the API key as the password (spike §4). type loginAuth struct { username, password string } func (a *loginAuth) Start(_ *smtp.ServerInfo) (string, []byte, error) { return "LOGIN", nil, nil } func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { if !more { return nil, nil } switch strings.ToLower(strings.TrimSpace(string(fromServer))) { case "username:": return []byte(a.username), nil case "password:": return []byte(a.password), nil default: return nil, fmt.Errorf("unexpected server challenge: %q", fromServer) } } // FromDomain parses the From *header* of a raw MIME message (not the envelope — Resend // checks the header domain; the unverified-domain 550 lands at DATA). Returns the // lowercased domain. func FromDomain(raw []byte) (string, error) { msg, err := mail.ReadMessage(bytes.NewReader(raw)) if err != nil { return "", fmt.Errorf("parsing message headers: %w", err) } from := strings.TrimSpace(msg.Header.Get("From")) if from == "" { return "", fmt.Errorf("missing From header") } addr, err := mail.ParseAddress(from) if err != nil { list, lerr := mail.ParseAddressList(from) if lerr != nil || len(list) == 0 { return "", fmt.Errorf("parsing From address %q: %w", from, err) } addr = list[0] } at := strings.LastIndex(addr.Address, "@") if at < 0 || at == len(addr.Address)-1 { return "", fmt.Errorf("From address %q has no domain", addr.Address) } return strings.ToLower(addr.Address[at+1:]), nil }