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 "fmt"23 "strconv"24 "strings"2526 "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)3435type sourceIn struct {36 t module.Table37 block sourceBlock38}3940type msgpipelineCfg struct {41 globalChecks []module.Check42 globalModifiers modify.Group43 sourceIn []sourceIn44 perSource map[string]sourceBlock45 defaultSource sourceBlock46 doDMARC bool47}4849func parseMsgPipelineRootCfg(globals map[string]interface{}, nodes []config.Node) (msgpipelineCfg, error) {50 cfg := msgpipelineCfg{51 perSource: map[string]sourceBlock{},52 }53 var defaultSrcRaw []config.Node54 var othersRaw []config.Node55 for _, node := range nodes {56 switch node.Name {57 case "check":58 globalChecks, err := parseChecksGroup(globals, node)59 if err != nil {60 return msgpipelineCfg{}, err61 }6263 cfg.globalChecks = append(cfg.globalChecks, globalChecks...)64 case "modify":65 globalModifiers, err := parseModifiersGroup(globals, node)66 if err != nil {67 return msgpipelineCfg{}, err68 }6970 cfg.globalModifiers.Modifiers = append(cfg.globalModifiers.Modifiers, globalModifiers.Modifiers...)71 case "source_in":72 var tbl module.Table73 if err := modconfig.ModuleFromNode("table", node.Args, config.Node{}, globals, &tbl); err != nil {74 return msgpipelineCfg{}, err75 }76 srcBlock, err := parseMsgPipelineSrcCfg(globals, node.Children)77 if err != nil {78 return msgpipelineCfg{}, err79 }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{}, err88 }8990 if len(node.Args) == 0 {91 return msgpipelineCfg{}, config.NodeErr(node, "expected at least one source matching rule")92 }9394 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 }103104 if !validMatchRule(rule) {105 return msgpipelineCfg{}, config.NodeErr(node, "invalid source routing rule: %v", rule)106 }107108 if _, ok := cfg.perSource[rule]; ok {109 continue110 }111112 cfg.perSource[rule] = srcBlock113 }114 case "default_source":115 if defaultSrcRaw != nil {116 return msgpipelineCfg{}, config.NodeErr(node, "duplicate 'default_source' block")117 }118 defaultSrcRaw = node.Children119 case "dmarc":120 switch len(node.Args) {121 case 1:122 switch node.Args[0] {123 case "yes":124 cfg.doDMARC = true125 case "no":126 default:127 return msgpipelineCfg{}, config.NodeErr(node, "invalid argument for dmarc")128 }129 case 0:130 cfg.doDMARC = true131 }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 }138139 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 }143144 var err error145 cfg.defaultSource, err = parseMsgPipelineSrcCfg(globals, othersRaw)146 return cfg, err147 } 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 }150151 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 }154155 var err error156 cfg.defaultSource, err = parseMsgPipelineSrcCfg(globals, defaultSrcRaw)157 return cfg, err158}159160func parseMsgPipelineSrcCfg(globals map[string]interface{}, nodes []config.Node) (sourceBlock, error) {161 src := sourceBlock{162 perRcpt: map[string]*rcptBlock{},163 }164 var defaultRcptRaw []config.Node165 var othersRaw []config.Node166 for _, node := range nodes {167 switch node.Name {168 case "check":169 checks, err := parseChecksGroup(globals, node)170 if err != nil {171 return sourceBlock{}, err172 }173174 src.checks = append(src.checks, checks...)175 case "modify":176 modifiers, err := parseModifiersGroup(globals, node)177 if err != nil {178 return sourceBlock{}, err179 }180181 src.modifiers.Modifiers = append(src.modifiers.Modifiers, modifiers.Modifiers...)182 case "destination_in":183 var tbl module.Table184 if err := modconfig.ModuleFromNode("table", node.Args, config.Node{}, globals, &tbl); err != nil {185 return sourceBlock{}, err186 }187 rcptBlock, err := parseMsgPipelineRcptCfg(globals, node.Children)188 if err != nil {189 return sourceBlock{}, err190 }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{}, err199 }200201 if len(node.Args) == 0 {202 return sourceBlock{}, config.NodeErr(node, "expected at least one destination match rule")203 }204205 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 }214215 if !validMatchRule(rule) {216 return sourceBlock{}, config.NodeErr(node, "invalid destination match rule: %v", rule)217 }218219 if _, ok := src.perRcpt[rule]; ok {220 continue221 }222223 src.perRcpt[rule] = rcptBlock224 }225 case "default_destination":226 if defaultRcptRaw != nil {227 return sourceBlock{}, config.NodeErr(node, "duplicate 'default_destination' block")228 }229 defaultRcptRaw = node.Children230 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 }236237 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 }241242 var err error243 src.defaultRcpt, err = parseMsgPipelineRcptCfg(globals, othersRaw)244 return src, err245 } 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 }248249 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 }252253 var err error254 src.defaultRcpt, err = parseMsgPipelineRcptCfg(globals, defaultRcptRaw)255 return src, err256}257258func 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, err266 }267268 rcpt.checks = append(rcpt.checks, checks...)269 case "modify":270 modifiers, err := parseModifiersGroup(globals, node)271 if err != nil {272 return nil, err273 }274275 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 }280281 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, err287 }288289 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 }294295 pipeline, err := New(globals, node.Children)296 if err != nil {297 return nil, err298 }299300 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 }305306 var err error307 rcpt.rejectErr, err = parseRejectDirective(node)308 if err != nil {309 return nil, err310 }311 default:312 return nil, config.NodeErr(node, "invalid directive")313 }314 }315 return &rcpt, nil316}317318func parseRejectDirective(node config.Node) (*exterrors.SMTPError, error) {319 code := 554320 enchCode := exterrors.EnhancedCode{5, 7, 0}321 msg := "Message rejected due to a local policy"322 var err error323 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 fallthrough330 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 fallthrough339 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 }, nil357}358359func 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 }364365 code := exterrors.EnhancedCode{}366 for i, part := range parts {367 num, err := strconv.Atoi(part)368 if err != nil {369 return code, err370 }371 code[i] = num372 }373 return code, nil374}375376func parseChecksGroup(globals map[string]interface{}, node config.Node) ([]module.Check, error) {377 var cg *CheckGroup378 err := modconfig.GroupFromNode("checks", node.Args, node, globals, &cg)379 if err != nil {380 return nil, err381 }382 return cg.L, nil383}384385func parseModifiersGroup(globals map[string]interface{}, node config.Node) (modify.Group, error) {386 // Module object is *modify.Group, not modify.Group.387 var mg *modify.Group388 err := modconfig.GroupFromNode("modifiers", node.Args, node, globals, &mg)389 if err != nil {390 return modify.Group{}, err391 }392 return *mg, nil393}394395func validMatchRule(rule string) bool {396 return address.ValidDomain(rule) || address.Valid(rule)397}