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 msgpipeline
 20
 21import (
 22	"reflect"
 23	"strings"
 24	"testing"
 25
 26	parser "github.com/foxcpp/maddy/framework/cfgparser"
 27	"github.com/foxcpp/maddy/framework/exterrors"
 28)
 29
 30func 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}
 38
 39func TestMsgPipelineCfg(t *testing.T) {
 40	cases := []struct {
 41		name  string
 42		str   string
 43		value msgpipelineCfg
 44		fail  bool
 45	}{
 46		{
 47			name: "basic",
 48			str: `
 49				source example.com {
 50					destination example.org {
 51						reject 410
 52				  	}
 53				  	default_destination {
 54						reject 420
 55				  	}
 56				}
 57				default_source {
 58				  	destination example.org {
 59						reject 430
 60				  	}
 61				  	default_destination {
 62						reject 440
 63				  	}
 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 410
 95				}
 96				default_source {
 97					reject 420
 98				}`,
 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 410
121				}
122				default_destination {
123					reject 420
124				}`,
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 410
144				}`,
145			fail: true,
146		},
147		{
148			name: "missing default destination handler",
149			str: `
150				destination example.org {
151					reject 410
152				}`,
153			fail: true,
154		},
155		{
156			name: "invalid domain",
157			str: `
158				destination .. {
159					reject 410
160				}
161				default_destination {
162					reject 500
163				}`,
164			fail: true,
165		},
166		{
167			name: "invalid address",
168			str: `
169				destination @example. {
170					reject 410
171				}
172				default_destination {
173					reject 500
174				}`,
175			fail: true,
176		},
177		{
178			name: "invalid address",
179			str: `
180				destination @example. {
181					reject 421
182				}
183				default_destination {
184					reject 500
185				}`,
186			fail: true,
187		},
188		{
189			name: "invalid reject code",
190			str: `
191				destination example.com {
192					reject 200
193				}
194				default_destination {
195					reject 500
196				}`,
197			fail: true,
198		},
199		{
200			name: "destination together with source",
201			str: `
202				destination example.com {
203					reject 410
204				}
205				source example.org {
206					reject 420
207				}
208				default_source {
209					reject 430
210				}`,
211			fail: true,
212		},
213		{
214			name: "empty destination rule",
215			str: `
216				destination {
217					reject 410
218				}
219				default_destination {
220					reject 420
221				}`,
222			fail: true,
223		},
224	}
225
226	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				return
239			}
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}
248
249func TestMsgPipelineCfg_SourceIn(t *testing.T) {
250	str := `
251		source_in dummy {
252			deliver_to dummy
253		}
254		default_source {
255			reject 500
256		}
257	`
258
259	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	}
264
265	if len(parsed.sourceIn) == 0 {
266		t.Fatalf("missing source_in dummy")
267	}
268}
269
270func TestMsgPipelineCfg_DestIn(t *testing.T) {
271	str := `
272		destination_in dummy {
273			deliver_to dummy
274		}
275		default_destination {
276			reject 500
277		}
278	`
279
280	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	}
285
286	if len(parsed.defaultSource.rcptIn) == 0 {
287		t.Fatalf("missing destination_in dummy")
288	}
289}
290
291func TestMsgPipelineCfg_GlobalChecks(t *testing.T) {
292	str := `
293		check {
294			test_check
295		}
296		default_destination {
297			reject 500
298		}
299	`
300
301	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	}
306
307	if len(parsed.globalChecks) == 0 {
308		t.Fatalf("missing test_check in globalChecks")
309	}
310}
311
312func TestMsgPipelineCfg_GlobalChecksMultiple(t *testing.T) {
313	str := `
314		check {
315			test_check
316		}
317		check {
318			test_check
319		}
320		default_destination {
321			reject 500
322		}
323	`
324
325	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	}
330
331	if len(parsed.globalChecks) != 2 {
332		t.Fatalf("wrong amount of test_check's in globalChecks: %d", len(parsed.globalChecks))
333	}
334}
335
336func TestMsgPipelineCfg_SourceChecks(t *testing.T) {
337	str := `
338		source example.org {
339			check {
340				test_check
341			}
342
343			reject 500
344		}
345		default_source {
346			reject 500
347		}
348	`
349
350	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	}
355
356	if len(parsed.perSource["example.org"].checks) == 0 {
357		t.Fatalf("missing test_check in source checks")
358	}
359}
360
361func TestMsgPipelineCfg_SourceChecks_Multiple(t *testing.T) {
362	str := `
363		source example.org {
364			check {
365				test_check
366			}
367			check {
368				test_check
369			}
370
371			reject 500
372		}
373		default_source {
374			reject 500
375		}
376	`
377
378	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	}
383
384	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}
388
389func TestMsgPipelineCfg_RcptChecks(t *testing.T) {
390	str := `
391		destination example.org {
392			check {
393				test_check
394			}
395
396			reject 500
397		}
398		default_destination {
399			reject 500
400		}
401	`
402
403	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	}
408
409	if len(parsed.defaultSource.perRcpt["example.org"].checks) == 0 {
410		t.Fatalf("missing test_check in rcpt checks")
411	}
412}
413
414func TestMsgPipelineCfg_RcptChecks_Multiple(t *testing.T) {
415	str := `
416		destination example.org {
417			check {
418				test_check
419			}
420			check {
421				test_check
422			}
423
424			reject 500
425		}
426		default_destination {
427			reject 500
428		}
429	`
430
431	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	}
436
437	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}