1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819package testutils2021import (22 "context"23 "crypto/sha1"24 "encoding/hex"25 "errors"26 "io"27 "reflect"28 "sort"29 "testing"3031 "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)3839type Msg struct {40 MsgMeta *module.MsgMetadata41 MailFrom string42 RcptTo []string43 Body []byte44 Header textproto.Header45}4647type Target struct {48 Messages []Msg49 DiscardMessages bool5051 StartErr error52 RcptErr map[string]error53 BodyErr error54 PartialBodyErr map[string]error55 AbortErr error56 CommitErr error5758 InstName string59}6061/*62module.Module is implemented with dummy functions for logging done by MsgPipeline code.63*/6465func (dt Target) Init(*config.Map) error {66 return nil67}6869func (dt Target) InstanceName() string {70 if dt.InstName != "" {71 return dt.InstName72 }73 return "test_instance"74}7576func (dt Target) Name() string {77 return "test_target"78}7980type testTargetDelivery struct {81 msg Msg82 tgt *Target83}8485type testTargetDeliveryPartial struct {86 testTargetDelivery87}8889func (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.StartErr97 }98 return &testTargetDelivery{99 tgt: dt,100 msg: Msg{MsgMeta: msgMeta, MailFrom: mailFrom},101 }, dt.StartErr102}103104func (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 err108 }109 }110111 dtd.msg.RcptTo = append(dtd.msg.RcptTo, to)112 return nil113}114115func (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 return121 }122123 dtd.msg.Header = header124125 body, err := buf.Open()126 if err != nil {127 for rcpt, err := range dtd.tgt.PartialBodyErr {128 c.SetStatus(rcpt, err)129 }130 return131 }132 defer body.Close()133134 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}141142func (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.BodyErr148 }149150 dtd.msg.Header = header151152 body, err := buf.Open()153 if err != nil {154 return err155 }156 defer body.Close()157158 if dtd.tgt.DiscardMessages {159 // Don't bother.160 _, err = io.Copy(io.Discard, body)161 return err162 }163164 dtd.msg.Body, err = io.ReadAll(body)165 return err166}167168func (dtd *testTargetDelivery) Abort(ctx context.Context) error {169 return dtd.tgt.AbortErr170}171172func (dtd *testTargetDelivery) Commit(ctx context.Context) error {173 if dtd.tgt.CommitErr != nil {174 return dtd.tgt.CommitErr175 }176 if dtd.tgt.DiscardMessages {177 return nil178 }179 dtd.tgt.Messages = append(dtd.tgt.Messages, dtd.msg)180 return nil181}182183func 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}189190func DoTestDeliveryMeta(t *testing.T, tgt module.DeliveryTarget, from string, to []string, msgMeta *module.MsgMetadata) string {191 t.Helper()192193 id, err := DoTestDeliveryErrMeta(t, tgt, from, to, msgMeta)194 if err != nil {195 t.Fatalf("Unexpected error: %v", err)196 }197 return id198}199200func DoTestDeliveryNonAtomic(t *testing.T, c module.StatusCollector, tgt module.DeliveryTarget, from string, to []string) string {201 t.Helper()202203 IDRaw := sha1.Sum([]byte(t.Name()))204 encodedID := hex.EncodeToString(IDRaw[:])205206 testCtx := context.Background()207208 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 encodedID220 }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 encodedID231 }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 }242243 return encodedID244}245246const DeliveryData = "A: 1\r\n" +247 "B: 2\r\n" +248 "\r\n" +249 "foobar\r\n"250251func DoTestDeliveryErr(t *testing.T, tgt module.DeliveryTarget, from string, to []string) (string, error) {252 return DoTestDeliveryErrMeta(t, tgt, from, to, &module.MsgMetadata{})253}254255func DoTestDeliveryErrMeta(t *testing.T, tgt module.DeliveryTarget, from string, to []string, msgMeta *module.MsgMetadata) (string, error) {256 t.Helper()257258 IDRaw := sha1.Sum([]byte(t.Name()))259 encodedID := hex.EncodeToString(IDRaw[:])260 testCtx := context.Background()261262 body := buffer.MemoryBuffer{Slice: []byte("foobar\r\n")}263 msgMeta.DontTraceSender = true264 msgMeta.ID = encodedID265 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, err270 }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, err280 }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, err293 }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, err298 }299300 return encodedID, err301}302303func CheckTestMessage(t *testing.T, tgt *Target, indx int, sender string, rcpt []string) {304 t.Helper()305306 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 return309 }310 msg := tgt.Messages[indx]311312 CheckMsg(t, &msg, sender, rcpt)313}314315func CheckMsg(t *testing.T, msg *Msg, sender string, rcpt []string) {316 t.Helper()317318 idRaw := sha1.Sum([]byte(t.Name()))319 encodedId := hex.EncodeToString(idRaw[:])320321 CheckMsgID(t, msg, sender, rcpt, encodedId)322}323324func CheckMsgID(t *testing.T, msg *Msg, sender string, rcpt []string, id string) string {325 t.Helper()326327 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 }333334 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 }342343 return msg.MsgMeta.ID344}