maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19// Package dsn contains the utilities used for dsn message (DSN) generation.
 20//
 21// It implements RFC 3464 and RFC 3462.
 22package dsn
 23
 24import (
 25	"errors"
 26	"fmt"
 27	"io"
 28	"strings"
 29	"text/template"
 30	"time"
 31
 32	"github.com/emersion/go-message/textproto"
 33	"github.com/emersion/go-smtp"
 34	"github.com/foxcpp/maddy/framework/address"
 35	"github.com/foxcpp/maddy/framework/dns"
 36)
 37
 38type ReportingMTAInfo struct {
 39	ReportingMTA    string
 40	ReceivedFromMTA string
 41
 42	// Message sender address, included as 'X-Maddy-Sender: rfc822; ADDR' field.
 43	XSender string
 44
 45	// Message identifier, included as 'X-Maddy-MsgId: MSGID' field.
 46	XMessageID string
 47
 48	// Time when message was enqueued for delivery by Reporting MTA.
 49	ArrivalDate time.Time
 50
 51	// Time when message delivery was attempted last time.
 52	LastAttemptDate time.Time
 53}
 54
 55func (info ReportingMTAInfo) WriteTo(utf8 bool, w io.Writer) error {
 56	// DSN format uses structure similar to MIME header, so we reuse
 57	// MIME generator here.
 58	h := textproto.Header{}
 59
 60	if info.ReportingMTA == "" {
 61		return errors.New("dsn: Reporting-MTA field is mandatory")
 62	}
 63
 64	reportingMTA, err := dns.SelectIDNA(utf8, info.ReportingMTA)
 65	if err != nil {
 66		return fmt.Errorf("dsn: cannot convert Reporting-MTA to a suitable representation: %w", err)
 67	}
 68
 69	h.Add("Reporting-MTA", "dns; "+reportingMTA)
 70
 71	if info.ReceivedFromMTA != "" {
 72		receivedFromMTA, err := dns.SelectIDNA(utf8, info.ReceivedFromMTA)
 73		if err != nil {
 74			return fmt.Errorf("dsn: cannot convert Received-From-MTA to a suitable representation: %w", err)
 75		}
 76
 77		h.Add("Received-From-MTA", "dns; "+receivedFromMTA)
 78	}
 79
 80	if info.XSender != "" {
 81		sender, err := address.SelectIDNA(utf8, info.XSender)
 82		if err != nil {
 83			return fmt.Errorf("dsn: cannot convert X-Maddy-Sender to a suitable representation: %w", err)
 84		}
 85
 86		if utf8 {
 87			h.Add("X-Maddy-Sender", "utf8; "+sender)
 88		} else {
 89			h.Add("X-Maddy-Sender", "rfc822; "+sender)
 90		}
 91	}
 92	if info.XMessageID != "" {
 93		h.Add("X-Maddy-MsgID", info.XMessageID)
 94	}
 95
 96	if !info.ArrivalDate.IsZero() {
 97		h.Add("Arrival-Date", info.ArrivalDate.Format("Mon, 2 Jan 2006 15:04:05 -0700"))
 98	}
 99	if !info.ArrivalDate.IsZero() {
100		h.Add("Last-Attempt-Date", info.LastAttemptDate.Format("Mon, 2 Jan 2006 15:04:05 -0700"))
101	}
102
103	return textproto.WriteHeader(w, h)
104}
105
106type Action string
107
108const (
109	ActionFailed    Action = "failed"
110	ActionDelayed   Action = "delayed"
111	ActionDelivered Action = "delivered"
112	ActionRelayed   Action = "relayed"
113	ActionExpanded  Action = "expanded"
114)
115
116type RecipientInfo struct {
117	FinalRecipient string
118	RemoteMTA      string
119
120	Action Action
121	Status smtp.EnhancedCode
122
123	// DiagnosticCode is the error that will be returned to the sender.
124	DiagnosticCode error
125}
126
127func (info RecipientInfo) WriteTo(utf8 bool, w io.Writer) error {
128	// DSN format uses structure similar to MIME header, so we reuse
129	// MIME generator here.
130	h := textproto.Header{}
131
132	if info.FinalRecipient == "" {
133		return errors.New("dsn: Final-Recipient is required")
134	}
135	finalRcpt, err := address.SelectIDNA(utf8, info.FinalRecipient)
136	if err != nil {
137		return fmt.Errorf("dsn: cannot convert Final-Recipient to a suitable representation: %w", err)
138	}
139	if utf8 {
140		h.Add("Final-Recipient", "utf8; "+finalRcpt)
141	} else {
142		h.Add("Final-Recipient", "rfc822; "+finalRcpt)
143	}
144
145	if info.Action == "" {
146		return errors.New("dsn: Action is required")
147	}
148	h.Add("Action", string(info.Action))
149	if info.Status[0] == 0 {
150		return errors.New("dsn: Status is required")
151	}
152	h.Add("Status", fmt.Sprintf("%d.%d.%d", info.Status[0], info.Status[1], info.Status[2]))
153
154	if smtpErr, ok := info.DiagnosticCode.(*smtp.SMTPError); ok {
155		// Error message may contain newlines if it is received from another SMTP server.
156		// But we cannot directly insert CR/LF into Disagnostic-Code so rewrite it.
157		h.Add("Diagnostic-Code", fmt.Sprintf("smtp; %d %d.%d.%d %s",
158			smtpErr.Code, smtpErr.EnhancedCode[0], smtpErr.EnhancedCode[1], smtpErr.EnhancedCode[2],
159			strings.ReplaceAll(strings.ReplaceAll(smtpErr.Message, "\n", " "), "\r", " ")))
160	} else if utf8 {
161		// It might contain Unicode, so don't include it if we are not allowed to.
162		// ... I didn't bother implementing mangling logic to remove Unicode
163		// characters.
164		errorDesc := info.DiagnosticCode.Error()
165		errorDesc = strings.ReplaceAll(strings.ReplaceAll(errorDesc, "\n", " "), "\r", " ")
166
167		h.Add("Diagnostic-Code", "X-Maddy; "+errorDesc)
168	}
169
170	if info.RemoteMTA != "" {
171		remoteMTA, err := dns.SelectIDNA(utf8, info.RemoteMTA)
172		if err != nil {
173			return fmt.Errorf("dsn: cannot convert Remote-MTA to a suitable representation: %w", err)
174		}
175
176		h.Add("Remote-MTA", "dns; "+remoteMTA)
177	}
178
179	return textproto.WriteHeader(w, h)
180}
181
182type Envelope struct {
183	MsgID string
184	From  string
185	To    string
186}
187
188// GenerateDSN is a top-level function that should be used for generation of the DSNs.
189//
190// DSN header will be returned, body itself will be written to outWriter.
191func GenerateDSN(utf8 bool, envelope Envelope, mtaInfo ReportingMTAInfo, rcptsInfo []RecipientInfo, failedHeader textproto.Header, outWriter io.Writer) (textproto.Header, error) {
192	partWriter := textproto.NewMultipartWriter(outWriter)
193
194	reportHeader := textproto.Header{}
195	reportHeader.Add("Date", time.Now().Format("Mon, 2 Jan 2006 15:04:05 -0700"))
196	reportHeader.Add("Message-Id", envelope.MsgID)
197	reportHeader.Add("Content-Transfer-Encoding", "8bit")
198	reportHeader.Add("Content-Type", "multipart/report; report-type=delivery-status; boundary="+partWriter.Boundary())
199	reportHeader.Add("MIME-Version", "1.0")
200	reportHeader.Add("Auto-Submitted", "auto-replied")
201	reportHeader.Add("To", envelope.To)
202	reportHeader.Add("From", envelope.From)
203	reportHeader.Add("Subject", "Undelivered Mail Returned to Sender")
204
205	defer partWriter.Close()
206
207	if err := writeHumanReadablePart(partWriter, mtaInfo, rcptsInfo); err != nil {
208		return textproto.Header{}, err
209	}
210	if err := writeMachineReadablePart(utf8, partWriter, mtaInfo, rcptsInfo); err != nil {
211		return textproto.Header{}, err
212	}
213	return reportHeader, writeHeader(utf8, partWriter, failedHeader)
214}
215
216func writeHeader(utf8 bool, w *textproto.MultipartWriter, header textproto.Header) error {
217	partHeader := textproto.Header{}
218	partHeader.Add("Content-Description", "Undelivered message header")
219	if utf8 {
220		partHeader.Add("Content-Type", "message/global-headers")
221	} else {
222		partHeader.Add("Content-Type", "message/rfc822-headers")
223	}
224	partHeader.Add("Content-Transfer-Encoding", "8bit")
225	headerWriter, err := w.CreatePart(partHeader)
226	if err != nil {
227		return err
228	}
229	return textproto.WriteHeader(headerWriter, header)
230}
231
232func writeMachineReadablePart(utf8 bool, w *textproto.MultipartWriter, mtaInfo ReportingMTAInfo, rcptsInfo []RecipientInfo) error {
233	machineHeader := textproto.Header{}
234	if utf8 {
235		machineHeader.Add("Content-Type", "message/global-delivery-status")
236	} else {
237		machineHeader.Add("Content-Type", "message/delivery-status")
238	}
239	machineHeader.Add("Content-Description", "Delivery report")
240	machineWriter, err := w.CreatePart(machineHeader)
241	if err != nil {
242		return err
243	}
244
245	// WriteTo will add an empty line after output.
246	if err := mtaInfo.WriteTo(utf8, machineWriter); err != nil {
247		return err
248	}
249
250	for _, rcpt := range rcptsInfo {
251		if err := rcpt.WriteTo(utf8, machineWriter); err != nil {
252			return err
253		}
254	}
255	return nil
256}
257
258// failedText is the text of the human-readable part of DSN.
259var failedText = template.Must(template.New("dsn-text").Parse(`
260This is the mail delivery system at {{.ReportingMTA}}.
261
262Unfortunately, your message could not be delivered to one or more
263recipients. The usual cause of this problem is invalid
264recipient address or maintenance at the recipient side.
265
266Contact the postmaster for further assistance, provide the Message ID (below):
267
268Message ID: {{.XMessageID}}
269Arrival: {{.ArrivalDate}}
270Last delivery attempt: {{.LastAttemptDate}}
271
272`))
273
274func writeHumanReadablePart(w *textproto.MultipartWriter, mtaInfo ReportingMTAInfo, rcptsInfo []RecipientInfo) error {
275	humanHeader := textproto.Header{}
276	humanHeader.Add("Content-Transfer-Encoding", "8bit")
277	humanHeader.Add("Content-Type", `text/plain; charset="utf-8"`)
278	humanHeader.Add("Content-Description", "Notification")
279	humanWriter, err := w.CreatePart(humanHeader)
280	if err != nil {
281		return err
282	}
283
284	mtaInfo.ArrivalDate = mtaInfo.ArrivalDate.Truncate(time.Second)
285	mtaInfo.LastAttemptDate = mtaInfo.LastAttemptDate.Truncate(time.Second)
286
287	if err := failedText.Execute(humanWriter, mtaInfo); err != nil {
288		return err
289	}
290
291	for _, rcpt := range rcptsInfo {
292		if _, err := fmt.Fprintf(humanWriter, "Delivery to %s failed with error: %v\n", rcpt.FinalRecipient, rcpt.DiagnosticCode); err != nil {
293			return err
294		}
295	}
296
297	return nil
298}