package controlplane import ( "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "log/slog" "net/http" "strconv" "strings" "time" "github.com/jackc/pgx/v5" ) const maxMailFeedbackBytes = 64 << 10 type MailNotificationConfig struct { LowBalanceMicros int64 SpendAnomalyMultiplier int64 SpendAnomalyMinMicros int64 Interval time.Duration } type mailFeedback struct { EventID string `json:"event_id"` EventType string `json:"event_type"` Recipient string `json:"recipient"` Provider string `json:"provider"` Detail string `json:"detail"` } // MailFeedbackHandler accepts a provider-neutral normalized callback. An edge // adapter maps the provider's native event into this payload and signs // "." with HMAC-SHA256. func (s *Store) MailFeedbackHandler(secret string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.Header().Set("Allow", http.MethodPost) http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } body, err := io.ReadAll(io.LimitReader(r.Body, maxMailFeedbackBytes+1)) if err != nil || len(body) > maxMailFeedbackBytes || !validMailFeedbackSignature(secret, r.Header.Get("X-AIGW-Mail-Timestamp"), r.Header.Get("X-AIGW-Mail-Signature"), body, time.Now().UTC()) { http.Error(w, "invalid feedback signature", http.StatusBadRequest) return } var event mailFeedback decoder := json.NewDecoder(strings.NewReader(string(body))) decoder.DisallowUnknownFields() if decoder.Decode(&event) != nil || strings.TrimSpace(event.EventID) == "" || !strings.Contains(event.Recipient, "@") || (event.EventType != "delivered" && event.EventType != "bounce" && event.EventType != "complaint") { http.Error(w, "invalid feedback event", http.StatusBadRequest) return } if err := s.applyMailFeedback(r.Context(), event); err != nil { http.Error(w, "feedback persistence failed", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") _, _ = io.WriteString(w, "{\"received\":true}\n") }) } func validMailFeedbackSignature(secret, timestamp, signature string, body []byte, now time.Time) bool { if secret == "" || timestamp == "" || !strings.HasPrefix(signature, "sha256=") { return false } seconds, err := strconv.ParseInt(timestamp, 10, 64) if err != nil || now.Sub(time.Unix(seconds, 0)).Abs() > 5*time.Minute { return false } provided, err := hex.DecodeString(strings.TrimPrefix(signature, "sha256=")) if err != nil { return false } mac := hmac.New(sha256.New, []byte(secret)) _, _ = mac.Write([]byte(timestamp)) _, _ = mac.Write([]byte(".")) _, _ = mac.Write(body) return hmac.Equal(provided, mac.Sum(nil)) } func (s *Store) applyMailFeedback(ctx context.Context, event mailFeedback) error { tx, err := s.db.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.ReadCommitted}) if err != nil { return err } defer tx.Rollback(ctx) tag, err := tx.Exec(ctx, `INSERT INTO mail_feedback_events (event_id,event_type,recipient,provider) VALUES ($1,$2,lower($3),$4) ON CONFLICT DO NOTHING`, event.EventID, event.EventType, event.Recipient, event.Provider) if err != nil || tag.RowsAffected() == 0 { if err != nil { return err } return tx.Commit(ctx) } if event.EventType == "bounce" || event.EventType == "complaint" { detail := event.Detail if len(detail) > 1000 { detail = detail[:1000] } if _, err := tx.Exec(ctx, `INSERT INTO mail_suppressions (recipient,reason,provider,provider_event_id,detail) VALUES (lower($1),$2,$3,$4,$5) ON CONFLICT (recipient) DO UPDATE SET reason=EXCLUDED.reason,provider=EXCLUDED.provider, provider_event_id=EXCLUDED.provider_event_id,detail=EXCLUDED.detail,updated_at=now()`, event.Recipient, event.EventType, event.Provider, event.EventID, detail); err != nil { return err } if _, err := tx.Exec(ctx, `UPDATE console_mail_outbox SET status='suppressed',last_error=$2,claimed_at=NULL WHERE lower(recipient)=lower($1) AND status IN ('pending','retry','sending')`, event.Recipient, "recipient suppressed after "+event.EventType); err != nil { return err } } return tx.Commit(ctx) } func (s *Store) RunMailNotificationWorker(ctx context.Context, config MailNotificationConfig, logger *slog.Logger) { if config.Interval <= 0 { config.Interval = 5 * time.Minute } if logger == nil { logger = slog.Default() } ticker := time.NewTicker(config.Interval) defer ticker.Stop() for { if err := s.queueBillingNotifications(ctx, config); err != nil && !errors.Is(err, context.Canceled) { logger.Warn("billing_notification_scan_failed", "error", err) } select { case <-ctx.Done(): return case <-ticker.C: } } } func (s *Store) queueBillingNotifications(ctx context.Context, config MailNotificationConfig) error { rows, err := s.db.Query(ctx, `WITH spend AS ( SELECT tenant_id, COALESCE(sum(-amount_micros) FILTER (WHERE kind='usage' AND created_at>=date_trunc('day',now())),0)::bigint today, (COALESCE(sum(-amount_micros) FILTER (WHERE kind='usage' AND created_at>=date_trunc('day',now())-interval '7 days' AND created_at 0 && today >= anomalyMinimum && today >= baseline*anomalyMultiplier { body := fmt.Sprintf("Hi %s,\n\nAIGW detected unusual API spend today: %.6f %s versus a seven-day daily baseline of %.6f %s. Review API keys and usage in the console.\n", displayName(name), float64(today)/1_000_000, strings.ToUpper(currency), float64(baseline)/1_000_000, strings.ToUpper(currency)) if err := s.queueNotification(ctx, tenantID, email, "spend_anomaly", day, "Unusual AIGW API spend detected", body); err != nil { return err } } } return rows.Err() } func (s *Store) queueNotification(ctx context.Context, tenantID, recipient, kind, dedupe, subject, body string) error { tx, err := s.db.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.ReadCommitted}) if err != nil { return err } defer tx.Rollback(ctx) tag, err := tx.Exec(ctx, `INSERT INTO mail_notification_events (tenant_id,recipient,notification_type,dedupe_key) VALUES ($1,lower($2),$3,$4) ON CONFLICT DO NOTHING`, tenantID, recipient, kind, dedupe) if err != nil || tag.RowsAffected() == 0 { if err != nil { return err } return tx.Commit(ctx) } ciphertext, err := s.cipher.Encrypt(body) if err != nil { return err } if _, err := tx.Exec(ctx, `INSERT INTO console_mail_outbox (recipient,template,subject,body_ciphertext) VALUES (lower($1),$2,$3,$4)`, recipient, kind, subject, ciphertext); err != nil { return err } return tx.Commit(ctx) } func displayName(value string) string { if value = strings.TrimSpace(value); value != "" { return value } return "there" }