package main // 🤖 A minimal SES v2 sender for the email anchor's sign-in codes (issue // #1091) — one SigV4-signed POST to the SendEmail API, riding the same // hand-rolled signer the S3 store uses (sigv4.go). No SDK: the whole mailer // is readable in one sitting, and home's dependency set stays boring. // // Deliverability (SPF/DKIM, sandbox exit, bounce handling) is SES-console // and DNS work, deliberately outside this file — see issue #1091's ops list. import ( "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "os" "time" ) // sesMailer sends through SES v2 in one region from one address. endpoint is // overridable for tests (a fake SES httptest server); empty means the real // regional endpoint. type sesMailer struct { region string from string accessKey string secretKey string endpoint string } func newSESMailer(region, from string) (*sesMailer, error) { access, secret := os.Getenv("AWS_ACCESS_KEY_ID"), os.Getenv("AWS_SECRET_ACCESS_KEY") if access == "" || secret == "" { return nil, errors.New("email anchor: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be set") } return &sesMailer{region: region, from: from, accessKey: access, secretKey: secret}, nil } // send delivers one plain-text email. The recipient address is used for this // send and then dropped — nothing here persists it (the blind-index rule). func (m *sesMailer) send(to, subject, text string) error { body, err := json.Marshal(map[string]any{ "FromEmailAddress": m.from, "Destination": map[string]any{"ToAddresses": []string{to}}, "Content": map[string]any{ "Simple": map[string]any{ "Subject": map[string]any{"Data": subject, "Charset": "UTF-8"}, "Body": map[string]any{ "Text": map[string]any{"Data": text, "Charset": "UTF-8"}, }, }, }, }) if err != nil { return err } endpoint := m.endpoint if endpoint == "" { endpoint = "https://email." + m.region + ".amazonaws.com" } req, err := http.NewRequest("POST", endpoint+"/v2/email/outbound-emails", bytes.NewReader(body)) if err != nil { return err } now := time.Now() digest := sha256.Sum256(body) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Amz-Date", now.UTC().Format("20060102T150405Z")) req.Header.Set("X-Amz-Content-Sha256", hex.EncodeToString(digest[:])) awsSignV4(req, m.accessKey, m.secretKey, m.region, "ses", now) resp, err := oidcHTTP.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode/100 != 2 { b, _ := io.ReadAll(io.LimitReader(resp.Body, 512)) return fmt.Errorf("ses: %s: %s", resp.Status, b) } return nil }