maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package testutils
 20
 21import (
 22	"context"
 23	"crypto/sha1"
 24	"encoding/hex"
 25	"errors"
 26	"io"
 27	"reflect"
 28	"sort"
 29	"testing"
 30
 31	"github.com/emersion/go-message/textproto"
 32	"github.com/emersion/go-smtp"
 33	"github.com/foxcpp/maddy/framework/buffer"
 34	"github.com/foxcpp/maddy/framework/config"
 35	"github.com/foxcpp/maddy/framework/exterrors"
 36	"github.com/foxcpp/maddy/framework/module"
 37)
 38
 39type Msg struct {
 40	MsgMeta  *module.MsgMetadata
 41	MailFrom string
 42	RcptTo   []string
 43	Body     []byte
 44	Header   textproto.Header
 45}
 46
 47type Target struct {
 48	Messages        []Msg
 49	DiscardMessages bool
 50
 51	StartErr       error
 52	RcptErr        map[string]error
 53	BodyErr        error
 54	PartialBodyErr map[string]error
 55	AbortErr       error
 56	CommitErr      error
 57
 58	InstName string
 59}
 60
 61/*
 62module.Module is implemented with dummy functions for logging done by MsgPipeline code.
 63*/
 64
 65func (dt Target) Init(*config.Map) error {
 66	return nil
 67}
 68
 69func (dt Target) InstanceName() string {
 70	if dt.InstName != "" {
 71		return dt.InstName
 72	}
 73	return "test_instance"
 74}
 75
 76func (dt Target) Name() string {
 77	return "test_target"
 78}
 79
 80type testTargetDelivery struct {
 81	msg Msg
 82	tgt *Target
 83}
 84
 85type testTargetDeliveryPartial struct {
 86	testTargetDelivery
 87}
 88
 89func (dt *Target) Start(ctx context.Context, msgMeta *module.MsgMetadata, mailFrom string) (module.Delivery, error) {
 90	if dt.PartialBodyErr != nil {
 91		return &testTargetDeliveryPartial{
 92			testTargetDelivery: testTargetDelivery{
 93				tgt: dt,
 94				msg: Msg{MsgMeta: msgMeta, MailFrom: mailFrom},
 95			},
 96		}, dt.StartErr
 97	}
 98	return &testTargetDelivery{
 99		tgt: dt,
100		msg: Msg{MsgMeta: msgMeta, MailFrom: mailFrom},
101	}, dt.StartErr
102}
103
104func (dtd *testTargetDelivery) AddRcpt(ctx context.Context, to string, _ smtp.RcptOptions) error {
105	if dtd.tgt.RcptErr != nil {
106		if err := dtd.tgt.RcptErr[to]; err != nil {
107			return err
108		}
109	}
110
111	dtd.msg.RcptTo = append(dtd.msg.RcptTo, to)
112	return nil
113}
114
115func (dtd *testTargetDeliveryPartial) BodyNonAtomic(ctx context.Context, c module.StatusCollector, header textproto.Header, buf buffer.Buffer) {
116	if dtd.tgt.PartialBodyErr != nil {
117		for rcpt, err := range dtd.tgt.PartialBodyErr {
118			c.SetStatus(rcpt, err)
119		}
120		return
121	}
122
123	dtd.msg.Header = header
124
125	body, err := buf.Open()
126	if err != nil {
127		for rcpt, err := range dtd.tgt.PartialBodyErr {
128			c.SetStatus(rcpt, err)
129		}
130		return
131	}
132	defer body.Close()
133
134	dtd.msg.Body, err = io.ReadAll(body)
135	if err != nil {
136		for rcpt, err := range dtd.tgt.PartialBodyErr {
137			c.SetStatus(rcpt, err)
138		}
139	}
140}
141
142func (dtd *testTargetDelivery) Body(ctx context.Context, header textproto.Header, buf buffer.Buffer) error {
143	if dtd.tgt.PartialBodyErr != nil {
144		return errors.New("partial failure occurred, no additional information available")
145	}
146	if dtd.tgt.BodyErr != nil {
147		return dtd.tgt.BodyErr
148	}
149
150	dtd.msg.Header = header
151
152	body, err := buf.Open()
153	if err != nil {
154		return err
155	}
156	defer body.Close()
157
158	if dtd.tgt.DiscardMessages {
159		// Don't bother.
160		_, err = io.Copy(io.Discard, body)
161		return err
162	}
163
164	dtd.msg.Body, err = io.ReadAll(body)
165	return err
166}
167
168func (dtd *testTargetDelivery) Abort(ctx context.Context) error {
169	return dtd.tgt.AbortErr
170}
171
172func (dtd *testTargetDelivery) Commit(ctx context.Context) error {
173	if dtd.tgt.CommitErr != nil {
174		return dtd.tgt.CommitErr
175	}
176	if dtd.tgt.DiscardMessages {
177		return nil
178	}
179	dtd.tgt.Messages = append(dtd.tgt.Messages, dtd.msg)
180	return nil
181}
182
183func DoTestDelivery(t *testing.T, tgt module.DeliveryTarget, from string, to []string) string {
184	t.Helper()
185	return DoTestDeliveryMeta(t, tgt, from, to, &module.MsgMetadata{
186		OriginalFrom: from,
187	})
188}
189
190func DoTestDeliveryMeta(t *testing.T, tgt module.DeliveryTarget, from string, to []string, msgMeta *module.MsgMetadata) string {
191	t.Helper()
192
193	id, err := DoTestDeliveryErrMeta(t, tgt, from, to, msgMeta)
194	if err != nil {
195		t.Fatalf("Unexpected error: %v", err)
196	}
197	return id
198}
199
200func DoTestDeliveryNonAtomic(t *testing.T, c module.StatusCollector, tgt module.DeliveryTarget, from string, to []string) string {
201	t.Helper()
202
203	IDRaw := sha1.Sum([]byte(t.Name()))
204	encodedID := hex.EncodeToString(IDRaw[:])
205
206	testCtx := context.Background()
207
208	body := buffer.MemoryBuffer{Slice: []byte("foobar\r\n")}
209	msgMeta := module.MsgMetadata{
210		DontTraceSender: true,
211		ID:              encodedID,
212		OriginalFrom:    from,
213	}
214	t.Log("-- tgt.Start", from)
215	delivery, err := tgt.Start(testCtx, &msgMeta, from)
216	if err != nil {
217		t.Log("-- ... tgt.Start", from, err, exterrors.Fields(err))
218		t.Fatalf("Unexpected err: %v %+v", err, exterrors.Fields(err))
219		return encodedID
220	}
221	for _, rcpt := range to {
222		t.Log("-- delivery.AddRcpt", rcpt)
223		if err := delivery.AddRcpt(testCtx, rcpt, smtp.RcptOptions{}); err != nil {
224			t.Log("-- ... delivery.AddRcpt", rcpt, err, exterrors.Fields(err))
225			t.Log("-- delivery.Abort")
226			if err := delivery.Abort(testCtx); err != nil {
227				t.Log("-- delivery.Abort:", err, exterrors.Fields(err))
228			}
229			t.Fatalf("Unexpected err: %v %+v", err, exterrors.Fields(err))
230			return encodedID
231		}
232	}
233	t.Log("-- delivery.BodyNonAtomic")
234	hdr := textproto.Header{}
235	hdr.Add("B", "2")
236	hdr.Add("A", "1")
237	delivery.(module.PartialDelivery).BodyNonAtomic(testCtx, c, hdr, body)
238	t.Log("-- delivery.Commit")
239	if err := delivery.Commit(testCtx); err != nil {
240		t.Fatalf("Unexpected err: %v %+v", err, exterrors.Fields(err))
241	}
242
243	return encodedID
244}
245
246const DeliveryData = "A: 1\r\n" +
247	"B: 2\r\n" +
248	"\r\n" +
249	"foobar\r\n"
250
251func DoTestDeliveryErr(t *testing.T, tgt module.DeliveryTarget, from string, to []string) (string, error) {
252	return DoTestDeliveryErrMeta(t, tgt, from, to, &module.MsgMetadata{})
253}
254
255func DoTestDeliveryErrMeta(t *testing.T, tgt module.DeliveryTarget, from string, to []string, msgMeta *module.MsgMetadata) (string, error) {
256	t.Helper()
257
258	IDRaw := sha1.Sum([]byte(t.Name()))
259	encodedID := hex.EncodeToString(IDRaw[:])
260	testCtx := context.Background()
261
262	body := buffer.MemoryBuffer{Slice: []byte("foobar\r\n")}
263	msgMeta.DontTraceSender = true
264	msgMeta.ID = encodedID
265	t.Log("-- tgt.Start", from)
266	delivery, err := tgt.Start(testCtx, msgMeta, from)
267	if err != nil {
268		t.Log("-- ... tgt.Start", from, err, exterrors.Fields(err))
269		return encodedID, err
270	}
271	for _, rcpt := range to {
272		t.Log("-- delivery.AddRcpt", rcpt)
273		if err := delivery.AddRcpt(testCtx, rcpt, smtp.RcptOptions{}); err != nil {
274			t.Log("-- ... delivery.AddRcpt", rcpt, err, exterrors.Fields(err))
275			t.Log("-- delivery.Abort")
276			if err := delivery.Abort(testCtx); err != nil {
277				t.Log("-- delivery.Abort:", err, exterrors.Fields(err))
278			}
279			return encodedID, err
280		}
281	}
282	t.Log("-- delivery.Body")
283	hdr := textproto.Header{}
284	hdr.Add("B", "2")
285	hdr.Add("A", "1")
286	if err := delivery.Body(testCtx, hdr, body); err != nil {
287		t.Log("-- ... delivery.Body", err, exterrors.Fields(err))
288		t.Log("-- delivery.Abort")
289		if err := delivery.Abort(testCtx); err != nil {
290			t.Log("-- ... delivery.Abort:", err, exterrors.Fields(err))
291		}
292		return encodedID, err
293	}
294	t.Log("-- delivery.Commit")
295	if err := delivery.Commit(testCtx); err != nil {
296		t.Log("-- ... delivery.Commit", err, exterrors.Fields(err))
297		return encodedID, err
298	}
299
300	return encodedID, err
301}
302
303func CheckTestMessage(t *testing.T, tgt *Target, indx int, sender string, rcpt []string) {
304	t.Helper()
305
306	if len(tgt.Messages) <= indx {
307		t.Errorf("wrong amount of messages received, want at least %d, got %d", indx+1, len(tgt.Messages))
308		return
309	}
310	msg := tgt.Messages[indx]
311
312	CheckMsg(t, &msg, sender, rcpt)
313}
314
315func CheckMsg(t *testing.T, msg *Msg, sender string, rcpt []string) {
316	t.Helper()
317
318	idRaw := sha1.Sum([]byte(t.Name()))
319	encodedId := hex.EncodeToString(idRaw[:])
320
321	CheckMsgID(t, msg, sender, rcpt, encodedId)
322}
323
324func CheckMsgID(t *testing.T, msg *Msg, sender string, rcpt []string, id string) string {
325	t.Helper()
326
327	if msg.MsgMeta.ID != id && id != "" {
328		t.Errorf("empty or wrong delivery context for passed message? %+v", msg.MsgMeta)
329	}
330	if msg.MailFrom != sender {
331		t.Errorf("wrong sender, want %s, got %s", sender, msg.MailFrom)
332	}
333
334	sort.Strings(rcpt)
335	sort.Strings(msg.RcptTo)
336	if !reflect.DeepEqual(msg.RcptTo, rcpt) {
337		t.Errorf("wrong recipients, want %v, got %v", rcpt, msg.RcptTo)
338	}
339	if string(msg.Body) != "foobar\r\n" {
340		t.Errorf("wrong body, want '%s', got '%s' (%v)", "foobar\r\n", string(msg.Body), msg.Body)
341	}
342
343	return msg.MsgMeta.ID
344}