maddy/msgpipeline/config.go
fox.cpp af4f180503
msgpipeline: Allow to chain pipelines
This allows for some complex but useful configurations, such as making
decision on delivery target based on the result of per-destination
address rewriting. One example where that can be useful is aliasing
local address to a remote address in a way that can't make the server
an open relay.
2019-11-07 22:39:04 +03:00

349 lines
9.7 KiB
Go

package msgpipeline
import (
"fmt"
"strconv"
"strings"
"github.com/foxcpp/maddy/address"
"github.com/foxcpp/maddy/config"
modconfig "github.com/foxcpp/maddy/config/module"
"github.com/foxcpp/maddy/exterrors"
"github.com/foxcpp/maddy/modify"
"github.com/foxcpp/maddy/module"
)
type msgpipelineCfg struct {
globalChecks []module.Check
globalModifiers modify.Group
perSource map[string]sourceBlock
defaultSource sourceBlock
doDMARC bool
}
func parseMsgPipelineRootCfg(globals map[string]interface{}, nodes []config.Node) (msgpipelineCfg, error) {
cfg := msgpipelineCfg{
perSource: map[string]sourceBlock{},
}
var defaultSrcRaw []config.Node
var othersRaw []config.Node
for _, node := range nodes {
switch node.Name {
case "check":
if len(node.Children) == 0 {
return msgpipelineCfg{}, config.NodeErr(&node, "empty checks block")
}
globalChecks, err := parseChecksGroup(globals, node.Children)
if err != nil {
return msgpipelineCfg{}, err
}
cfg.globalChecks = append(cfg.globalChecks, globalChecks...)
case "modify":
if len(node.Children) == 0 {
return msgpipelineCfg{}, config.NodeErr(&node, "empty modifiers block")
}
globalModifiers, err := parseModifiersGroup(globals, node.Children)
if err != nil {
return msgpipelineCfg{}, err
}
cfg.globalModifiers.Modifiers = append(cfg.globalModifiers.Modifiers, globalModifiers.Modifiers...)
case "source":
srcBlock, err := parseMsgPipelineSrcCfg(globals, node.Children)
if err != nil {
return msgpipelineCfg{}, err
}
if len(node.Args) == 0 {
return msgpipelineCfg{}, config.NodeErr(&node, "expected at least one source matching rule")
}
for _, rule := range node.Args {
if !validMatchRule(rule) {
return msgpipelineCfg{}, config.NodeErr(&node, "invalid source routing rule: %v", rule)
}
cfg.perSource[rule] = srcBlock
}
case "default_source":
if defaultSrcRaw != nil {
return msgpipelineCfg{}, config.NodeErr(&node, "duplicate 'default_source' block")
}
defaultSrcRaw = node.Children
case "dmarc":
switch len(node.Args) {
case 1:
switch node.Args[0] {
case "yes":
cfg.doDMARC = true
case "no":
default:
return msgpipelineCfg{}, config.NodeErr(&node, "invalid argument for dmarc")
}
case 0:
cfg.doDMARC = true
}
default:
othersRaw = append(othersRaw, node)
}
}
if len(cfg.perSource) == 0 && len(defaultSrcRaw) == 0 {
if len(othersRaw) == 0 {
return msgpipelineCfg{}, fmt.Errorf("empty pipeline configuration, use 'reject' to reject messages")
}
var err error
cfg.defaultSource, err = parseMsgPipelineSrcCfg(globals, othersRaw)
return cfg, err
} else if len(othersRaw) != 0 {
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?")
}
if len(defaultSrcRaw) == 0 {
return msgpipelineCfg{}, config.NodeErr(&nodes[0], "missing or empty default source block, use default_source { reject } to reject messages")
}
var err error
cfg.defaultSource, err = parseMsgPipelineSrcCfg(globals, defaultSrcRaw)
return cfg, err
}
func parseMsgPipelineSrcCfg(globals map[string]interface{}, nodes []config.Node) (sourceBlock, error) {
src := sourceBlock{
perRcpt: map[string]*rcptBlock{},
}
var defaultRcptRaw []config.Node
var othersRaw []config.Node
for _, node := range nodes {
switch node.Name {
case "check":
if len(node.Children) == 0 {
return sourceBlock{}, config.NodeErr(&node, "empty checks block")
}
checks, err := parseChecksGroup(globals, node.Children)
if err != nil {
return sourceBlock{}, err
}
src.checks = append(src.checks, checks...)
case "modify":
if len(node.Children) == 0 {
return sourceBlock{}, config.NodeErr(&node, "empty modifiers block")
}
modifiers, err := parseModifiersGroup(globals, node.Children)
if err != nil {
return sourceBlock{}, err
}
src.modifiers.Modifiers = append(src.modifiers.Modifiers, modifiers.Modifiers...)
case "destination":
rcptBlock, err := parseMsgPipelineRcptCfg(globals, node.Children)
if err != nil {
return sourceBlock{}, err
}
if len(node.Args) == 0 {
return sourceBlock{}, config.NodeErr(&node, "expected at least one destination match rule")
}
for _, rule := range node.Args {
if !validMatchRule(rule) {
return sourceBlock{}, config.NodeErr(&node, "invalid destination match rule: %v", rule)
}
src.perRcpt[rule] = rcptBlock
}
case "default_destination":
if defaultRcptRaw != nil {
return sourceBlock{}, config.NodeErr(&node, "duplicate 'default_destination' block")
}
defaultRcptRaw = node.Children
default:
othersRaw = append(othersRaw, node)
}
}
if len(src.perRcpt) == 0 && len(defaultRcptRaw) == 0 {
if len(othersRaw) == 0 {
return sourceBlock{}, fmt.Errorf("empty source block, use 'reject' to reject messages")
}
var err error
src.defaultRcpt, err = parseMsgPipelineRcptCfg(globals, othersRaw)
return src, err
} else if len(othersRaw) != 0 {
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?")
}
if len(defaultRcptRaw) == 0 {
return sourceBlock{}, config.NodeErr(&nodes[0], "missing or empty default destination block, use default_destination { reject } to reject messages")
}
var err error
src.defaultRcpt, err = parseMsgPipelineRcptCfg(globals, defaultRcptRaw)
return src, err
}
func parseMsgPipelineRcptCfg(globals map[string]interface{}, nodes []config.Node) (*rcptBlock, error) {
rcpt := rcptBlock{}
for _, node := range nodes {
switch node.Name {
case "check":
if len(node.Children) == 0 {
return nil, config.NodeErr(&node, "empty checks block")
}
checks, err := parseChecksGroup(globals, node.Children)
if err != nil {
return nil, err
}
rcpt.checks = append(rcpt.checks, checks...)
case "modify":
if len(node.Children) == 0 {
return nil, config.NodeErr(&node, "empty modifiers block")
}
modifiers, err := parseModifiersGroup(globals, node.Children)
if err != nil {
return nil, err
}
rcpt.modifiers.Modifiers = append(rcpt.modifiers.Modifiers, modifiers.Modifiers...)
case "deliver_to":
if rcpt.rejectErr != nil {
return nil, config.NodeErr(&node, "can't use 'reject' and 'deliver_to' together")
}
if len(node.Args) == 0 {
return nil, config.NodeErr(&node, "required at least one argument")
}
mod, err := modconfig.DeliveryTarget(globals, node.Args, &node)
if err != nil {
return nil, err
}
rcpt.targets = append(rcpt.targets, mod)
case "reroute":
if len(node.Children) == 0 {
return nil, config.NodeErr(&node, "missing or empty reroute pipeline configuration")
}
pipeline, err := New(globals, node.Children)
if err != nil {
return nil, err
}
rcpt.targets = append(rcpt.targets, pipeline)
case "reject":
if len(rcpt.targets) != 0 {
return nil, config.NodeErr(&node, "can't use 'reject' and 'deliver_to' together")
}
var err error
rcpt.rejectErr, err = parseRejectDirective(node)
if err != nil {
return nil, err
}
default:
return nil, config.NodeErr(&node, "invalid directive")
}
}
return &rcpt, nil
}
func parseRejectDirective(node config.Node) (*exterrors.SMTPError, error) {
code := 554
enchCode := exterrors.EnhancedCode{5, 7, 0}
msg := "Message rejected due to a local policy"
var err error
switch len(node.Args) {
case 3:
msg = node.Args[2]
if msg == "" {
return nil, config.NodeErr(&node, "message can't be empty")
}
fallthrough
case 2:
enchCode, err = parseEnhancedCode(node.Args[1])
if err != nil {
return nil, config.NodeErr(&node, "%v", err)
}
if enchCode[0] != 4 && enchCode[0] != 5 {
return nil, config.NodeErr(&node, "enhanced code should use either 4 or 5 as a first number")
}
fallthrough
case 1:
code, err = strconv.Atoi(node.Args[0])
if err != nil {
return nil, config.NodeErr(&node, "invalid error code integer: %v", err)
}
if (code/100) != 4 && (code/100) != 5 {
return nil, config.NodeErr(&node, "error code should start with either 4 or 5")
}
case 0:
default:
return nil, config.NodeErr(&node, "invalid count of arguments")
}
return &exterrors.SMTPError{
Code: code,
EnhancedCode: enchCode,
Message: msg,
Misc: map[string]interface{}{
"reason": "reject directive used",
},
}, nil
}
func parseEnhancedCode(s string) (exterrors.EnhancedCode, error) {
parts := strings.Split(s, ".")
if len(parts) != 3 {
return exterrors.EnhancedCode{}, fmt.Errorf("wrong amount of enhanced code parts")
}
code := exterrors.EnhancedCode{}
for i, part := range parts {
num, err := strconv.Atoi(part)
if err != nil {
return code, err
}
code[i] = num
}
return code, nil
}
func parseChecksGroup(globals map[string]interface{}, nodes []config.Node) ([]module.Check, error) {
checks := make([]module.Check, 0, len(nodes))
for _, child := range nodes {
msgCheck, err := modconfig.MessageCheck(globals, append([]string{child.Name}, child.Args...), &child)
if err != nil {
return nil, err
}
checks = append(checks, msgCheck)
}
return checks, nil
}
func parseModifiersGroup(globals map[string]interface{}, nodes []config.Node) (modify.Group, error) {
modifiers := modify.Group{}
for _, child := range nodes {
modifier, err := modconfig.MsgModifier(globals, append([]string{child.Name}, child.Args...), &child)
if err != nil {
return modify.Group{}, err
}
modifiers.Modifiers = append(modifiers.Modifiers, modifier)
}
return modifiers, nil
}
func validMatchRule(rule string) bool {
return address.ValidDomain(rule) || address.Valid(rule)
}