2 // Copyright 2020 Blue Static <https://www.bluestatic.org>
3 // This program is free software licensed under the GNU General Public License,
4 // version 3.0. The full text of the license can be found in LICENSE.txt.
5 // SPDX-License-Identifier: GPL-3.0-only
23 type deliveryServer struct {
28 func (s *deliveryServer) OnMessageDelivered(env Envelope) *ReplyLine {
29 s.messages = append(s.messages, env)
33 func TestRelayRoundTrip(t *testing.T) {
35 testServer: testServer{domain: "receive.net"},
41 MailFrom: mail.Address{Address: "from@sender.org"},
42 RcptTo: []mail.Address{{Address: "to@receive.net"}},
43 Data: []byte("~~~Message~~~\n"),
47 host, port, _ := net.SplitHostPort(l.Addr().String())
48 relayMessageToHost(s, env, zap.NewNop(), env.RcptTo[0].Address, host, port)
50 if len(s.messages) != 1 {
51 t.Errorf("Expected 1 message to be delivered, got %d", len(s.messages))
55 received := s.messages[0]
57 if env.MailFrom.Address != received.MailFrom.Address {
58 t.Errorf("Expected MailFrom %s, got %s", env.MailFrom.Address, received.MailFrom.Address)
60 if len(received.RcptTo) != 1 {
61 t.Errorf("Expected 1 RcptTo, got %d", len(received.RcptTo))
64 if env.RcptTo[0].Address != received.RcptTo[0].Address {
65 t.Errorf("Expected RcptTo %s, got %s", env.RcptTo[0].Address, received.RcptTo[0].Address)
68 if !bytes.HasSuffix(received.Data, env.Data) {
69 t.Errorf("Delivered message does not match relayed one. Delivered=%q Relayed=%q", string(env.Data), string(received.Data))
73 func TestDeliveryFailureMessage(t *testing.T) {
74 s := &deliveryServer{}
77 MailFrom: mail.Address{Address: "from@sender.org"},
78 RcptTo: []mail.Address{{Address: "to@receive.net"}},
79 Data: []byte("Message\n"),
81 EHLO: "mx.receive.net",
82 RemoteAddr: &net.IPAddr{net.IPv4(127, 0, 0, 1), ""},
85 errorStr1 := "internal message"
86 errorStr2 := "general error 122"
87 deliverRelayFailure(s, env, zap.NewNop(), env.RcptTo[0].Address, errorStr1, fmt.Errorf(errorStr2))
89 if len(s.messages) != 1 {
90 t.Errorf("Expected 1 failure notification, got %d", len(s.messages))
94 failure := s.messages[0]
96 if failure.RcptTo[0].Address != env.MailFrom.Address {
97 t.Errorf("Failure message should be delivered to sender %s, actually %s", env.MailFrom.Address, failure.RcptTo[0].Address)
100 // Read the failure message.
101 buf := bytes.NewBuffer(failure.Data)
102 msg, err := mail.ReadMessage(buf)
104 t.Errorf("Failed to read message: %v", err)
108 // Parse out the Content-Type to get the multipart boundary string.
109 mediatype, mtheaders, err := mime.ParseMediaType(msg.Header["Content-Type"][0])
111 t.Errorf("Failed to parse MIME headers: %v", err)
115 expected := "multipart/report"
116 if mediatype != expected {
117 t.Errorf("Expected MIME type of %q, got %q", expected, mediatype)
120 expected = "delivery-status"
121 if mtheaders["report-type"] != expected {
122 t.Errorf("Expected report-type of %q, got %q", expected, mtheaders["report-type"])
125 boundary := mtheaders["boundary"]
127 expected = "Delivery Status Notification (Failure)"
128 if msg.Header["Subject"][0] != expected {
129 t.Errorf("Subject did not match %q, got %q", expected, mtheaders["Subject"])
132 if msg.Header["To"][0] != "<"+env.MailFrom.Address+">" {
133 t.Errorf("To field does not match %q, got %q", env.MailFrom.Address, msg.Header["To"][0])
136 // Parse the multi-part messsage.
137 mpr := multipart.NewReader(msg.Body, boundary)
138 part, err := mpr.NextPart()
140 t.Errorf("Error reading part 0: %v", err)
144 // First part is the human-readable error.
145 expected = "text/plain; charset=UTF-8"
146 if part.Header["Content-Type"][0] != expected {
147 t.Errorf("Part 0 type expected %q, got %q", expected, part.Header["Content-Type"][0])
150 content, err := ioutil.ReadAll(part)
152 t.Errorf("Failed to read part 0 content: %v", err)
155 contentStr := string(content)
157 if !strings.Contains(contentStr, "Delivery Failure") {
158 t.Errorf("Missing Delivery Failure")
161 expected = fmt.Sprintf("%s:\n%s", errorStr1, errorStr2)
162 if !strings.Contains(contentStr, expected) {
163 t.Errorf("Missing error string %q", expected)
166 // Second part is the status information.
167 part, err = mpr.NextPart()
169 t.Errorf("Error reading part 1: %v", err)
173 expected = "message/delivery-status"
174 if part.Header["Content-Type"][0] != expected {
175 t.Errorf("Part 1 type expected %q, got %q", expected, part.Header["Content-Type"][0])
178 content, err = ioutil.ReadAll(part)
180 t.Errorf("Failed to read part 1 content: %v", err)
183 contentStr = string(content)
185 expected = "Original-Envelope-ID: " + env.ID + "\n"
186 if !strings.Contains(contentStr, expected) {
187 t.Errorf("Missing %q in %q", expected, contentStr)
190 expected = "Reporting-UA: " + env.EHLO + "\n"
191 if !strings.Contains(contentStr, expected) {
192 t.Errorf("Missing %q in %q", expected, contentStr)
195 expected = "Reporting-MTA: dns; localhost [127.0.0.1]\n"
196 if !strings.Contains(contentStr, expected) {
197 t.Errorf("Missing %q in %q", expected, contentStr)
200 // Third part is the original message.
201 part, err = mpr.NextPart()
203 t.Errorf("Error reading part 2: %v", err)
207 expected = "message/rfc822"
208 if part.Header["Content-Type"][0] != expected {
209 t.Errorf("Part 2 type expected %q, got %q", expected, part.Header["Content-Type"][0])
212 content, err = ioutil.ReadAll(part)
214 t.Errorf("Failed to read part 2 content: %v", err)
218 if !bytes.Equal(content, env.Data) {
219 t.Errorf("Byte content of original message does not match")