build-base

This commit is contained in:
2026-05-22 06:06:44 +00:00
parent 5a127bf2a2
commit e8f9dea282
38 changed files with 7151 additions and 4 deletions
+137
View File
@@ -1,12 +1,19 @@
package smtp
import (
"bytes"
"context"
"fmt"
"log"
"mime/multipart"
"net/textproto"
"strings"
"time"
"ghb.freebede.com/nahakubuilder/mailgosend/internal/crypto"
"ghb.freebede.com/nahakubuilder/mailgosend/internal/delivery"
"ghb.freebede.com/nahakubuilder/mailgosend/internal/models"
"ghb.freebede.com/nahakubuilder/mailgosend/internal/storage"
)
// QueueWorker polls the delivery queue and dispatches messages.
@@ -106,6 +113,7 @@ func (w *QueueWorker) drainQueue() {
}
// markFailed updates queue status with exponential back-off or marks permanent failure.
// On permanent failure with a non-empty sender, generates a DSN (RFC 3464) bounce.
func (w *QueueWorker) markFailed(ctx context.Context, queueID int64, from, to, errMsg string, perm bool) {
status := "failed"
if perm {
@@ -125,6 +133,135 @@ func (w *QueueWorker) markFailed(ctx context.Context, queueID int64, from, to, e
}
w.deps.DB.LogDelivery(ctx, queueID, from, to, status, 0, errMsg, "") //nolint:errcheck
log.Printf("[queue] %s %d → %s: %s", status, queueID, to, errMsg)
// Generate DSN bounce for permanent failures only, never bounce a bounce
// (null sender <> = already a DSN).
if perm && from != "" && from != "<>" {
w.sendDSN(ctx, from, to, errMsg)
}
}
// sendDSN delivers a Delivery Status Notification (RFC 3464) to the original sender.
// Failures here are logged but not re-queued to avoid bounce loops.
func (w *QueueWorker) sendDSN(ctx context.Context, originalFrom, failedTo, reason string) {
sender := strings.ToLower(originalFrom)
// Determine if original sender is a local user.
user, err := w.deps.DB.GetUserByEmail(ctx, sender)
if err != nil {
log.Printf("[queue] dsn lookup sender %s: %v", sender, err)
return
}
if user == nil || !user.Enabled {
// Sender is remote — attempt external SMTP delivery of DSN.
dsnRaw, buildErr := buildDSN(w.deps.Cfg.Hostname, failedTo, reason)
if buildErr != nil {
log.Printf("[queue] dsn build: %v", buildErr)
return
}
ehlo := w.deps.Cfg.SMTPHostname
if ehlo == "" {
ehlo = w.deps.Cfg.Hostname
}
result := delivery.Deliver(ctx, ehlo, "", sender, dsnRaw)
if result.SMTPCode != 250 {
log.Printf("[queue] dsn delivery to %s failed: %s", sender, result.Message)
}
return
}
// Local sender — save DSN directly to INBOX.
inbox, err := w.deps.DB.GetMailboxByType(ctx, user.ID, models.MailboxInbox)
if err != nil || inbox == nil {
log.Printf("[queue] dsn inbox for %s: %v", sender, err)
return
}
dsnRaw, err := buildDSN(w.deps.Cfg.Hostname, failedTo, reason)
if err != nil {
log.Printf("[queue] dsn build: %v", err)
return
}
hostname := w.deps.Cfg.Hostname
if hostname == "" {
hostname = "localhost"
}
msg := &storage.IncomingMessage{
Raw: dsnRaw,
FromEmail: "",
Subject: "Delivery Status Notification: failed to deliver to " + failedTo,
Date: time.Now().UTC(),
MessageID: fmt.Sprintf("<dsn-%d@%s>", time.Now().UnixNano(), hostname),
}
if _, err := w.deps.Store.SaveIncoming(ctx, user.ID, inbox.ID, msg); err != nil {
log.Printf("[queue] dsn save: %v", err)
}
}
// buildDSN constructs a minimal RFC 3464 multipart/report message.
func buildDSN(hostname, failedTo, reason string) ([]byte, error) {
if hostname == "" {
hostname = "localhost"
}
now := time.Now().UTC()
msgID := fmt.Sprintf("<dsn-%d@%s>", now.UnixNano(), hostname)
var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
boundary := mw.Boundary()
// Outer headers.
header := fmt.Sprintf(
"From: Mail Delivery Subsystem <mailer-daemon@%s>\r\n"+
"To: <%s>\r\n"+
"Subject: Delivery Status Notification (Failure)\r\n"+
"Date: %s\r\n"+
"Message-ID: %s\r\n"+
"MIME-Version: 1.0\r\n"+
"Content-Type: multipart/report; report-type=delivery-status; boundary=%q\r\n"+
"\r\n",
hostname, failedTo,
now.Format("Mon, 02 Jan 2006 15:04:05 -0700"),
msgID, boundary,
)
buf.WriteString(header)
// Part 1: human-readable explanation.
ph := make(textproto.MIMEHeader)
ph.Set("Content-Type", "text/plain; charset=utf-8")
pw, err := mw.CreatePart(ph)
if err != nil {
return nil, err
}
fmt.Fprintf(pw,
"Your message could not be delivered to the following recipient:\r\n\r\n"+
" Recipient: %s\r\n"+
" Reason: %s\r\n\r\n"+
"This is a permanent error. The message has not been delivered and will not be retried.\r\n",
failedTo, reason)
// Part 2: machine-readable delivery-status (RFC 3464).
sh := make(textproto.MIMEHeader)
sh.Set("Content-Type", "message/delivery-status")
sw, err := mw.CreatePart(sh)
if err != nil {
return nil, err
}
fmt.Fprintf(sw,
"Reporting-MTA: dns; %s\r\n\r\n"+
"Final-Recipient: rfc822; %s\r\n"+
"Action: failed\r\n"+
"Status: 5.0.0\r\n"+
"Diagnostic-Code: smtp; %s\r\n",
hostname, failedTo, reason)
if err := mw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// nextBackoff returns the back-off duration based on attempt count using