1package dsn
2
3import (
4 "bytes"
5 "fmt"
6 "io"
7 "net"
8 "reflect"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/mjl-/mox/dns"
14 "github.com/mjl-/mox/message"
15 "github.com/mjl-/mox/mlog"
16 "github.com/mjl-/mox/smtp"
17)
18
19var pkglog = mlog.New("dsn", nil)
20
21func xparseDomain(s string) dns.Domain {
22 d, err := dns.ParseDomain(s)
23 if err != nil {
24 panic(fmt.Sprintf("parsing domain %q: %v", s, err))
25 }
26 return d
27}
28
29func xparseIPDomain(s string) dns.IPDomain {
30 return dns.IPDomain{Domain: xparseDomain(s)}
31}
32
33func tparseMessage(t *testing.T, data []byte, nparts int) (*Message, *message.Part) {
34 t.Helper()
35 m, p, err := Parse(pkglog.Logger, bytes.NewReader(data))
36 if err != nil {
37 t.Fatalf("parsing dsn: %v", err)
38 }
39 if len(p.Parts) != nparts {
40 t.Fatalf("got %d parts, expected %d", len(p.Parts), nparts)
41 }
42 return m, p
43}
44
45func tcheckType(t *testing.T, p *message.Part, mt, mst, cte string) {
46 t.Helper()
47 if !strings.EqualFold(p.MediaType, mt) {
48 t.Fatalf("got mediatype %q, expected %q", p.MediaType, mt)
49 }
50 if !strings.EqualFold(p.MediaSubType, mst) {
51 t.Fatalf("got mediasubtype %q, expected %q", p.MediaSubType, mst)
52 }
53 if !strings.EqualFold(p.ContentTransferEncoding, cte) {
54 t.Fatalf("got content-transfer-encoding %q, expected %q", p.ContentTransferEncoding, cte)
55 }
56}
57
58func tcompare(t *testing.T, got, exp any) {
59 t.Helper()
60 if !reflect.DeepEqual(got, exp) {
61 t.Fatalf("got %#v, expected %#v", got, exp)
62 }
63}
64
65func tcompareReader(t *testing.T, r io.Reader, exp []byte) {
66 t.Helper()
67 buf, err := io.ReadAll(r)
68 if err != nil {
69 t.Fatalf("data read, got %q, expected %q", buf, exp)
70 }
71}
72
73func TestDSN(t *testing.T) {
74 log := mlog.New("dsn", nil)
75
76 now := time.Now()
77
78 // An ascii-only message.
79 m := Message{
80 SMTPUTF8: false,
81
82 From: smtp.Path{Localpart: "postmaster", IPDomain: xparseIPDomain("mox.example")},
83 To: smtp.Path{Localpart: "mjl", IPDomain: xparseIPDomain("remote.example")},
84 Subject: "dsn",
85 MessageID: "test@localhost",
86 TextBody: "delivery failure\n",
87
88 ReportingMTA: "mox.example",
89 ReceivedFromMTA: smtp.Ehlo{Name: xparseIPDomain("relay.example"), ConnIP: net.ParseIP("10.10.10.10")},
90 ArrivalDate: now,
91 FutureReleaseRequest: "for;123",
92
93 Recipients: []Recipient{
94 {
95 FinalRecipient: smtp.Path{Localpart: "mjl", IPDomain: xparseIPDomain("remote.example")},
96 Action: Failed,
97 Status: "5.0.0",
98 LastAttemptDate: now,
99 },
100 },
101
102 Original: []byte("Subject: test\r\n"),
103 }
104 msgbuf, err := m.Compose(log, false)
105 if err != nil {
106 t.Fatalf("composing dsn: %v", err)
107 }
108
109 pmsg, part := tparseMessage(t, msgbuf, 3)
110 tcheckType(t, part, "multipart", "report", "")
111 tcheckType(t, &part.Parts[0], "text", "plain", "7bit")
112 tcheckType(t, &part.Parts[1], "message", "delivery-status", "7bit")
113 tcheckType(t, &part.Parts[2], "text", "rfc822-headers", "7bit")
114 tcompare(t, part.Parts[2].ContentTypeParams["charset"], "")
115 tcompareReader(t, part.Parts[2].Reader(), m.Original)
116 tcompare(t, pmsg.Recipients[0].FinalRecipient, m.Recipients[0].FinalRecipient)
117 // todo: test more fields
118
119 msgbufutf8, err := m.Compose(log, true)
120 if err != nil {
121 t.Fatalf("composing dsn with utf-8: %v", err)
122 }
123 pmsg, part = tparseMessage(t, msgbufutf8, 3)
124 tcheckType(t, part, "multipart", "report", "")
125 tcheckType(t, &part.Parts[0], "text", "plain", "7bit")
126 tcheckType(t, &part.Parts[1], "message", "delivery-status", "7bit")
127 tcheckType(t, &part.Parts[2], "text", "rfc822-headers", "7bit")
128 tcompare(t, part.Parts[2].ContentTypeParams["charset"], "")
129 tcompareReader(t, part.Parts[2].Reader(), m.Original)
130 tcompare(t, pmsg.Recipients[0].FinalRecipient, m.Recipients[0].FinalRecipient)
131
132 // An utf-8 message.
133 m = Message{
134 SMTPUTF8: true,
135
136 From: smtp.Path{Localpart: "postmæster", IPDomain: xparseIPDomain("møx.example")},
137 To: smtp.Path{Localpart: "møx", IPDomain: xparseIPDomain("remøte.example")},
138 Subject: "dsn¡",
139 MessageID: "test@localhost",
140 TextBody: "delivery failure¿\n",
141
142 ReportingMTA: "mox.example",
143 ReceivedFromMTA: smtp.Ehlo{Name: xparseIPDomain("reläy.example"), ConnIP: net.ParseIP("10.10.10.10")},
144 ArrivalDate: now,
145
146 Recipients: []Recipient{
147 {
148 Action: Failed,
149 FinalRecipient: smtp.Path{Localpart: "møx", IPDomain: xparseIPDomain("remøte.example")},
150 Status: "5.0.0",
151 LastAttemptDate: now,
152 },
153 },
154
155 Original: []byte("Subject: tést\r\n"),
156 }
157 msgbuf, err = m.Compose(log, false)
158 if err != nil {
159 t.Fatalf("composing utf-8 dsn without utf-8 support: %v", err)
160 }
161 pmsg, part = tparseMessage(t, msgbuf, 3)
162 tcheckType(t, part, "multipart", "report", "")
163 tcheckType(t, &part.Parts[0], "text", "plain", "7bit")
164 tcheckType(t, &part.Parts[1], "message", "delivery-status", "7bit")
165 tcheckType(t, &part.Parts[2], "text", "rfc822-headers", "base64")
166 tcompare(t, part.Parts[2].ContentTypeParams["charset"], "utf-8")
167 tcompareReader(t, part.Parts[2].Reader(), m.Original)
168 tcompare(t, pmsg.Recipients[0].FinalRecipient, m.Recipients[0].FinalRecipient)
169
170 msgbufutf8, err = m.Compose(log, true)
171 if err != nil {
172 t.Fatalf("composing utf-8 dsn with utf-8 support: %v", err)
173 }
174 pmsg, part = tparseMessage(t, msgbufutf8, 3)
175 tcheckType(t, part, "multipart", "report", "")
176 tcheckType(t, &part.Parts[0], "text", "plain", "8bit")
177 tcheckType(t, &part.Parts[1], "message", "global-delivery-status", "8bit")
178 tcheckType(t, &part.Parts[2], "message", "global-headers", "8bit")
179 tcompare(t, part.Parts[2].ContentTypeParams["charset"], "")
180 tcompareReader(t, part.Parts[2].Reader(), m.Original)
181 tcompare(t, pmsg.Recipients[0].FinalRecipient, m.Recipients[0].FinalRecipient)
182
183 // Now a message without 3rd multipart.
184 m.Original = nil
185 msgbufutf8, err = m.Compose(log, true)
186 if err != nil {
187 t.Fatalf("composing utf-8 dsn with utf-8 support: %v", err)
188 }
189 pmsg, part = tparseMessage(t, msgbufutf8, 2)
190 tcheckType(t, part, "multipart", "report", "")
191 tcheckType(t, &part.Parts[0], "text", "plain", "8bit")
192 tcheckType(t, &part.Parts[1], "message", "global-delivery-status", "8bit")
193 tcompare(t, pmsg.Recipients[0].FinalRecipient, m.Recipients[0].FinalRecipient)
194}
195