git » chasquid » main » tree

[main] / internal / queue / dsn.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}}--
`))