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 msgpipeline2021import (22 "errors"23 "testing"2425 "github.com/emersion/go-message/textproto"26 "github.com/emersion/go-msgauth/authres"27 "github.com/foxcpp/maddy/framework/module"28 "github.com/foxcpp/maddy/internal/testutils"29)3031func TestMsgPipeline_Checks(t *testing.T) {32 target := testutils.Target{}33 check1, check2 := testutils.Check{}, testutils.Check{}34 d := MsgPipeline{35 msgpipelineCfg: msgpipelineCfg{36 globalChecks: []module.Check{&check1, &check2},37 perSource: map[string]sourceBlock{},38 defaultSource: sourceBlock{39 perRcpt: map[string]*rcptBlock{},40 defaultRcpt: &rcptBlock{41 targets: []module.DeliveryTarget{&target},42 },43 },44 },45 Log: testutils.Logger(t, "msgpipeline"),46 }4748 testutils.DoTestDelivery(t, &d, "whatever@whatever", []string{"whatever@whatever"})4950 if len(target.Messages) != 1 {51 t.Fatalf("wrong amount of messages received, want %d, got %d", 1, len(target.Messages))52 }53 if target.Messages[0].MsgMeta.Quarantine {54 t.Fatalf("message is quarantined when it shouldn't")55 }5657 if check1.UnclosedStates != 0 || check2.UnclosedStates != 0 {58 t.Fatalf("checks state objects leak or double-closed, alive counters: %v, %v", check1.UnclosedStates, check2.UnclosedStates)59 }60}6162func TestMsgPipeline_AuthResults(t *testing.T) {63 target := testutils.Target{}64 check1, check2 := testutils.Check{65 BodyRes: module.CheckResult{66 AuthResult: []authres.Result{67 &authres.SPFResult{68 Value: authres.ResultFail,69 From: "FROM",70 Helo: "HELO",71 },72 },73 },74 }, testutils.Check{75 BodyRes: module.CheckResult{76 AuthResult: []authres.Result{77 &authres.SPFResult{78 Value: authres.ResultFail,79 From: "FROM2",80 Helo: "HELO2",81 },82 },83 },84 }85 d := MsgPipeline{86 msgpipelineCfg: msgpipelineCfg{87 globalChecks: []module.Check{&check1, &check2},88 perSource: map[string]sourceBlock{},89 defaultSource: sourceBlock{90 perRcpt: map[string]*rcptBlock{},91 defaultRcpt: &rcptBlock{92 targets: []module.DeliveryTarget{&target},93 },94 },95 },96 Hostname: "TEST-HOST",97 Log: testutils.Logger(t, "msgpipeline"),98 }99100 testutils.DoTestDelivery(t, &d, "whatever@whatever", []string{"whatever@whatever"})101102 if len(target.Messages) != 1 {103 t.Fatalf("wrong amount of messages received, want %d, got %d", 1, len(target.Messages))104 }105106 authRes := target.Messages[0].Header.Get("Authentication-Results")107 id, parsed, err := authres.Parse(authRes)108 if err != nil {109 t.Fatalf("failed to parse results")110 }111 if id != "TEST-HOST" {112 t.Fatalf("wrong authres identifier")113 }114 if len(parsed) != 2 {115 t.Fatalf("wrong amount of parts, want %d, got %d", 2, len(parsed))116 }117118 var seen1, seen2 bool119 for _, parts := range parsed {120 spfPart, ok := parts.(*authres.SPFResult)121 if !ok {122 t.Fatalf("Not SPFResult")123 }124125 if spfPart.From == "FROM" {126 seen1 = true127 }128 if spfPart.From == "FROM2" {129 seen2 = true130 }131 }132133 if !seen1 {134 t.Fatalf("First authRes is missing")135 }136 if !seen2 {137 t.Fatalf("Second authRes is missing")138 }139140 if check1.UnclosedStates != 0 || check2.UnclosedStates != 0 {141 t.Fatalf("checks state objects leak or double-closed, alive counters: %v, %v", check1.UnclosedStates, check2.UnclosedStates)142 }143}144145func TestMsgPipeline_Headers(t *testing.T) {146 hdr1 := textproto.Header{}147 hdr1.Add("HDR1", "1")148 hdr2 := textproto.Header{}149 hdr2.Add("HDR2", "2")150151 target := testutils.Target{}152 check1, check2 := testutils.Check{153 BodyRes: module.CheckResult{154 Header: hdr1,155 },156 }, testutils.Check{157 BodyRes: module.CheckResult{158 Header: hdr2,159 },160 }161 d := MsgPipeline{162 msgpipelineCfg: msgpipelineCfg{163 globalChecks: []module.Check{&check1, &check2},164 perSource: map[string]sourceBlock{},165 defaultSource: sourceBlock{166 perRcpt: map[string]*rcptBlock{},167 defaultRcpt: &rcptBlock{168 targets: []module.DeliveryTarget{&target},169 },170 },171 },172 Hostname: "TEST-HOST",173 Log: testutils.Logger(t, "msgpipeline"),174 }175176 testutils.DoTestDelivery(t, &d, "whatever@whatever", []string{"whatever@whatever"})177178 if len(target.Messages) != 1 {179 t.Fatalf("wrong amount of messages received, want %d, got %d", 1, len(target.Messages))180 }181182 if target.Messages[0].Header.Get("HDR1") != "1" {183 t.Fatalf("wrong HDR1 value, want %s, got %s", "1", target.Messages[0].Header.Get("HDR1"))184 }185 if target.Messages[0].Header.Get("HDR2") != "2" {186 t.Fatalf("wrong HDR2 value, want %s, got %s", "1", target.Messages[0].Header.Get("HDR2"))187 }188189 if check1.UnclosedStates != 0 || check2.UnclosedStates != 0 {190 t.Fatalf("checks state objects leak or double-closed, alive counters: %v, %v", check1.UnclosedStates, check2.UnclosedStates)191 }192}193194func TestMsgPipeline_Globalcheck_Errors(t *testing.T) {195 target := testutils.Target{}196 check_ := testutils.Check{197 InitErr: errors.New("1"),198 ConnRes: module.CheckResult{Reject: true, Reason: errors.New("2")},199 SenderRes: module.CheckResult{Reject: true, Reason: errors.New("3")},200 RcptRes: module.CheckResult{Reject: true, Reason: errors.New("4")},201 BodyRes: module.CheckResult{Reject: true, Reason: errors.New("5")},202 }203 d := MsgPipeline{204 msgpipelineCfg: msgpipelineCfg{205 globalChecks: []module.Check{&check_},206 perSource: map[string]sourceBlock{},207 defaultSource: sourceBlock{208 perRcpt: map[string]*rcptBlock{},209 defaultRcpt: &rcptBlock{210 targets: []module.DeliveryTarget{&target},211 },212 },213 },214 Hostname: "TEST-HOST",215 Log: testutils.Logger(t, "msgpipeline"),216 }217218 t.Run("init err", func(t *testing.T) {219 _, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})220 if err == nil {221 t.Fatal("expected error")222 }223 })224225 check_.InitErr = nil226227 t.Run("conn err", func(t *testing.T) {228 _, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})229 if err == nil {230 t.Fatal("expected error")231 }232 })233234 check_.ConnRes.Reject = false235236 t.Run("mail from err", func(t *testing.T) {237 _, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})238 if err == nil {239 t.Fatal("expected error")240 }241 })242243 check_.SenderRes.Reject = false244245 t.Run("rcpt to err", func(t *testing.T) {246 _, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})247 if err == nil {248 t.Fatal("expected error")249 }250 })251252 check_.RcptRes.Reject = false253254 t.Run("body err", func(t *testing.T) {255 _, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})256 if err == nil {257 t.Fatal("expected error")258 }259 })260261 check_.BodyRes.Reject = false262263 t.Run("no err", func(t *testing.T) {264 testutils.DoTestDelivery(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})265 })266267 if check_.UnclosedStates != 0 {268 t.Fatalf("check state objects leak or double-closed, counters: %d", check_.UnclosedStates)269 }270}271272func TestMsgPipeline_SourceCheck_Errors(t *testing.T) {273 target := testutils.Target{}274 check_ := testutils.Check{275 InitErr: errors.New("1"),276 ConnRes: module.CheckResult{Reject: true, Reason: errors.New("2")},277 SenderRes: module.CheckResult{Reject: true, Reason: errors.New("3")},278 RcptRes: module.CheckResult{Reject: true, Reason: errors.New("4")},279 BodyRes: module.CheckResult{Reject: true, Reason: errors.New("5")},280 }281 globalCheck := testutils.Check{}282 d := MsgPipeline{283 msgpipelineCfg: msgpipelineCfg{284 globalChecks: []module.Check{&globalCheck},285 perSource: map[string]sourceBlock{},286 defaultSource: sourceBlock{287 perRcpt: map[string]*rcptBlock{},288 checks: []module.Check{&check_},289 defaultRcpt: &rcptBlock{290 targets: []module.DeliveryTarget{&target},291 },292 },293 },294 Hostname: "TEST-HOST",295 Log: testutils.Logger(t, "msgpipeline"),296 }297298 t.Run("init err", func(t *testing.T) {299 _, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})300 if err == nil {301 t.Fatal("expected error")302 }303 })304305 check_.InitErr = nil306307 t.Run("conn err", func(t *testing.T) {308 _, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})309 if err == nil {310 t.Fatal("expected error")311 }312 })313314 check_.ConnRes.Reject = false315316 t.Run("mail from err", func(t *testing.T) {317 _, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})318 if err == nil {319 t.Fatal("expected error")320 }321 })322323 check_.SenderRes.Reject = false324325 t.Run("rcpt to err", func(t *testing.T) {326 _, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})327 if err == nil {328 t.Fatal("expected error")329 }330 })331332 check_.RcptRes.Reject = false333334 t.Run("body err", func(t *testing.T) {335 _, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})336 if err == nil {337 t.Fatal("expected error")338 }339 })340341 check_.BodyRes.Reject = false342343 t.Run("no err", func(t *testing.T) {344 testutils.DoTestDelivery(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})345 })346347 if check_.UnclosedStates != 0 || globalCheck.UnclosedStates != 0 {348 t.Fatalf("check state objects leak or double-closed, counters: %d, %d",349 check_.UnclosedStates, globalCheck.UnclosedStates)350 }351}352353func TestMsgPipeline_RcptCheck_Errors(t *testing.T) {354 target := testutils.Target{}355 check_ := testutils.Check{356 InitErr: errors.New("1"),357 ConnRes: module.CheckResult{Reject: true, Reason: errors.New("2")},358 SenderRes: module.CheckResult{Reject: true, Reason: errors.New("3")},359 RcptRes: module.CheckResult{Reject: true, Reason: errors.New("4")},360 BodyRes: module.CheckResult{Reject: true, Reason: errors.New("5")},361362 InstName: "err_check",363 }364 // Added to check whether it leaks.365 globalCheck := testutils.Check{InstName: "global_check"}366 sourceCheck := testutils.Check{InstName: "source_check"}367 d := MsgPipeline{368 msgpipelineCfg: msgpipelineCfg{369 globalChecks: []module.Check{&globalCheck},370 perSource: map[string]sourceBlock{},371 defaultSource: sourceBlock{372 perRcpt: map[string]*rcptBlock{},373 checks: []module.Check{&check_},374 defaultRcpt: &rcptBlock{375 targets: []module.DeliveryTarget{&target},376 },377 },378 },379 Hostname: "TEST-HOST",380 Log: testutils.Logger(t, "msgpipeline"),381 }382383 t.Run("init err", func(t *testing.T) {384 d.Log = testutils.Logger(t, "msgpipeline")385 _, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})386 if err == nil {387 t.Fatal("expected error")388 }389390 t.Log("!!!", check_.UnclosedStates)391 })392393 check_.InitErr = nil394395 t.Run("conn err", func(t *testing.T) {396 d.Log = testutils.Logger(t, "msgpipeline")397 _, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})398 if err == nil {399 t.Fatal("expected error")400 }401402 t.Log("!!!", check_.UnclosedStates)403 })404405 check_.ConnRes.Reject = false406407 t.Run("mail from err", func(t *testing.T) {408 d.Log = testutils.Logger(t, "msgpipeline")409 _, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})410 if err == nil {411 t.Fatal("expected error")412 }413414 t.Log("!!!", check_.UnclosedStates)415 })416417 check_.SenderRes.Reject = false418419 t.Run("rcpt to err", func(t *testing.T) {420 d.Log = testutils.Logger(t, "msgpipeline")421 _, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})422 if err == nil {423 t.Fatal("expected error")424 }425 })426427 check_.RcptRes.Reject = false428429 t.Run("body err", func(t *testing.T) {430 d.Log = testutils.Logger(t, "msgpipeline")431 _, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})432 if err == nil {433 t.Fatal("expected error")434 }435 })436437 check_.BodyRes.Reject = false438439 t.Run("no err", func(t *testing.T) {440 d.Log = testutils.Logger(t, "msgpipeline")441 testutils.DoTestDelivery(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})442 })443444 if check_.UnclosedStates != 0 || sourceCheck.UnclosedStates != 0 || globalCheck.UnclosedStates != 0 {445 t.Fatalf("check state objects leak or double-closed, counters: %d, %d, %d",446 check_.UnclosedStates, sourceCheck.UnclosedStates, globalCheck.UnclosedStates)447 }448}