1package queue
2
3import (
4 "bufio"
5 "context"
6 "errors"
7 "fmt"
8 "log/slog"
9 "net"
10 "os"
11 "strings"
12 "time"
13
14 "github.com/prometheus/client_golang/prometheus"
15 "github.com/prometheus/client_golang/prometheus/promauto"
16
17 "github.com/mjl-/bstore"
18
19 "github.com/mjl-/mox/dns"
20 "github.com/mjl-/mox/dsn"
21 "github.com/mjl-/mox/message"
22 "github.com/mjl-/mox/mlog"
23 "github.com/mjl-/mox/mox-"
24 "github.com/mjl-/mox/smtp"
25 "github.com/mjl-/mox/smtpclient"
26 "github.com/mjl-/mox/store"
27)
28
29var (
30 metricDMARCReportFailure = promauto.NewCounter(
31 prometheus.CounterOpts{
32 Name: "mox_queue_dmarcreport_failure_total",
33 Help: "Permanent failures to deliver a DMARC report.",
34 },
35 )
36)
37
38// todo: rename function, perhaps put some of the params in a delivery struct so we don't pass all the params all the time?
39func fail(ctx context.Context, qlog mlog.Log, msgs []*Msg, dialedIPs map[string][]net.IP, backoff time.Duration, remoteMTA dsn.NameIP, err error) {
40 // todo future: when we implement relaying, we should be able to send DSNs to non-local users. and possibly specify a null mailfrom. ../rfc/5321:1503
41 // todo future: when we implement relaying, and a dsn cannot be delivered, and requiretls was active, we cannot drop the message. instead deliver to local postmaster? though ../rfc/8689:383 may intend to say the dsn should be delivered without requiretls?
42 // todo future: when we implement smtp dsn extension, parameter RET=FULL must be disregarded for messages with REQUIRETLS. ../rfc/8689:379
43
44 m0 := msgs[0]
45
46 var smtpLines []string
47 var cerr smtpclient.Error
48 var permanent bool
49 var errmsg = err.Error()
50 var code int
51 var secodeOpt string
52 if errors.As(err, &cerr) {
53 if cerr.Line != "" {
54 smtpLines = append([]string{cerr.Line}, cerr.MoreLines...)
55 }
56 permanent = cerr.Permanent
57 code = cerr.Code
58 secodeOpt = cerr.Secode
59 }
60 qlog = qlog.With(
61 slog.Bool("permanent", permanent),
62 slog.Int("code", code),
63 slog.String("secode", secodeOpt),
64 )
65
66 ids := make([]int64, len(msgs))
67 for i, m := range msgs {
68 ids[i] = m.ID
69 }
70
71 if permanent || m0.MaxAttempts == 0 && m0.Attempts >= 8 || m0.MaxAttempts > 0 && m0.Attempts >= m0.MaxAttempts {
72 for _, m := range msgs {
73 qmlog := qlog.With(slog.Int64("msgid", m.ID), slog.Any("recipient", m.Recipient()))
74 qmlog.Errorx("permanent failure delivering from queue", err)
75 deliverDSNFailure(ctx, qmlog, *m, remoteMTA, secodeOpt, errmsg, smtpLines)
76 }
77 if err := queueDelete(context.Background(), ids...); err != nil {
78 qlog.Errorx("deleting messages from queue after permanent failure", err)
79 }
80 return
81 }
82
83 // All messages should have the same DialedIPs, so we can update them all at once.
84 qup := bstore.QueryDB[Msg](context.Background(), DB)
85 qup.FilterIDs(ids)
86 if _, xerr := qup.UpdateNonzero(Msg{LastError: errmsg, DialedIPs: dialedIPs}); err != nil {
87 qlog.Errorx("storing delivery error", xerr, slog.String("deliveryerror", errmsg))
88 }
89
90 if m0.Attempts == 5 {
91 // We've attempted deliveries at these intervals: 0, 7.5m, 15m, 30m, 1h, 2u.
92 // Let sender know delivery is delayed.
93
94 retryUntil := m0.LastAttempt.Add((4 + 8 + 16) * time.Hour)
95 for _, m := range msgs {
96 qmlog := qlog.With(slog.Int64("msgid", m.ID), slog.Any("recipient", m.Recipient()))
97 qmlog.Errorx("temporary failure delivering from queue, sending delayed dsn", err, slog.Duration("backoff", backoff))
98 deliverDSNDelay(ctx, qmlog, *m, remoteMTA, secodeOpt, errmsg, smtpLines, retryUntil)
99 }
100 } else {
101 for _, m := range msgs {
102 qlog.Errorx("temporary failure delivering from queue", err,
103 slog.Int64("msgid", m.ID),
104 slog.Any("recipient", m.Recipient()),
105 slog.Duration("backoff", backoff),
106 slog.Time("nextattempt", m0.NextAttempt))
107 }
108 }
109}
110
111func deliverDSNFailure(ctx context.Context, log mlog.Log, m Msg, remoteMTA dsn.NameIP, secodeOpt, errmsg string, smtpLines []string) {
112 const subject = "mail delivery failed"
113 message := fmt.Sprintf(`
114Delivery has failed permanently for your email to:
115
116 %s
117
118No further deliveries will be attempted.
119
120Error during the last delivery attempt:
121
122 %s
123`, m.Recipient().XString(m.SMTPUTF8), errmsg)
124 if len(smtpLines) > 0 {
125 message += "\nFull SMTP response:\n\n\t" + strings.Join(smtpLines, "\n\t") + "\n"
126 }
127
128 deliverDSN(ctx, log, m, remoteMTA, secodeOpt, errmsg, smtpLines, true, nil, subject, message)
129}
130
131func deliverDSNDelay(ctx context.Context, log mlog.Log, m Msg, remoteMTA dsn.NameIP, secodeOpt, errmsg string, smtpLines []string, retryUntil time.Time) {
132 // Should not happen, but doesn't hurt to prevent sending delayed delivery
133 // notifications for DMARC reports. We don't want to waste postmaster attention.
134 if m.IsDMARCReport {
135 return
136 }
137
138 const subject = "mail delivery delayed"
139 message := fmt.Sprintf(`
140Delivery has been delayed of your email to:
141
142 %s
143
144Next attempts to deliver: in 4 hours, 8 hours and 16 hours.
145If these attempts all fail, you will receive a notice.
146
147Error during the last delivery attempt:
148
149 %s
150`, m.Recipient().XString(false), errmsg)
151 if len(smtpLines) > 0 {
152 message += "\nFull SMTP response:\n\n\t" + strings.Join(smtpLines, "\n\t") + "\n"
153 }
154
155 deliverDSN(ctx, log, m, remoteMTA, secodeOpt, errmsg, smtpLines, false, &retryUntil, subject, message)
156}
157
158// We only queue DSNs for delivery failures for emails submitted by authenticated
159// users. So we are delivering to local users. ../rfc/5321:1466
160// ../rfc/5321:1494
161// ../rfc/7208:490
162func deliverDSN(ctx context.Context, log mlog.Log, m Msg, remoteMTA dsn.NameIP, secodeOpt, errmsg string, smtpLines []string, permanent bool, retryUntil *time.Time, subject, textBody string) {
163 kind := "delayed delivery"
164 if permanent {
165 kind = "failure"
166 }
167
168 qlog := func(text string, err error) {
169 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))
170 }
171
172 msgf, err := os.Open(m.MessagePath())
173 if err != nil {
174 qlog("opening queued message", err)
175 return
176 }
177 msgr := store.FileMsgReader(m.MsgPrefix, msgf)
178 defer func() {
179 err := msgr.Close()
180 log.Check(err, "closing message reader after queuing dsn")
181 }()
182 headers, err := message.ReadHeaders(bufio.NewReader(msgr))
183 if err != nil {
184 qlog("reading headers of queued message", err)
185 return
186 }
187
188 var action dsn.Action
189 var status string
190 if permanent {
191 status = "5."
192 action = dsn.Failed
193 } else {
194 action = dsn.Delayed
195 status = "4."
196 }
197 if secodeOpt != "" {
198 status += secodeOpt
199 } else {
200 status += "0.0"
201 }
202
203 // ../rfc/3461:1329
204 var smtpDiag string
205 if len(smtpLines) > 0 {
206 smtpDiag = "smtp; " + strings.Join(smtpLines, " ")
207 }
208
209 dsnMsg := &dsn.Message{
210 SMTPUTF8: m.SMTPUTF8,
211 From: smtp.Path{Localpart: "postmaster", IPDomain: dns.IPDomain{Domain: mox.Conf.Static.HostnameDomain}},
212 To: m.Sender(),
213 Subject: subject,
214 MessageID: mox.MessageIDGen(false),
215 References: m.MessageID,
216 TextBody: textBody,
217
218 ReportingMTA: mox.Conf.Static.HostnameDomain.ASCII,
219 ArrivalDate: m.Queued,
220 FutureReleaseRequest: m.FutureReleaseRequest,
221
222 Recipients: []dsn.Recipient{
223 {
224 FinalRecipient: m.Recipient(),
225 Action: action,
226 Status: status,
227 StatusComment: errmsg,
228 RemoteMTA: remoteMTA,
229 DiagnosticCode: smtpDiag,
230 LastAttemptDate: *m.LastAttempt,
231 WillRetryUntil: retryUntil,
232 },
233 },
234
235 Original: headers,
236 }
237 msgData, err := dsnMsg.Compose(log, m.SMTPUTF8)
238 if err != nil {
239 qlog("composing dsn", err)
240 return
241 }
242
243 prefix := []byte("Return-Path: <" + dsnMsg.From.XString(m.SMTPUTF8) + ">\r\n" + "Delivered-To: " + m.Sender().XString(m.SMTPUTF8) + "\r\n")
244 msgData = append(prefix, msgData...)
245
246 mailbox := "Inbox"
247 senderAccount := m.SenderAccount
248 if m.IsDMARCReport {
249 // senderAccount should already by postmaster, but doesn't hurt to ensure it.
250 senderAccount = mox.Conf.Static.Postmaster.Account
251 }
252 acc, err := store.OpenAccount(log, senderAccount)
253 if err != nil {
254 acc, err = store.OpenAccount(log, mox.Conf.Static.Postmaster.Account)
255 if err != nil {
256 qlog("looking up postmaster account after sender account was not found", err)
257 return
258 }
259 mailbox = mox.Conf.Static.Postmaster.Mailbox
260 }
261 defer func() {
262 err := acc.Close()
263 log.Check(err, "queue dsn: closing account", slog.String("sender", m.Sender().XString(m.SMTPUTF8)), slog.String("kind", kind))
264 }()
265
266 msgFile, err := store.CreateMessageTemp(log, "queue-dsn")
267 if err != nil {
268 qlog("creating temporary message file", err)
269 return
270 }
271 defer store.CloseRemoveTempFile(log, msgFile, "dsn message")
272
273 msgWriter := message.NewWriter(msgFile)
274 if _, err := msgWriter.Write(msgData); err != nil {
275 qlog("writing dsn message", err)
276 return
277 }
278
279 msg := &store.Message{
280 Received: time.Now(),
281 Size: msgWriter.Size,
282 MsgPrefix: []byte{},
283 DSN: true,
284 }
285
286 // If this is a DMARC report, deliver it as seen message to a submailbox of the
287 // postmaster mailbox. We mark it as seen so it doesn't waste postmaster attention,
288 // but we deliver them so they can be checked in case of problems.
289 if m.IsDMARCReport {
290 mailbox = fmt.Sprintf("%s/dmarc", mox.Conf.Static.Postmaster.Mailbox)
291 msg.Seen = true
292 metricDMARCReportFailure.Inc()
293 log.Info("delivering dsn for failure to deliver outgoing dmarc report")
294 }
295
296 acc.WithWLock(func() {
297 if err := acc.DeliverMailbox(log, mailbox, msg, msgFile); err != nil {
298 qlog("delivering dsn to mailbox", err)
299 return
300 }
301 })
302}
303