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 "reflect"23 "strings"24 "testing"2526 parser "github.com/foxcpp/maddy/framework/cfgparser"27 "github.com/foxcpp/maddy/framework/exterrors"28)2930func policyError(code int) error {31 return &exterrors.SMTPError{32 Message: "Message rejected due to a local policy",33 Code: code,34 EnhancedCode: exterrors.EnhancedCode{5, 7, 0},35 Reason: "reject directive used",36 }37}3839func TestMsgPipelineCfg(t *testing.T) {40 cases := []struct {41 name string42 str string43 value msgpipelineCfg44 fail bool45 }{46 {47 name: "basic",48 str: `49 source example.com {50 destination example.org {51 reject 41052 }53 default_destination {54 reject 42055 }56 }57 default_source {58 destination example.org {59 reject 43060 }61 default_destination {62 reject 44063 }64 }`,65 value: msgpipelineCfg{66 perSource: map[string]sourceBlock{67 "example.com": {68 perRcpt: map[string]*rcptBlock{69 "example.org": {70 rejectErr: policyError(410),71 },72 },73 defaultRcpt: &rcptBlock{74 rejectErr: policyError(420),75 },76 },77 },78 defaultSource: sourceBlock{79 perRcpt: map[string]*rcptBlock{80 "example.org": {81 rejectErr: policyError(430),82 },83 },84 defaultRcpt: &rcptBlock{85 rejectErr: policyError(440),86 },87 },88 },89 },90 {91 name: "implied default destination",92 str: `93 source example.com {94 reject 41095 }96 default_source {97 reject 42098 }`,99 value: msgpipelineCfg{100 perSource: map[string]sourceBlock{101 "example.com": {102 perRcpt: map[string]*rcptBlock{},103 defaultRcpt: &rcptBlock{104 rejectErr: policyError(410),105 },106 },107 },108 defaultSource: sourceBlock{109 perRcpt: map[string]*rcptBlock{},110 defaultRcpt: &rcptBlock{111 rejectErr: policyError(420),112 },113 },114 },115 },116 {117 name: "implied default sender",118 str: `119 destination example.com {120 reject 410121 }122 default_destination {123 reject 420124 }`,125 value: msgpipelineCfg{126 perSource: map[string]sourceBlock{},127 defaultSource: sourceBlock{128 perRcpt: map[string]*rcptBlock{129 "example.com": {130 rejectErr: policyError(410),131 },132 },133 defaultRcpt: &rcptBlock{134 rejectErr: policyError(420),135 },136 },137 },138 },139 {140 name: "missing default source handler",141 str: `142 source example.org {143 reject 410144 }`,145 fail: true,146 },147 {148 name: "missing default destination handler",149 str: `150 destination example.org {151 reject 410152 }`,153 fail: true,154 },155 {156 name: "invalid domain",157 str: `158 destination .. {159 reject 410160 }161 default_destination {162 reject 500163 }`,164 fail: true,165 },166 {167 name: "invalid address",168 str: `169 destination @example. {170 reject 410171 }172 default_destination {173 reject 500174 }`,175 fail: true,176 },177 {178 name: "invalid address",179 str: `180 destination @example. {181 reject 421182 }183 default_destination {184 reject 500185 }`,186 fail: true,187 },188 {189 name: "invalid reject code",190 str: `191 destination example.com {192 reject 200193 }194 default_destination {195 reject 500196 }`,197 fail: true,198 },199 {200 name: "destination together with source",201 str: `202 destination example.com {203 reject 410204 }205 source example.org {206 reject 420207 }208 default_source {209 reject 430210 }`,211 fail: true,212 },213 {214 name: "empty destination rule",215 str: `216 destination {217 reject 410218 }219 default_destination {220 reject 420221 }`,222 fail: true,223 },224 }225226 for _, case_ := range cases {227 t.Run(case_.name, func(t *testing.T) {228 cfg, _ := parser.Read(strings.NewReader(case_.str), "literal")229 parsed, err := parseMsgPipelineRootCfg(nil, cfg)230 if err != nil && !case_.fail {231 t.Fatalf("unexpected parse error: %v", err)232 }233 if err == nil && case_.fail {234 t.Fatalf("unexpected parse success")235 }236 if case_.fail {237 t.Log(err)238 return239 }240 if !reflect.DeepEqual(parsed, case_.value) {241 t.Errorf("Wrong parsed configuration")242 t.Errorf("Wanted: %+v", case_.value)243 t.Errorf("Got: %+v", parsed)244 }245 })246 }247}248249func TestMsgPipelineCfg_SourceIn(t *testing.T) {250 str := `251 source_in dummy {252 deliver_to dummy253 }254 default_source {255 reject 500256 }257 `258259 cfg, _ := parser.Read(strings.NewReader(str), "literal")260 parsed, err := parseMsgPipelineRootCfg(nil, cfg)261 if err != nil {262 t.Fatalf("unexpected parse error: %v", err)263 }264265 if len(parsed.sourceIn) == 0 {266 t.Fatalf("missing source_in dummy")267 }268}269270func TestMsgPipelineCfg_DestIn(t *testing.T) {271 str := `272 destination_in dummy {273 deliver_to dummy274 }275 default_destination {276 reject 500277 }278 `279280 cfg, _ := parser.Read(strings.NewReader(str), "literal")281 parsed, err := parseMsgPipelineRootCfg(nil, cfg)282 if err != nil {283 t.Fatalf("unexpected parse error: %v", err)284 }285286 if len(parsed.defaultSource.rcptIn) == 0 {287 t.Fatalf("missing destination_in dummy")288 }289}290291func TestMsgPipelineCfg_GlobalChecks(t *testing.T) {292 str := `293 check {294 test_check295 }296 default_destination {297 reject 500298 }299 `300301 cfg, _ := parser.Read(strings.NewReader(str), "literal")302 parsed, err := parseMsgPipelineRootCfg(nil, cfg)303 if err != nil {304 t.Fatalf("unexpected parse error: %v", err)305 }306307 if len(parsed.globalChecks) == 0 {308 t.Fatalf("missing test_check in globalChecks")309 }310}311312func TestMsgPipelineCfg_GlobalChecksMultiple(t *testing.T) {313 str := `314 check {315 test_check316 }317 check {318 test_check319 }320 default_destination {321 reject 500322 }323 `324325 cfg, _ := parser.Read(strings.NewReader(str), "literal")326 parsed, err := parseMsgPipelineRootCfg(nil, cfg)327 if err != nil {328 t.Fatalf("unexpected parse error: %v", err)329 }330331 if len(parsed.globalChecks) != 2 {332 t.Fatalf("wrong amount of test_check's in globalChecks: %d", len(parsed.globalChecks))333 }334}335336func TestMsgPipelineCfg_SourceChecks(t *testing.T) {337 str := `338 source example.org {339 check {340 test_check341 }342343 reject 500344 }345 default_source {346 reject 500347 }348 `349350 cfg, _ := parser.Read(strings.NewReader(str), "literal")351 parsed, err := parseMsgPipelineRootCfg(nil, cfg)352 if err != nil {353 t.Fatalf("unexpected parse error: %v", err)354 }355356 if len(parsed.perSource["example.org"].checks) == 0 {357 t.Fatalf("missing test_check in source checks")358 }359}360361func TestMsgPipelineCfg_SourceChecks_Multiple(t *testing.T) {362 str := `363 source example.org {364 check {365 test_check366 }367 check {368 test_check369 }370371 reject 500372 }373 default_source {374 reject 500375 }376 `377378 cfg, _ := parser.Read(strings.NewReader(str), "literal")379 parsed, err := parseMsgPipelineRootCfg(nil, cfg)380 if err != nil {381 t.Fatalf("unexpected parse error: %v", err)382 }383384 if len(parsed.perSource["example.org"].checks) != 2 {385 t.Fatalf("wrong amount of test_check's in source checks: %d", len(parsed.perSource["example.org"].checks))386 }387}388389func TestMsgPipelineCfg_RcptChecks(t *testing.T) {390 str := `391 destination example.org {392 check {393 test_check394 }395396 reject 500397 }398 default_destination {399 reject 500400 }401 `402403 cfg, _ := parser.Read(strings.NewReader(str), "literal")404 parsed, err := parseMsgPipelineRootCfg(nil, cfg)405 if err != nil {406 t.Fatalf("unexpected parse error: %v", err)407 }408409 if len(parsed.defaultSource.perRcpt["example.org"].checks) == 0 {410 t.Fatalf("missing test_check in rcpt checks")411 }412}413414func TestMsgPipelineCfg_RcptChecks_Multiple(t *testing.T) {415 str := `416 destination example.org {417 check {418 test_check419 }420 check {421 test_check422 }423424 reject 500425 }426 default_destination {427 reject 500428 }429 `430431 cfg, _ := parser.Read(strings.NewReader(str), "literal")432 parsed, err := parseMsgPipelineRootCfg(nil, cfg)433 if err != nil {434 t.Fatalf("unexpected parse error: %v", err)435 }436437 if len(parsed.defaultSource.perRcpt["example.org"].checks) != 2 {438 t.Fatalf("wrong amount of test_check's in rcpt checks: %d", len(parsed.defaultSource.perRcpt["example.org"].checks))439 }440}