103 lines
4.3 KiB
Go
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
|
|
}
|