mirror of
https://github.com/foxcpp/maddy.git
synced 2025-04-04 13:37:41 +03:00
440 lines
8.6 KiB
Go
440 lines
8.6 KiB
Go
/*
|
|
Maddy Mail Server - Composable all-in-one email server.
|
|
Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package msgpipeline
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
parser "github.com/foxcpp/maddy/framework/cfgparser"
|
|
"github.com/foxcpp/maddy/framework/exterrors"
|
|
)
|
|
|
|
func policyError(code int) error {
|
|
return &exterrors.SMTPError{
|
|
Message: "Message rejected due to a local policy",
|
|
Code: code,
|
|
EnhancedCode: exterrors.EnhancedCode{5, 7, 0},
|
|
Reason: "reject directive used",
|
|
}
|
|
}
|
|
|
|
func TestMsgPipelineCfg(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
str string
|
|
value msgpipelineCfg
|
|
fail bool
|
|
}{
|
|
{
|
|
name: "basic",
|
|
str: `
|
|
source example.com {
|
|
destination example.org {
|
|
reject 410
|
|
}
|
|
default_destination {
|
|
reject 420
|
|
}
|
|
}
|
|
default_source {
|
|
destination example.org {
|
|
reject 430
|
|
}
|
|
default_destination {
|
|
reject 440
|
|
}
|
|
}`,
|
|
value: msgpipelineCfg{
|
|
perSource: map[string]sourceBlock{
|
|
"example.com": {
|
|
perRcpt: map[string]*rcptBlock{
|
|
"example.org": {
|
|
rejectErr: policyError(410),
|
|
},
|
|
},
|
|
defaultRcpt: &rcptBlock{
|
|
rejectErr: policyError(420),
|
|
},
|
|
},
|
|
},
|
|
defaultSource: sourceBlock{
|
|
perRcpt: map[string]*rcptBlock{
|
|
"example.org": {
|
|
rejectErr: policyError(430),
|
|
},
|
|
},
|
|
defaultRcpt: &rcptBlock{
|
|
rejectErr: policyError(440),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "implied default destination",
|
|
str: `
|
|
source example.com {
|
|
reject 410
|
|
}
|
|
default_source {
|
|
reject 420
|
|
}`,
|
|
value: msgpipelineCfg{
|
|
perSource: map[string]sourceBlock{
|
|
"example.com": {
|
|
perRcpt: map[string]*rcptBlock{},
|
|
defaultRcpt: &rcptBlock{
|
|
rejectErr: policyError(410),
|
|
},
|
|
},
|
|
},
|
|
defaultSource: sourceBlock{
|
|
perRcpt: map[string]*rcptBlock{},
|
|
defaultRcpt: &rcptBlock{
|
|
rejectErr: policyError(420),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "implied default sender",
|
|
str: `
|
|
destination example.com {
|
|
reject 410
|
|
}
|
|
default_destination {
|
|
reject 420
|
|
}`,
|
|
value: msgpipelineCfg{
|
|
perSource: map[string]sourceBlock{},
|
|
defaultSource: sourceBlock{
|
|
perRcpt: map[string]*rcptBlock{
|
|
"example.com": {
|
|
rejectErr: policyError(410),
|
|
},
|
|
},
|
|
defaultRcpt: &rcptBlock{
|
|
rejectErr: policyError(420),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "missing default source handler",
|
|
str: `
|
|
source example.org {
|
|
reject 410
|
|
}`,
|
|
fail: true,
|
|
},
|
|
{
|
|
name: "missing default destination handler",
|
|
str: `
|
|
destination example.org {
|
|
reject 410
|
|
}`,
|
|
fail: true,
|
|
},
|
|
{
|
|
name: "invalid domain",
|
|
str: `
|
|
destination .. {
|
|
reject 410
|
|
}
|
|
default_destination {
|
|
reject 500
|
|
}`,
|
|
fail: true,
|
|
},
|
|
{
|
|
name: "invalid address",
|
|
str: `
|
|
destination @example. {
|
|
reject 410
|
|
}
|
|
default_destination {
|
|
reject 500
|
|
}`,
|
|
fail: true,
|
|
},
|
|
{
|
|
name: "invalid address",
|
|
str: `
|
|
destination @example. {
|
|
reject 421
|
|
}
|
|
default_destination {
|
|
reject 500
|
|
}`,
|
|
fail: true,
|
|
},
|
|
{
|
|
name: "invalid reject code",
|
|
str: `
|
|
destination example.com {
|
|
reject 200
|
|
}
|
|
default_destination {
|
|
reject 500
|
|
}`,
|
|
fail: true,
|
|
},
|
|
{
|
|
name: "destination together with source",
|
|
str: `
|
|
destination example.com {
|
|
reject 410
|
|
}
|
|
source example.org {
|
|
reject 420
|
|
}
|
|
default_source {
|
|
reject 430
|
|
}`,
|
|
fail: true,
|
|
},
|
|
{
|
|
name: "empty destination rule",
|
|
str: `
|
|
destination {
|
|
reject 410
|
|
}
|
|
default_destination {
|
|
reject 420
|
|
}`,
|
|
fail: true,
|
|
},
|
|
}
|
|
|
|
for _, case_ := range cases {
|
|
t.Run(case_.name, func(t *testing.T) {
|
|
cfg, _ := parser.Read(strings.NewReader(case_.str), "literal")
|
|
parsed, err := parseMsgPipelineRootCfg(nil, cfg)
|
|
if err != nil && !case_.fail {
|
|
t.Fatalf("unexpected parse error: %v", err)
|
|
}
|
|
if err == nil && case_.fail {
|
|
t.Fatalf("unexpected parse success")
|
|
}
|
|
if case_.fail {
|
|
t.Log(err)
|
|
return
|
|
}
|
|
if !reflect.DeepEqual(parsed, case_.value) {
|
|
t.Errorf("Wrong parsed configuration")
|
|
t.Errorf("Wanted: %+v", case_.value)
|
|
t.Errorf("Got: %+v", parsed)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMsgPipelineCfg_SourceIn(t *testing.T) {
|
|
str := `
|
|
source_in dummy {
|
|
deliver_to dummy
|
|
}
|
|
default_source {
|
|
reject 500
|
|
}
|
|
`
|
|
|
|
cfg, _ := parser.Read(strings.NewReader(str), "literal")
|
|
parsed, err := parseMsgPipelineRootCfg(nil, cfg)
|
|
if err != nil {
|
|
t.Fatalf("unexpected parse error: %v", err)
|
|
}
|
|
|
|
if len(parsed.sourceIn) == 0 {
|
|
t.Fatalf("missing source_in dummy")
|
|
}
|
|
}
|
|
|
|
func TestMsgPipelineCfg_DestIn(t *testing.T) {
|
|
str := `
|
|
destination_in dummy {
|
|
deliver_to dummy
|
|
}
|
|
default_destination {
|
|
reject 500
|
|
}
|
|
`
|
|
|
|
cfg, _ := parser.Read(strings.NewReader(str), "literal")
|
|
parsed, err := parseMsgPipelineRootCfg(nil, cfg)
|
|
if err != nil {
|
|
t.Fatalf("unexpected parse error: %v", err)
|
|
}
|
|
|
|
if len(parsed.defaultSource.rcptIn) == 0 {
|
|
t.Fatalf("missing destination_in dummy")
|
|
}
|
|
}
|
|
|
|
func TestMsgPipelineCfg_GlobalChecks(t *testing.T) {
|
|
str := `
|
|
check {
|
|
test_check
|
|
}
|
|
default_destination {
|
|
reject 500
|
|
}
|
|
`
|
|
|
|
cfg, _ := parser.Read(strings.NewReader(str), "literal")
|
|
parsed, err := parseMsgPipelineRootCfg(nil, cfg)
|
|
if err != nil {
|
|
t.Fatalf("unexpected parse error: %v", err)
|
|
}
|
|
|
|
if len(parsed.globalChecks) == 0 {
|
|
t.Fatalf("missing test_check in globalChecks")
|
|
}
|
|
}
|
|
|
|
func TestMsgPipelineCfg_GlobalChecksMultiple(t *testing.T) {
|
|
str := `
|
|
check {
|
|
test_check
|
|
}
|
|
check {
|
|
test_check
|
|
}
|
|
default_destination {
|
|
reject 500
|
|
}
|
|
`
|
|
|
|
cfg, _ := parser.Read(strings.NewReader(str), "literal")
|
|
parsed, err := parseMsgPipelineRootCfg(nil, cfg)
|
|
if err != nil {
|
|
t.Fatalf("unexpected parse error: %v", err)
|
|
}
|
|
|
|
if len(parsed.globalChecks) != 2 {
|
|
t.Fatalf("wrong amount of test_check's in globalChecks: %d", len(parsed.globalChecks))
|
|
}
|
|
}
|
|
|
|
func TestMsgPipelineCfg_SourceChecks(t *testing.T) {
|
|
str := `
|
|
source example.org {
|
|
check {
|
|
test_check
|
|
}
|
|
|
|
reject 500
|
|
}
|
|
default_source {
|
|
reject 500
|
|
}
|
|
`
|
|
|
|
cfg, _ := parser.Read(strings.NewReader(str), "literal")
|
|
parsed, err := parseMsgPipelineRootCfg(nil, cfg)
|
|
if err != nil {
|
|
t.Fatalf("unexpected parse error: %v", err)
|
|
}
|
|
|
|
if len(parsed.perSource["example.org"].checks) == 0 {
|
|
t.Fatalf("missing test_check in source checks")
|
|
}
|
|
}
|
|
|
|
func TestMsgPipelineCfg_SourceChecks_Multiple(t *testing.T) {
|
|
str := `
|
|
source example.org {
|
|
check {
|
|
test_check
|
|
}
|
|
check {
|
|
test_check
|
|
}
|
|
|
|
reject 500
|
|
}
|
|
default_source {
|
|
reject 500
|
|
}
|
|
`
|
|
|
|
cfg, _ := parser.Read(strings.NewReader(str), "literal")
|
|
parsed, err := parseMsgPipelineRootCfg(nil, cfg)
|
|
if err != nil {
|
|
t.Fatalf("unexpected parse error: %v", err)
|
|
}
|
|
|
|
if len(parsed.perSource["example.org"].checks) != 2 {
|
|
t.Fatalf("wrong amount of test_check's in source checks: %d", len(parsed.perSource["example.org"].checks))
|
|
}
|
|
}
|
|
|
|
func TestMsgPipelineCfg_RcptChecks(t *testing.T) {
|
|
str := `
|
|
destination example.org {
|
|
check {
|
|
test_check
|
|
}
|
|
|
|
reject 500
|
|
}
|
|
default_destination {
|
|
reject 500
|
|
}
|
|
`
|
|
|
|
cfg, _ := parser.Read(strings.NewReader(str), "literal")
|
|
parsed, err := parseMsgPipelineRootCfg(nil, cfg)
|
|
if err != nil {
|
|
t.Fatalf("unexpected parse error: %v", err)
|
|
}
|
|
|
|
if len(parsed.defaultSource.perRcpt["example.org"].checks) == 0 {
|
|
t.Fatalf("missing test_check in rcpt checks")
|
|
}
|
|
}
|
|
|
|
func TestMsgPipelineCfg_RcptChecks_Multiple(t *testing.T) {
|
|
str := `
|
|
destination example.org {
|
|
check {
|
|
test_check
|
|
}
|
|
check {
|
|
test_check
|
|
}
|
|
|
|
reject 500
|
|
}
|
|
default_destination {
|
|
reject 500
|
|
}
|
|
`
|
|
|
|
cfg, _ := parser.Read(strings.NewReader(str), "literal")
|
|
parsed, err := parseMsgPipelineRootCfg(nil, cfg)
|
|
if err != nil {
|
|
t.Fatalf("unexpected parse error: %v", err)
|
|
}
|
|
|
|
if len(parsed.defaultSource.perRcpt["example.org"].checks) != 2 {
|
|
t.Fatalf("wrong amount of test_check's in rcpt checks: %d", len(parsed.defaultSource.perRcpt["example.org"].checks))
|
|
}
|
|
}
|