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