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 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}}-- `))