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 modconfig
 20
 21import (
 22	"errors"
 23	"fmt"
 24	"strconv"
 25	"strings"
 26
 27	"github.com/foxcpp/maddy/framework/config"
 28	"github.com/foxcpp/maddy/framework/exterrors"
 29	"github.com/foxcpp/maddy/framework/module"
 30)
 31
 32// FailAction specifies actions that messages pipeline should take based on the
 33// result of the check.
 34//
 35// Its check module responsibility to apply FailAction on the CheckResult it
 36// 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}, nil
 43//		}, 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 the
 48// 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 bool
 57	Reject     bool
 58
 59	ReasonOverride *exterrors.SMTPError
 60}
 61
 62func 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	}
 66
 67	val, err := ParseActionDirective(node.Args)
 68	if err != nil {
 69		return nil, config.NodeErr(node, "%v", err)
 70	}
 71	return val, nil
 72}
 73
 74func ParseActionDirective(args []string) (FailAction, error) {
 75	if len(args) == 0 {
 76		return FailAction{}, errors.New("expected at least 1 argument")
 77	}
 78
 79	res := FailAction{}
 80
 81	switch args[0] {
 82	case "reject", "quarantine":
 83		if len(args) > 1 {
 84			var err error
 85			res.ReasonOverride, err = ParseRejectDirective(args[1:])
 86			if err != nil {
 87				return FailAction{}, err
 88			}
 89		}
 90	case "ignore":
 91	default:
 92		return FailAction{}, errors.New("invalid action")
 93	}
 94
 95	res.Reject = args[0] == "reject"
 96	res.Quarantine = args[0] == "quarantine"
 97	return res, nil
 98}
 99
100// Apply merges the result of check execution with action configuration specified
101// in the check configuration.
102func (cfa FailAction) Apply(originalRes module.CheckResult) module.CheckResult {
103	if originalRes.Reason == nil {
104		return originalRes
105	}
106
107	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	}
116
117	originalRes.Quarantine = cfa.Quarantine || originalRes.Quarantine
118	originalRes.Reject = cfa.Reject || originalRes.Reject
119	return originalRes
120}
121
122func ParseRejectDirective(args []string) (*exterrors.SMTPError, error) {
123	code := 554
124	enchCode := exterrors.EnhancedCode{0, 7, 0}
125	msg := "Message rejected due to a local policy"
126	var err error
127	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		fallthrough
134	case 2:
135		enchCode, err = parseEnhancedCode(args[1])
136		if err != nil {
137			return nil, err
138		}
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		fallthrough
143	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 / 100
154		}
155	case 0:
156		// If no codes provided at all - use 5.7.0 and 554.
157		enchCode[0] = 5
158	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	}, nil
167}
168
169func 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	}
174
175	code := exterrors.EnhancedCode{}
176	for i, part := range parts {
177		num, err := strconv.Atoi(part)
178		if err != nil {
179			return code, err
180		}
181		code[i] = num
182	}
183	return code, nil
184}