This patch contains some changes to generate tidier DSNs, which should make them slightly more readable. In particular, it also makes it able to handle multi-line errors much better than before.
169 lines
4.3 KiB
Go
169 lines
4.3 KiB
Go
package queue
|
|
|
|
import (
|
|
"bytes"
|
|
"net/mail"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
)
|
|
|
|
// Maximum length of the original message to include in the DSN.
|
|
// The receiver of the DSN might have a smaller message size than what we
|
|
// accepted, so we truncate to a value that should be large enough to be
|
|
// useful, but not problematic for modern deployments.
|
|
const maxOrigMsgLen = 256 * 1024
|
|
|
|
// deliveryStatusNotification creates a delivery status notification (DSN) for
|
|
// the given item, and puts it in the queue.
|
|
//
|
|
// References:
|
|
// - https://tools.ietf.org/html/rfc3464 (DSN)
|
|
// - https://tools.ietf.org/html/rfc6533 (Internationalized DSN)
|
|
func deliveryStatusNotification(domainFrom string, item *Item) ([]byte, error) {
|
|
info := dsnInfo{
|
|
OurDomain: domainFrom,
|
|
Destination: item.From,
|
|
MessageID: "chasquid-dsn-" + <-newID + "@" + domainFrom,
|
|
Date: time.Now().Format(time.RFC1123Z),
|
|
To: item.To,
|
|
Recipients: item.Rcpt,
|
|
FailedTo: map[string]string{},
|
|
}
|
|
|
|
for _, rcpt := range item.Rcpt {
|
|
if rcpt.Status != Recipient_SENT {
|
|
info.FailedTo[rcpt.OriginalAddress] = rcpt.OriginalAddress
|
|
switch rcpt.Status {
|
|
case Recipient_FAILED:
|
|
info.FailedRecipients = append(info.FailedRecipients, rcpt)
|
|
case Recipient_PENDING:
|
|
info.PendingRecipients = append(info.PendingRecipients, rcpt)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(item.Data) > maxOrigMsgLen {
|
|
info.OriginalMessage = string(item.Data[:maxOrigMsgLen])
|
|
} else {
|
|
info.OriginalMessage = string(item.Data)
|
|
}
|
|
|
|
info.OriginalMessageID = getMessageID(item.Data)
|
|
|
|
info.Boundary = <-newID
|
|
|
|
buf := &bytes.Buffer{}
|
|
err := dsnTemplate.Execute(buf, info)
|
|
return buf.Bytes(), err
|
|
}
|
|
|
|
func getMessageID(data []byte) string {
|
|
msg, err := mail.ReadMessage(bytes.NewBuffer(data))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return msg.Header.Get("Message-ID")
|
|
}
|
|
|
|
type dsnInfo struct {
|
|
OurDomain string
|
|
Destination string
|
|
MessageID string
|
|
Date string
|
|
To []string
|
|
FailedTo map[string]string
|
|
Recipients []*Recipient
|
|
FailedRecipients []*Recipient
|
|
PendingRecipients []*Recipient
|
|
OriginalMessage string
|
|
|
|
// Message-ID of the original message.
|
|
OriginalMessageID string
|
|
|
|
// MIME boundary to use to form the message.
|
|
Boundary string
|
|
}
|
|
|
|
// indent s with the given number of spaces.
|
|
func indent(sp int, s string) string {
|
|
pad := strings.Repeat(" ", sp)
|
|
return strings.Replace(s, "\n", "\n"+pad, -1)
|
|
}
|
|
|
|
var dsnTemplate = template.Must(
|
|
template.New("dsn").Funcs(
|
|
template.FuncMap{
|
|
"indent": indent,
|
|
"trim": strings.TrimSpace,
|
|
}).Parse(
|
|
`From: Mail Delivery System <postmaster-dsn@{{.OurDomain}}>
|
|
To: <{{.Destination}}>
|
|
Subject: Mail delivery failed: returning message to sender
|
|
Message-ID: <{{.MessageID}}>
|
|
Date: {{.Date}}
|
|
In-Reply-To: {{.OriginalMessageID}}
|
|
References: {{.OriginalMessageID}}
|
|
X-Failed-Recipients: {{range .FailedTo}}{{.}}, {{end}}
|
|
Auto-Submitted: auto-replied
|
|
MIME-Version: 1.0
|
|
Content-Type: multipart/report; report-type=delivery-status;
|
|
boundary="{{.Boundary}}"
|
|
|
|
|
|
--{{.Boundary}}
|
|
Content-Type: text/plain; charset="utf-8"
|
|
Content-Disposition: inline
|
|
Content-Description: Notification
|
|
Content-Transfer-Encoding: 8bit
|
|
|
|
Delivery of your message to the following recipient(s) failed permanently:
|
|
|
|
{{range .FailedTo}} - {{.}}
|
|
{{end}}
|
|
|
|
Technical details:
|
|
{{- range .FailedRecipients}}
|
|
- "{{.Address}}" ({{.Type}}) failed permanently with error:
|
|
{{.LastFailureMessage | trim | indent 4}}
|
|
{{- end}}
|
|
{{- range .PendingRecipients}}
|
|
- "{{.Address}}" ({{.Type}}) failed repeatedly and timed out, last error:
|
|
{{.LastFailureMessage | trim | indent 4}}
|
|
{{- end}}
|
|
|
|
|
|
--{{.Boundary}}
|
|
Content-Type: message/global-delivery-status
|
|
Content-Description: Delivery Report
|
|
Content-Transfer-Encoding: 8bit
|
|
|
|
Reporting-MTA: dns; {{.OurDomain}}
|
|
|
|
{{range .FailedRecipients -}}
|
|
Original-Recipient: utf-8; {{.OriginalAddress}}
|
|
Final-Recipient: utf-8; {{.Address}}
|
|
Action: failed
|
|
Status: 5.0.0
|
|
Diagnostic-Code: smtp; {{.LastFailureMessage | trim | indent 4}}
|
|
|
|
{{end -}}
|
|
{{range .PendingRecipients -}}
|
|
Original-Recipient: utf-8; {{.OriginalAddress}}
|
|
Final-Recipient: utf-8; {{.Address}}
|
|
Action: failed
|
|
Status: 4.0.0
|
|
Diagnostic-Code: smtp; {{.LastFailureMessage | trim | indent 4}}
|
|
|
|
{{end}}
|
|
|
|
--{{.Boundary}}
|
|
Content-Type: message/rfc822
|
|
Content-Description: Undelivered Message
|
|
Content-Transfer-Encoding: 8bit
|
|
|
|
{{.OriginalMessage}}
|
|
|
|
--{{.Boundary}}--
|
|
`))
|