Files
mailgoserver/internal/relay/bounce.go
T

103 lines
4.3 KiB
Go

package relay
import (
"fmt"
"strings"
"time"
"mailgoserver/internal/mailstore"
"mailgoserver/internal/toolbox"
)
// buildBounceMessage renders a simplified delivery-status notification: plain
// text/plain, not the full RFC 3464 multipart/report shape (a machine-readable
// message/delivery-status part), since every real mail client just shows the
// human-readable part anyway and this avoids a second MIME structure to maintain for
// something informational, not delivery-critical.
func buildBounceMessage(hostname, to, originalSubject, originalMessageID string, failed []Result) (content, messageID string) {
messageID = toolbox.GenerateMessageID(hostname)
var body strings.Builder
body.WriteString("This is an automatically generated Delivery Status Notification.\r\n\r\n")
body.WriteString("Delivery to the following recipient(s) failed permanently:\r\n\r\n")
for _, f := range failed {
reason := f.ErrorMessage
if reason == "" {
reason = f.ServerResponse
}
if reason == "" {
reason = "unknown error"
}
body.WriteString(fmt.Sprintf(" - %s\r\n", f.Recipient))
if f.ErrorCode != "" {
body.WriteString(fmt.Sprintf(" Reason: %s (%s)\r\n\r\n", reason, f.ErrorCode))
} else {
body.WriteString(fmt.Sprintf(" Reason: %s\r\n\r\n", reason))
}
}
body.WriteString("----- Original message -----\r\n")
if originalSubject != "" {
body.WriteString("Subject: " + originalSubject + "\r\n")
}
if originalMessageID != "" {
body.WriteString("Message-ID: <" + originalMessageID + ">\r\n")
}
body.WriteString("\r\nThis is an automated message from " + hostname + " — please do not reply.\r\n")
headers := []string{
"Message-ID: <" + messageID + ">",
"Date: " + time.Now().Format(time.RFC1123Z),
"From: Mail Delivery System <mailer-daemon@" + hostname + ">",
"To: " + to,
"Subject: Undelivered Mail Returned to Sender",
// Marks this as an automated notification (RFC 3834) so any auto-responder or
// bounce-of-a-bounce logic on the receiving end knows not to reply to it —
// same anti-loop convention SendBounce itself relies on for its own delivery.
"Auto-Submitted: auto-replied",
`Content-Type: text/plain; charset="UTF-8"`,
"Content-Transfer-Encoding: 8bit",
"MIME-Version: 1.0",
}
return strings.Join(headers, "\r\n") + "\r\n\r\n" + body.String(), messageID
}
// SendBounce notifies to that delivery failed for the recipients in failed, mirroring
// what a real MTA does when it can't express "delivered to some, not others" as a
// single SMTP response and has to accept-then-notify instead of reject-and-let-the-
// client's-own-MTA-bounce-it. A no-op if to is empty (this itself would be responding
// to a null-sender/already-bounced message — replying to those is the classic bounce-
// loop bug, so it's refused unconditionally, not left to the caller to remember) or if
// there's nothing failed to report.
//
// Delivery is local (straight into to's own INBOX, no SMTP round-trip) when to
// resolves to a mailbox this server hosts, otherwise it's relayed out exactly like any
// other outbound message — using a null reverse-path (empty MAIL FROM, i.e. the wire
// form "MAIL FROM:<>") so a failure bouncing the bounce itself can never recurse.
func (r *Relay) SendBounce(to, originalSubject, originalMessageID string, failed []Result) error {
if to == "" || len(failed) == 0 {
return nil
}
content, bounceMessageID := buildBounceMessage(r.Hostname, to, originalSubject, originalMessageID, failed)
if r.Mailstore != nil {
if mbox, err := r.Mailstore.ResolveRecipient(to); err == nil && mbox != nil {
_, err := r.Mailstore.StoreMessage(mbox.ID, "INBOX", []byte(content), bounceMessageID,
"Mail Delivery System <mailer-daemon@"+r.Hostname+">", "Undelivered Mail Returned to Sender")
if err == mailstore.ErrQuotaExceeded {
// Nothing sensible to do — the mailbox that would receive the bounce
// is itself over quota. Drop it; the sender already saw an inline
// error (webmail) or their own MTA is retrying (SMTP), so this isn't
// the only signal they have.
return nil
}
return err
}
}
results := r.RelayEmailAsync("", []string{to}, content, []string{"to"})
if len(results) > 0 && results[0].Status != "success" {
return fmt.Errorf("bounce to %s: %s", to, results[0].ErrorMessage)
}
return nil
}