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 dns
 20
 21import (
 22	"strings"
 23
 24	"github.com/foxcpp/maddy/framework/address"
 25	modconfig "github.com/foxcpp/maddy/framework/config/module"
 26	"github.com/foxcpp/maddy/framework/dns"
 27	"github.com/foxcpp/maddy/framework/exterrors"
 28	"github.com/foxcpp/maddy/framework/module"
 29	"github.com/foxcpp/maddy/internal/check"
 30)
 31
 32func requireMatchingRDNS(ctx check.StatelessCheckContext) module.CheckResult {
 33	if ctx.MsgMeta.Conn == nil {
 34		ctx.Logger.Msg("locally-generated message, skipping")
 35		return module.CheckResult{}
 36	}
 37	if ctx.MsgMeta.Conn.RDNSName == nil {
 38		ctx.Logger.Msg("rDNS lookup is disabled, skipping")
 39		return module.CheckResult{}
 40	}
 41
 42	rdnsNameI, err := ctx.MsgMeta.Conn.RDNSName.Get()
 43	if err != nil {
 44		reason, misc := exterrors.UnwrapDNSErr(err)
 45		return module.CheckResult{
 46			Reason: &exterrors.SMTPError{
 47				Code:         exterrors.SMTPCode(err, 450, 550),
 48				EnhancedCode: exterrors.SMTPEnchCode(err, exterrors.EnhancedCode{0, 7, 25}),
 49				Message:      "DNS error during policy check",
 50				CheckName:    "require_matching_rdns",
 51				Err:          err,
 52				Reason:       reason,
 53				Misc:         misc,
 54			},
 55		}
 56	}
 57	if rdnsNameI == nil {
 58		return module.CheckResult{
 59			Reason: &exterrors.SMTPError{
 60				Code:         550,
 61				EnhancedCode: exterrors.EnhancedCode{5, 7, 25},
 62				Message:      "No PTR record found",
 63				CheckName:    "require_matching_rdns",
 64				Err:          err,
 65			},
 66		}
 67	}
 68	rdnsName := rdnsNameI.(string)
 69
 70	srcDomain := strings.TrimSuffix(ctx.MsgMeta.Conn.Hostname, ".")
 71	rdnsName = strings.TrimSuffix(rdnsName, ".")
 72
 73	if dns.Equal(rdnsName, srcDomain) {
 74		ctx.Logger.Debugf("PTR record %s matches source domain, OK", rdnsName)
 75		return module.CheckResult{}
 76	}
 77
 78	return module.CheckResult{
 79		Reason: &exterrors.SMTPError{
 80			Code:         550,
 81			EnhancedCode: exterrors.EnhancedCode{5, 7, 25},
 82			Message:      "rDNS name does not match source hostname",
 83			CheckName:    "require_matching_rdns",
 84		},
 85	}
 86}
 87
 88func requireMXRecord(ctx check.StatelessCheckContext, mailFrom string) module.CheckResult {
 89	if mailFrom == "" {
 90		// Permit null reverse-path for bounces.
 91		return module.CheckResult{}
 92	}
 93
 94	_, domain, err := address.Split(mailFrom)
 95	if err != nil {
 96		return module.CheckResult{
 97			Reason: &exterrors.SMTPError{
 98				Code:         501,
 99				EnhancedCode: exterrors.EnhancedCode{5, 1, 8},
100				Message:      "Malformed sender address",
101				CheckName:    "require_mx_record",
102			},
103		}
104	}
105	if domain == "" {
106		return module.CheckResult{
107			Reason: &exterrors.SMTPError{
108				Code:         501,
109				EnhancedCode: exterrors.EnhancedCode{5, 1, 8},
110				Message:      "No domain part in address",
111				CheckName:    "require_mx_record",
112			},
113		}
114	}
115
116	srcMx, err := ctx.Resolver.LookupMX(ctx, domain)
117	if err != nil {
118		reason, misc := exterrors.UnwrapDNSErr(err)
119		return module.CheckResult{
120			Reason: &exterrors.SMTPError{
121				Code:         exterrors.SMTPCode(err, 450, 550),
122				EnhancedCode: exterrors.SMTPEnchCode(err, exterrors.EnhancedCode{0, 7, 0}),
123				Message:      "DNS error during policy check",
124				CheckName:    "require_mx_record",
125				Err:          err,
126				Reason:       reason,
127				Misc:         misc,
128			},
129		}
130	}
131
132	if len(srcMx) == 0 {
133		return module.CheckResult{
134			Reason: &exterrors.SMTPError{
135				Code:         501,
136				EnhancedCode: exterrors.EnhancedCode{5, 7, 27},
137				Message:      "Domain in MAIL FROM does not have any MX records",
138				CheckName:    "require_mx_record",
139			},
140		}
141	}
142
143	for _, mx := range srcMx {
144		if mx.Host == "." {
145			return module.CheckResult{
146				Reason: &exterrors.SMTPError{
147					Code:         501,
148					EnhancedCode: exterrors.EnhancedCode{5, 7, 27},
149					Message:      "Domain in MAIL FROM has null MX record",
150					CheckName:    "require_mx_record",
151				},
152			}
153		}
154	}
155
156	return module.CheckResult{}
157}
158func init() {
159	check.RegisterStatelessCheck("require_matching_rdns", modconfig.FailAction{Quarantine: true},
160		requireMatchingRDNS, nil, nil, nil)
161	check.RegisterStatelessCheck("require_mx_record", modconfig.FailAction{Quarantine: true},
162		nil, requireMXRecord, nil, nil)
163}