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	"fmt"
 23	"strconv"
 24	"strings"
 25
 26	"github.com/foxcpp/maddy/framework/address"
 27	"github.com/foxcpp/maddy/framework/config"
 28	modconfig "github.com/foxcpp/maddy/framework/config/module"
 29	"github.com/foxcpp/maddy/framework/dns"
 30	"github.com/foxcpp/maddy/framework/exterrors"
 31	"github.com/foxcpp/maddy/framework/module"
 32	"github.com/foxcpp/maddy/internal/modify"
 33)
 34
 35type sourceIn struct {
 36	t     module.Table
 37	block sourceBlock
 38}
 39
 40type msgpipelineCfg struct {
 41	globalChecks    []module.Check
 42	globalModifiers modify.Group
 43	sourceIn        []sourceIn
 44	perSource       map[string]sourceBlock
 45	defaultSource   sourceBlock
 46	doDMARC         bool
 47}
 48
 49func parseMsgPipelineRootCfg(globals map[string]interface{}, nodes []config.Node) (msgpipelineCfg, error) {
 50	cfg := msgpipelineCfg{
 51		perSource: map[string]sourceBlock{},
 52	}
 53	var defaultSrcRaw []config.Node
 54	var othersRaw []config.Node
 55	for _, node := range nodes {
 56		switch node.Name {
 57		case "check":
 58			globalChecks, err := parseChecksGroup(globals, node)
 59			if err != nil {
 60				return msgpipelineCfg{}, err
 61			}
 62
 63			cfg.globalChecks = append(cfg.globalChecks, globalChecks...)
 64		case "modify":
 65			globalModifiers, err := parseModifiersGroup(globals, node)
 66			if err != nil {
 67				return msgpipelineCfg{}, err
 68			}
 69
 70			cfg.globalModifiers.Modifiers = append(cfg.globalModifiers.Modifiers, globalModifiers.Modifiers...)
 71		case "source_in":
 72			var tbl module.Table
 73			if err := modconfig.ModuleFromNode("table", node.Args, config.Node{}, globals, &tbl); err != nil {
 74				return msgpipelineCfg{}, err
 75			}
 76			srcBlock, err := parseMsgPipelineSrcCfg(globals, node.Children)
 77			if err != nil {
 78				return msgpipelineCfg{}, err
 79			}
 80			cfg.sourceIn = append(cfg.sourceIn, sourceIn{
 81				t:     tbl,
 82				block: srcBlock,
 83			})
 84		case "source":
 85			srcBlock, err := parseMsgPipelineSrcCfg(globals, node.Children)
 86			if err != nil {
 87				return msgpipelineCfg{}, err
 88			}
 89
 90			if len(node.Args) == 0 {
 91				return msgpipelineCfg{}, config.NodeErr(node, "expected at least one source matching rule")
 92			}
 93
 94			for _, rule := range node.Args {
 95				if strings.Contains(rule, "@") {
 96					rule, err = address.ForLookup(rule)
 97				} else {
 98					rule, err = dns.ForLookup(rule)
 99				}
100				if err != nil {
101					return msgpipelineCfg{}, config.NodeErr(node, "invalid source match rule: %v: %v", rule, err)
102				}
103
104				if !validMatchRule(rule) {
105					return msgpipelineCfg{}, config.NodeErr(node, "invalid source routing rule: %v", rule)
106				}
107
108				if _, ok := cfg.perSource[rule]; ok {
109					continue
110				}
111
112				cfg.perSource[rule] = srcBlock
113			}
114		case "default_source":
115			if defaultSrcRaw != nil {
116				return msgpipelineCfg{}, config.NodeErr(node, "duplicate 'default_source' block")
117			}
118			defaultSrcRaw = node.Children
119		case "dmarc":
120			switch len(node.Args) {
121			case 1:
122				switch node.Args[0] {
123				case "yes":
124					cfg.doDMARC = true
125				case "no":
126				default:
127					return msgpipelineCfg{}, config.NodeErr(node, "invalid argument for dmarc")
128				}
129			case 0:
130				cfg.doDMARC = true
131			}
132		case "deliver_to", "reroute", "destination_in", "destination", "default_destination", "reject":
133			othersRaw = append(othersRaw, node)
134		default:
135			return msgpipelineCfg{}, config.NodeErr(node, "unknown pipeline directive: %s", node.Name)
136		}
137	}
138
139	if len(cfg.perSource) == 0 && len(defaultSrcRaw) == 0 {
140		if len(othersRaw) == 0 {
141			return msgpipelineCfg{}, fmt.Errorf("empty pipeline configuration, use 'reject' to reject messages")
142		}
143
144		var err error
145		cfg.defaultSource, err = parseMsgPipelineSrcCfg(globals, othersRaw)
146		return cfg, err
147	} else if len(othersRaw) != 0 {
148		return msgpipelineCfg{}, config.NodeErr(othersRaw[0], "can't put handling directives together with source rules, did you mean to put it into 'default_source' block or into all source blocks?")
149	}
150
151	if len(defaultSrcRaw) == 0 {
152		return msgpipelineCfg{}, config.NodeErr(nodes[0], "missing or empty default source block, use default_source { reject } to reject messages")
153	}
154
155	var err error
156	cfg.defaultSource, err = parseMsgPipelineSrcCfg(globals, defaultSrcRaw)
157	return cfg, err
158}
159
160func parseMsgPipelineSrcCfg(globals map[string]interface{}, nodes []config.Node) (sourceBlock, error) {
161	src := sourceBlock{
162		perRcpt: map[string]*rcptBlock{},
163	}
164	var defaultRcptRaw []config.Node
165	var othersRaw []config.Node
166	for _, node := range nodes {
167		switch node.Name {
168		case "check":
169			checks, err := parseChecksGroup(globals, node)
170			if err != nil {
171				return sourceBlock{}, err
172			}
173
174			src.checks = append(src.checks, checks...)
175		case "modify":
176			modifiers, err := parseModifiersGroup(globals, node)
177			if err != nil {
178				return sourceBlock{}, err
179			}
180
181			src.modifiers.Modifiers = append(src.modifiers.Modifiers, modifiers.Modifiers...)
182		case "destination_in":
183			var tbl module.Table
184			if err := modconfig.ModuleFromNode("table", node.Args, config.Node{}, globals, &tbl); err != nil {
185				return sourceBlock{}, err
186			}
187			rcptBlock, err := parseMsgPipelineRcptCfg(globals, node.Children)
188			if err != nil {
189				return sourceBlock{}, err
190			}
191			src.rcptIn = append(src.rcptIn, rcptIn{
192				t:     tbl,
193				block: rcptBlock,
194			})
195		case "destination":
196			rcptBlock, err := parseMsgPipelineRcptCfg(globals, node.Children)
197			if err != nil {
198				return sourceBlock{}, err
199			}
200
201			if len(node.Args) == 0 {
202				return sourceBlock{}, config.NodeErr(node, "expected at least one destination match rule")
203			}
204
205			for _, rule := range node.Args {
206				if strings.Contains(rule, "@") {
207					rule, err = address.ForLookup(rule)
208				} else {
209					rule, err = dns.ForLookup(rule)
210				}
211				if err != nil {
212					return sourceBlock{}, config.NodeErr(node, "invalid destination match rule: %v: %v", rule, err)
213				}
214
215				if !validMatchRule(rule) {
216					return sourceBlock{}, config.NodeErr(node, "invalid destination match rule: %v", rule)
217				}
218
219				if _, ok := src.perRcpt[rule]; ok {
220					continue
221				}
222
223				src.perRcpt[rule] = rcptBlock
224			}
225		case "default_destination":
226			if defaultRcptRaw != nil {
227				return sourceBlock{}, config.NodeErr(node, "duplicate 'default_destination' block")
228			}
229			defaultRcptRaw = node.Children
230		case "deliver_to", "reroute", "reject":
231			othersRaw = append(othersRaw, node)
232		default:
233			return sourceBlock{}, config.NodeErr(node, "unknown pipeline directive: %s", node.Name)
234		}
235	}
236
237	if len(src.perRcpt) == 0 && len(defaultRcptRaw) == 0 {
238		if len(othersRaw) == 0 {
239			return sourceBlock{}, fmt.Errorf("empty source block, use 'reject' to reject messages")
240		}
241
242		var err error
243		src.defaultRcpt, err = parseMsgPipelineRcptCfg(globals, othersRaw)
244		return src, err
245	} else if len(othersRaw) != 0 {
246		return sourceBlock{}, config.NodeErr(othersRaw[0], "can't put handling directives together with destination rules, did you mean to put it into 'default' block or into all recipient blocks?")
247	}
248
249	if len(defaultRcptRaw) == 0 {
250		return sourceBlock{}, config.NodeErr(nodes[0], "missing or empty default destination block, use default_destination { reject } to reject messages")
251	}
252
253	var err error
254	src.defaultRcpt, err = parseMsgPipelineRcptCfg(globals, defaultRcptRaw)
255	return src, err
256}
257
258func parseMsgPipelineRcptCfg(globals map[string]interface{}, nodes []config.Node) (*rcptBlock, error) {
259	rcpt := rcptBlock{}
260	for _, node := range nodes {
261		switch node.Name {
262		case "check":
263			checks, err := parseChecksGroup(globals, node)
264			if err != nil {
265				return nil, err
266			}
267
268			rcpt.checks = append(rcpt.checks, checks...)
269		case "modify":
270			modifiers, err := parseModifiersGroup(globals, node)
271			if err != nil {
272				return nil, err
273			}
274
275			rcpt.modifiers.Modifiers = append(rcpt.modifiers.Modifiers, modifiers.Modifiers...)
276		case "deliver_to":
277			if rcpt.rejectErr != nil {
278				return nil, config.NodeErr(node, "can't use 'reject' and 'deliver_to' together")
279			}
280
281			if len(node.Args) == 0 {
282				return nil, config.NodeErr(node, "required at least one argument")
283			}
284			mod, err := modconfig.DeliveryTarget(globals, node.Args, node)
285			if err != nil {
286				return nil, err
287			}
288
289			rcpt.targets = append(rcpt.targets, mod)
290		case "reroute":
291			if len(node.Children) == 0 {
292				return nil, config.NodeErr(node, "missing or empty reroute pipeline configuration")
293			}
294
295			pipeline, err := New(globals, node.Children)
296			if err != nil {
297				return nil, err
298			}
299
300			rcpt.targets = append(rcpt.targets, pipeline)
301		case "reject":
302			if len(rcpt.targets) != 0 {
303				return nil, config.NodeErr(node, "can't use 'reject' and 'deliver_to' together")
304			}
305
306			var err error
307			rcpt.rejectErr, err = parseRejectDirective(node)
308			if err != nil {
309				return nil, err
310			}
311		default:
312			return nil, config.NodeErr(node, "invalid directive")
313		}
314	}
315	return &rcpt, nil
316}
317
318func parseRejectDirective(node config.Node) (*exterrors.SMTPError, error) {
319	code := 554
320	enchCode := exterrors.EnhancedCode{5, 7, 0}
321	msg := "Message rejected due to a local policy"
322	var err error
323	switch len(node.Args) {
324	case 3:
325		msg = node.Args[2]
326		if msg == "" {
327			return nil, config.NodeErr(node, "message can't be empty")
328		}
329		fallthrough
330	case 2:
331		enchCode, err = parseEnhancedCode(node.Args[1])
332		if err != nil {
333			return nil, config.NodeErr(node, "%v", err)
334		}
335		if enchCode[0] != 4 && enchCode[0] != 5 {
336			return nil, config.NodeErr(node, "enhanced code should use either 4 or 5 as a first number")
337		}
338		fallthrough
339	case 1:
340		code, err = strconv.Atoi(node.Args[0])
341		if err != nil {
342			return nil, config.NodeErr(node, "invalid error code integer: %v", err)
343		}
344		if (code/100) != 4 && (code/100) != 5 {
345			return nil, config.NodeErr(node, "error code should start with either 4 or 5")
346		}
347	case 0:
348	default:
349		return nil, config.NodeErr(node, "invalid count of arguments")
350	}
351	return &exterrors.SMTPError{
352		Code:         code,
353		EnhancedCode: enchCode,
354		Message:      msg,
355		Reason:       "reject directive used",
356	}, nil
357}
358
359func parseEnhancedCode(s string) (exterrors.EnhancedCode, error) {
360	parts := strings.Split(s, ".")
361	if len(parts) != 3 {
362		return exterrors.EnhancedCode{}, fmt.Errorf("wrong amount of enhanced code parts")
363	}
364
365	code := exterrors.EnhancedCode{}
366	for i, part := range parts {
367		num, err := strconv.Atoi(part)
368		if err != nil {
369			return code, err
370		}
371		code[i] = num
372	}
373	return code, nil
374}
375
376func parseChecksGroup(globals map[string]interface{}, node config.Node) ([]module.Check, error) {
377	var cg *CheckGroup
378	err := modconfig.GroupFromNode("checks", node.Args, node, globals, &cg)
379	if err != nil {
380		return nil, err
381	}
382	return cg.L, nil
383}
384
385func parseModifiersGroup(globals map[string]interface{}, node config.Node) (modify.Group, error) {
386	// Module object is *modify.Group, not modify.Group.
387	var mg *modify.Group
388	err := modconfig.GroupFromNode("modifiers", node.Args, node, globals, &mg)
389	if err != nil {
390		return modify.Group{}, err
391	}
392	return *mg, nil
393}
394
395func validMatchRule(rule string) bool {
396	return address.ValidDomain(rule) || address.Valid(rule)
397}