1package queue
2
3import (
4 "bufio"
5 "context"
6 "fmt"
7 "os"
8 "time"
9
10 "golang.org/x/exp/slog"
11
12 "github.com/prometheus/client_golang/prometheus"
13 "github.com/prometheus/client_golang/prometheus/promauto"
14
15 "github.com/mjl-/mox/dns"
16 "github.com/mjl-/mox/dsn"
17 "github.com/mjl-/mox/message"
18 "github.com/mjl-/mox/mlog"
19 "github.com/mjl-/mox/mox-"
20 "github.com/mjl-/mox/smtp"
21 "github.com/mjl-/mox/store"
22)
23
24var (
25 metricDMARCReportFailure = promauto.NewCounter(
26 prometheus.CounterOpts{
27 Name: "mox_queue_dmarcreport_failure_total",
28 Help: "Permanent failures to deliver a DMARC report.",
29 },
30 )
31)
32
33func deliverDSNFailure(ctx context.Context, log mlog.Log, m Msg, remoteMTA dsn.NameIP, secodeOpt, errmsg string) {
34 const subject = "mail delivery failed"
35 message := fmt.Sprintf(`
36Delivery has failed permanently for your email to:
37
38 %s
39
40No further deliveries will be attempted.
41
42Error during the last delivery attempt:
43
44 %s
45`, m.Recipient().XString(m.SMTPUTF8), errmsg)
46
47 deliverDSN(ctx, log, m, remoteMTA, secodeOpt, errmsg, true, nil, subject, message)
48}
49
50func deliverDSNDelay(ctx context.Context, log mlog.Log, m Msg, remoteMTA dsn.NameIP, secodeOpt, errmsg string, retryUntil time.Time) {
51 // Should not happen, but doesn't hurt to prevent sending delayed delivery
52 // notifications for DMARC reports. We don't want to waste postmaster attention.
53 if m.IsDMARCReport {
54 return
55 }
56
57 const subject = "mail delivery delayed"
58 message := fmt.Sprintf(`
59Delivery has been delayed of your email to:
60
61 %s
62
63Next attempts to deliver: in 4 hours, 8 hours and 16 hours.
64If these attempts all fail, you will receive a notice.
65
66Error during the last delivery attempt:
67
68 %s
69`, m.Recipient().XString(false), errmsg)
70
71 deliverDSN(ctx, log, m, remoteMTA, secodeOpt, errmsg, false, &retryUntil, subject, message)
72}
73
74// We only queue DSNs for delivery failures for emails submitted by authenticated
75// users. So we are delivering to local users. ../rfc/5321:1466
76// ../rfc/5321:1494
77// ../rfc/7208:490
78func deliverDSN(ctx context.Context, log mlog.Log, m Msg, remoteMTA dsn.NameIP, secodeOpt, errmsg string, permanent bool, retryUntil *time.Time, subject, textBody string) {
79 kind := "delayed delivery"
80 if permanent {
81 kind = "failure"
82 }
83
84 qlog := func(text string, err error) {
85 log.Errorx("queue dsn: "+text+": sender will not be informed about dsn", err, slog.String("sender", m.Sender().XString(m.SMTPUTF8)), slog.String("kind", kind))
86 }
87
88 msgf, err := os.Open(m.MessagePath())
89 if err != nil {
90 qlog("opening queued message", err)
91 return
92 }
93 msgr := store.FileMsgReader(m.MsgPrefix, msgf)
94 defer func() {
95 err := msgr.Close()
96 log.Check(err, "closing message reader after queuing dsn")
97 }()
98 headers, err := message.ReadHeaders(bufio.NewReader(msgr))
99 if err != nil {
100 qlog("reading headers of queued message", err)
101 return
102 }
103
104 var action dsn.Action
105 var status string
106 if permanent {
107 status = "5."
108 action = dsn.Failed
109 } else {
110 action = dsn.Delayed
111 status = "4."
112 }
113 if secodeOpt != "" {
114 status += secodeOpt
115 } else {
116 status += "0.0"
117 }
118 diagCode := errmsg
119 if !dsn.HasCode(diagCode) {
120 diagCode = status + " " + errmsg
121 }
122
123 dsnMsg := &dsn.Message{
124 SMTPUTF8: m.SMTPUTF8,
125 From: smtp.Path{Localpart: "postmaster", IPDomain: dns.IPDomain{Domain: mox.Conf.Static.HostnameDomain}},
126 To: m.Sender(),
127 Subject: subject,
128 MessageID: mox.MessageIDGen(false),
129 References: m.MessageID,
130 TextBody: textBody,
131
132 ReportingMTA: mox.Conf.Static.HostnameDomain.ASCII,
133 ArrivalDate: m.Queued,
134
135 Recipients: []dsn.Recipient{
136 {
137 FinalRecipient: m.Recipient(),
138 Action: action,
139 Status: status,
140 RemoteMTA: remoteMTA,
141 DiagnosticCode: diagCode,
142 LastAttemptDate: *m.LastAttempt,
143 WillRetryUntil: retryUntil,
144 },
145 },
146
147 Original: headers,
148 }
149 msgData, err := dsnMsg.Compose(log, m.SMTPUTF8)
150 if err != nil {
151 qlog("composing dsn", err)
152 return
153 }
154
155 msgData = append([]byte("Return-Path: <"+dsnMsg.From.XString(m.SMTPUTF8)+">\r\n"), msgData...)
156
157 mailbox := "Inbox"
158 senderAccount := m.SenderAccount
159 if m.IsDMARCReport {
160 // senderAccount should already by postmaster, but doesn't hurt to ensure it.
161 senderAccount = mox.Conf.Static.Postmaster.Account
162 }
163 acc, err := store.OpenAccount(log, senderAccount)
164 if err != nil {
165 acc, err = store.OpenAccount(log, mox.Conf.Static.Postmaster.Account)
166 if err != nil {
167 qlog("looking up postmaster account after sender account was not found", err)
168 return
169 }
170 mailbox = mox.Conf.Static.Postmaster.Mailbox
171 }
172 defer func() {
173 err := acc.Close()
174 log.Check(err, "queue dsn: closing account", slog.String("sender", m.Sender().XString(m.SMTPUTF8)), slog.String("kind", kind))
175 }()
176
177 msgFile, err := store.CreateMessageTemp(log, "queue-dsn")
178 if err != nil {
179 qlog("creating temporary message file", err)
180 return
181 }
182 defer store.CloseRemoveTempFile(log, msgFile, "dsn message")
183
184 msgWriter := message.NewWriter(msgFile)
185 if _, err := msgWriter.Write(msgData); err != nil {
186 qlog("writing dsn message", err)
187 return
188 }
189
190 msg := &store.Message{
191 Received: time.Now(),
192 Size: msgWriter.Size,
193 MsgPrefix: []byte{},
194 }
195
196 // If this is a DMARC report, deliver it as seen message to a submailbox of the
197 // postmaster mailbox. We mark it as seen so it doesn't waste postmaster attention,
198 // but we deliver them so they can be checked in case of problems.
199 if m.IsDMARCReport {
200 mailbox = fmt.Sprintf("%s/dmarc", mox.Conf.Static.Postmaster.Mailbox)
201 msg.Seen = true
202 metricDMARCReportFailure.Inc()
203 log.Info("delivering dsn for failure to deliver outgoing dmarc report")
204 }
205
206 acc.WithWLock(func() {
207 if err := acc.DeliverMailbox(log, mailbox, msg, msgFile); err != nil {
208 qlog("delivering dsn to mailbox", err)
209 return
210 }
211 })
212}
213