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 modconfig2021import (22 "errors"23 "fmt"24 "strconv"25 "strings"2627 "github.com/foxcpp/maddy/framework/config"28 "github.com/foxcpp/maddy/framework/exterrors"29 "github.com/foxcpp/maddy/framework/module"30)3132// FailAction specifies actions that messages pipeline should take based on the33// result of the check.34//35// Its check module responsibility to apply FailAction on the CheckResult it36// returns. It is intended to be used as follows:37//38// Add the configuration directive to allow user to specify the action:39//40// cfg.Custom("SOME_action", false, false,41// func() (interface{}, error) {42// return modconfig.FailAction{Quarantine: true}, nil43// }, modconfig.FailActionDirective, &yourModule.SOMEAction)44//45// return in func literal is the default value, you might want to adjust it.46//47// Call yourModule.SOMEAction.Apply on CheckResult containing only the48// Reason field:49//50// func (yourModule YourModule) CheckConnection() module.CheckResult {51// return yourModule.SOMEAction.Apply(module.CheckResult{52// Reason: ...,53// })54// }55type FailAction struct {56 Quarantine bool57 Reject bool5859 ReasonOverride *exterrors.SMTPError60}6162func FailActionDirective(_ *config.Map, node config.Node) (interface{}, error) {63 if len(node.Children) != 0 {64 return nil, config.NodeErr(node, "can't declare block here")65 }6667 val, err := ParseActionDirective(node.Args)68 if err != nil {69 return nil, config.NodeErr(node, "%v", err)70 }71 return val, nil72}7374func ParseActionDirective(args []string) (FailAction, error) {75 if len(args) == 0 {76 return FailAction{}, errors.New("expected at least 1 argument")77 }7879 res := FailAction{}8081 switch args[0] {82 case "reject", "quarantine":83 if len(args) > 1 {84 var err error85 res.ReasonOverride, err = ParseRejectDirective(args[1:])86 if err != nil {87 return FailAction{}, err88 }89 }90 case "ignore":91 default:92 return FailAction{}, errors.New("invalid action")93 }9495 res.Reject = args[0] == "reject"96 res.Quarantine = args[0] == "quarantine"97 return res, nil98}99100// Apply merges the result of check execution with action configuration specified101// in the check configuration.102func (cfa FailAction) Apply(originalRes module.CheckResult) module.CheckResult {103 if originalRes.Reason == nil {104 return originalRes105 }106107 if cfa.ReasonOverride != nil {108 // Wrap instead of replace to preserve other fields.109 originalRes.Reason = &exterrors.SMTPError{110 Code: cfa.ReasonOverride.Code,111 EnhancedCode: cfa.ReasonOverride.EnhancedCode,112 Message: cfa.ReasonOverride.Message,113 Err: originalRes.Reason,114 }115 }116117 originalRes.Quarantine = cfa.Quarantine || originalRes.Quarantine118 originalRes.Reject = cfa.Reject || originalRes.Reject119 return originalRes120}121122func ParseRejectDirective(args []string) (*exterrors.SMTPError, error) {123 code := 554124 enchCode := exterrors.EnhancedCode{0, 7, 0}125 msg := "Message rejected due to a local policy"126 var err error127 switch len(args) {128 case 3:129 msg = args[2]130 if msg == "" {131 return nil, fmt.Errorf("message can't be empty")132 }133 fallthrough134 case 2:135 enchCode, err = parseEnhancedCode(args[1])136 if err != nil {137 return nil, err138 }139 if enchCode[0] != 4 && enchCode[0] != 5 {140 return nil, fmt.Errorf("enhanced code should use either 4 or 5 as a first number")141 }142 fallthrough143 case 1:144 code, err = strconv.Atoi(args[0])145 if err != nil {146 return nil, fmt.Errorf("invalid error code integer: %v", err)147 }148 if (code/100) != 4 && (code/100) != 5 {149 return nil, fmt.Errorf("error code should start with either 4 or 5")150 }151 // If enchanced code is not set - set first digit based on provided "basic" code.152 if enchCode[0] == 0 {153 enchCode[0] = code / 100154 }155 case 0:156 // If no codes provided at all - use 5.7.0 and 554.157 enchCode[0] = 5158 default:159 return nil, fmt.Errorf("invalid count of arguments")160 }161 return &exterrors.SMTPError{162 Code: code,163 EnhancedCode: enchCode,164 Message: msg,165 Reason: "reject directive used",166 }, nil167}168169func parseEnhancedCode(s string) (exterrors.EnhancedCode, error) {170 parts := strings.Split(s, ".")171 if len(parts) != 3 {172 return exterrors.EnhancedCode{}, fmt.Errorf("wrong amount of enhanced code parts")173 }174175 code := exterrors.EnhancedCode{}176 for i, part := range parts {177 num, err := strconv.Atoi(part)178 if err != nil {179 return code, err180 }181 code[i] = num182 }183 return code, nil184}