1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819// Package dsn contains the utilities used for dsn message (DSN) generation.20//21// It implements RFC 3464 and RFC 3462.22package dsn2324import (25 "errors"26 "fmt"27 "io"28 "strings"29 "text/template"30 "time"3132 "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)3738type ReportingMTAInfo struct {39 ReportingMTA string40 ReceivedFromMTA string4142 // Message sender address, included as 'X-Maddy-Sender: rfc822; ADDR' field.43 XSender string4445 // Message identifier, included as 'X-Maddy-MsgId: MSGID' field.46 XMessageID string4748 // Time when message was enqueued for delivery by Reporting MTA.49 ArrivalDate time.Time5051 // Time when message delivery was attempted last time.52 LastAttemptDate time.Time53}5455func (info ReportingMTAInfo) WriteTo(utf8 bool, w io.Writer) error {56 // DSN format uses structure similar to MIME header, so we reuse57 // MIME generator here.58 h := textproto.Header{}5960 if info.ReportingMTA == "" {61 return errors.New("dsn: Reporting-MTA field is mandatory")62 }6364 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 }6869 h.Add("Reporting-MTA", "dns; "+reportingMTA)7071 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 }7677 h.Add("Received-From-MTA", "dns; "+receivedFromMTA)78 }7980 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 }8586 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 }9596 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 }102103 return textproto.WriteHeader(w, h)104}105106type Action string107108const (109 ActionFailed Action = "failed"110 ActionDelayed Action = "delayed"111 ActionDelivered Action = "delivered"112 ActionRelayed Action = "relayed"113 ActionExpanded Action = "expanded"114)115116type RecipientInfo struct {117 FinalRecipient string118 RemoteMTA string119120 Action Action121 Status smtp.EnhancedCode122123 // DiagnosticCode is the error that will be returned to the sender.124 DiagnosticCode error125}126127func (info RecipientInfo) WriteTo(utf8 bool, w io.Writer) error {128 // DSN format uses structure similar to MIME header, so we reuse129 // MIME generator here.130 h := textproto.Header{}131132 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 }144145 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]))153154 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 Unicode163 // characters.164 errorDesc := info.DiagnosticCode.Error()165 errorDesc = strings.ReplaceAll(strings.ReplaceAll(errorDesc, "\n", " "), "\r", " ")166167 h.Add("Diagnostic-Code", "X-Maddy; "+errorDesc)168 }169170 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 }175176 h.Add("Remote-MTA", "dns; "+remoteMTA)177 }178179 return textproto.WriteHeader(w, h)180}181182type Envelope struct {183 MsgID string184 From string185 To string186}187188// 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)193194 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")204205 defer partWriter.Close()206207 if err := writeHumanReadablePart(partWriter, mtaInfo, rcptsInfo); err != nil {208 return textproto.Header{}, err209 }210 if err := writeMachineReadablePart(utf8, partWriter, mtaInfo, rcptsInfo); err != nil {211 return textproto.Header{}, err212 }213 return reportHeader, writeHeader(utf8, partWriter, failedHeader)214}215216func 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 err228 }229 return textproto.WriteHeader(headerWriter, header)230}231232func 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 err243 }244245 // WriteTo will add an empty line after output.246 if err := mtaInfo.WriteTo(utf8, machineWriter); err != nil {247 return err248 }249250 for _, rcpt := range rcptsInfo {251 if err := rcpt.WriteTo(utf8, machineWriter); err != nil {252 return err253 }254 }255 return nil256}257258// 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}}.261262Unfortunately, your message could not be delivered to one or more263recipients. The usual cause of this problem is invalid264recipient address or maintenance at the recipient side.265266Contact the postmaster for further assistance, provide the Message ID (below):267268Message ID: {{.XMessageID}}269Arrival: {{.ArrivalDate}}270Last delivery attempt: {{.LastAttemptDate}}271272`))273274func 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 err282 }283284 mtaInfo.ArrivalDate = mtaInfo.ArrivalDate.Truncate(time.Second)285 mtaInfo.LastAttemptDate = mtaInfo.LastAttemptDate.Truncate(time.Second)286287 if err := failedText.Execute(humanWriter, mtaInfo); err != nil {288 return err289 }290291 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 err294 }295 }296297 return nil298}