maddy/internal/check/milter/milter.go
Martin Matous dbb424f164
fix(milter): remove erroneous path check
A remnant of d0e7df023c when milter.NewClient() didn't accept path.

Signed-off-by: Martin Matous <m@matous.dev>
2023-08-22 17:52:22 +02:00

446 lines
12 KiB
Go

/*
Maddy Mail Server - Composable all-in-one email server.
Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package milter
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"time"
"github.com/emersion/go-message/textproto"
"github.com/emersion/go-milter"
"github.com/foxcpp/maddy/framework/buffer"
"github.com/foxcpp/maddy/framework/config"
"github.com/foxcpp/maddy/framework/exterrors"
"github.com/foxcpp/maddy/framework/log"
"github.com/foxcpp/maddy/framework/module"
"github.com/foxcpp/maddy/internal/target"
)
const modName = "check.milter"
type Check struct {
cl *milter.Client
milterUrl string
failOpen bool
instName string
log log.Logger
}
func New(_, instName string, _, inlineArgs []string) (module.Module, error) {
c := &Check{
instName: instName,
log: log.Logger{Name: modName, Debug: log.DefaultLogger.Debug},
}
switch len(inlineArgs) {
case 1:
c.milterUrl = inlineArgs[0]
case 0:
default:
return nil, fmt.Errorf("%s: unexpected amount of arguments, want 1 or 0", modName)
}
return c, nil
}
func (c *Check) Name() string {
return modName
}
func (c *Check) InstanceName() string {
return c.instName
}
func (c *Check) Init(cfg *config.Map) error {
cfg.String("endpoint", false, false, c.milterUrl, &c.milterUrl)
cfg.Bool("fail_open", false, false, &c.failOpen)
if _, err := cfg.Process(); err != nil {
return err
}
if c.milterUrl == "" {
return fmt.Errorf("%s: milter endpoint is not set", modName)
}
endp, err := config.ParseEndpoint(c.milterUrl)
if err != nil {
return fmt.Errorf("%s: %v", modName, err)
}
switch endp.Scheme {
case "tcp", "unix":
default:
return fmt.Errorf("%s: scheme unsupported: %v", modName, endp.Scheme)
}
c.cl = milter.NewClientWithOptions(endp.Network(), endp.Address(), milter.ClientOptions{
Dialer: &net.Dialer{
Timeout: 10 * time.Second,
},
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
ActionMask: milter.OptAddHeader | milter.OptQuarantine,
ProtocolMask: 0,
})
return nil
}
type state struct {
c *Check
session *milter.ClientSession
msgMeta *module.MsgMetadata
skipChecks bool
log log.Logger
}
func (c *Check) CheckStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.CheckState, error) {
session, err := c.cl.Session()
if err != nil {
return nil, err
}
return &state{
c: c,
session: session,
msgMeta: msgMeta,
log: target.DeliveryLogger(c.log, msgMeta),
}, nil
}
func (s *state) handleAction(act *milter.Action) module.CheckResult {
switch act.Code {
case milter.ActAccept:
s.skipChecks = true
return module.CheckResult{}
case milter.ActContinue:
return module.CheckResult{}
case milter.ActReplyCode:
return module.CheckResult{
Reject: true,
Reason: &exterrors.SMTPError{
Code: act.SMTPCode,
EnhancedCode: exterrors.EnhancedCode{5, 7, 1},
Message: "Message rejected due to local policy",
Reason: "reply code action",
CheckName: "milter",
Misc: map[string]interface{}{
"milter": s.c.milterUrl,
},
},
}
case milter.ActDiscard:
s.log.Msg("silent discard is not supported, rejecting message")
fallthrough
case milter.ActTempFail:
return module.CheckResult{
Reject: true,
Reason: &exterrors.SMTPError{
Code: 450,
EnhancedCode: exterrors.EnhancedCode{4, 7, 1},
Message: "Message rejected due to local policy",
Reason: "reject action",
CheckName: "milter",
Misc: map[string]interface{}{
"milter": s.c.milterUrl,
},
},
}
case milter.ActReject:
return module.CheckResult{
Reject: true,
Reason: &exterrors.SMTPError{
Code: 550,
EnhancedCode: exterrors.EnhancedCode{5, 7, 1},
Message: "Message rejected due to local policy",
Reason: "reject action",
CheckName: "milter",
Misc: map[string]interface{}{
"milter": s.c.milterUrl,
},
},
}
default:
s.log.Msg("unknown action code ignored", "code", act.Code, "milter", s.c.milterUrl)
return module.CheckResult{}
}
}
// apply applies the modification actions returned by milter to the check results object.
func (s *state) apply(modifyActs []milter.ModifyAction, res module.CheckResult) module.CheckResult {
out := res
for _, act := range modifyActs {
switch act.Code {
case milter.ActAddRcpt, milter.ActDelRcpt:
s.log.Msg("envelope changes are not supported", "rcpt", act.Rcpt, "code", act.Code, "milter", s.c.milterUrl)
case milter.ActChangeFrom:
s.log.Msg("envelope changes are not supported", "from", act.From, "code", act.Code, "milter", s.c.milterUrl)
case milter.ActChangeHeader:
s.log.Msg("header field changes are not supported", "field", act.HeaderName, "milter", s.c.milterUrl)
case milter.ActInsertHeader:
if act.HeaderIndex != 1 {
s.log.Msg("header inserting not on top is not supported, prepending instead", "field", act.HeaderName, "milter", s.c.milterUrl)
}
fallthrough
case milter.ActAddHeader:
// Header field might be arbitarly folded by the caller and we want
// to preserve that exact format in case it is important (DKIM
// signature is added by milter).
field := make([]byte, 0, len(act.HeaderName)+2+len(act.HeaderValue)+2)
field = append(field, act.HeaderName...)
field = append(field, ':', ' ')
field = append(field, act.HeaderValue...)
field = append(field, '\r', '\n')
out.Header.AddRaw(field)
case milter.ActQuarantine:
out.Quarantine = true
out.Reason = exterrors.WithFields(errors.New("milter quarantine action"), map[string]interface{}{
"check": "milter",
"milter": s.c.milterUrl,
"reason": act.Reason,
})
}
}
return out
}
func (s *state) CheckConnection(ctx context.Context) module.CheckResult {
if s.msgMeta.Conn == nil {
// Submit some dummy values as the message is likely generated locally.
act, err := s.session.Conn("localhost", milter.FamilyInet, 25, "127.0.0.1")
if err != nil {
return s.ioError(err)
}
if act.Code != milter.ActContinue {
return s.handleAction(act)
}
act, err = s.session.Helo("localhost")
if err != nil {
return s.ioError(err)
}
return s.handleAction(act)
}
if !s.session.ProtocolOption(milter.OptNoConnect) {
if err := s.session.Macros(milter.CodeConn,
"daemon_name", "maddy",
"if_name", "unknown",
"if_addr", "0.0.0.0",
// TODO: $j
// TODO: $_
); err != nil {
return s.ioError(err)
}
var (
protoFamily milter.ProtoFamily
port uint16
addr string
)
switch rAddr := s.msgMeta.Conn.RemoteAddr.(type) {
case *net.TCPAddr:
port = uint16(rAddr.Port)
if v4 := rAddr.IP.To4(); v4 != nil {
// Make sure to not accidentally send IPv6-mapped IPv4 address.
protoFamily = milter.FamilyInet
addr = v4.String()
} else {
protoFamily = milter.FamilyInet6
addr = rAddr.IP.String()
}
case *net.UnixAddr:
protoFamily = milter.FamilyUnix
addr = rAddr.Name
default:
protoFamily = milter.FamilyUnknown
}
act, err := s.session.Conn(s.msgMeta.Conn.Hostname, protoFamily, port, addr)
if err != nil {
return s.ioError(err)
}
if act.Code != milter.ActContinue {
return s.handleAction(act)
}
}
if !s.session.ProtocolOption(milter.OptNoHelo) {
if s.msgMeta.Conn.TLS.HandshakeComplete {
fields := make([]string, 0, 4*2)
tlsState := s.msgMeta.Conn.TLS
switch tlsState.Version {
case tls.VersionTLS10:
fields = append(fields, "tls_version", "TLSv1")
case tls.VersionTLS11:
fields = append(fields, "tls_version", "TLSv1.1")
case tls.VersionTLS12:
fields = append(fields, "tls_version", "TLSv1.2")
case tls.VersionTLS13:
fields = append(fields, "tls_version", "TLSv1.3")
}
fields = append(fields, "cipher", tls.CipherSuiteName(tlsState.CipherSuite))
if len(tlsState.PeerCertificates) != 0 {
fields = append(fields, "cert_subject",
tlsState.PeerCertificates[len(tlsState.PeerCertificates)-1].Subject.String())
fields = append(fields, "cert_issuer",
tlsState.PeerCertificates[len(tlsState.PeerCertificates)-1].Issuer.String())
}
if err := s.session.Macros(milter.CodeHelo, fields...); err != nil {
return s.ioError(err)
}
}
act, err := s.session.Helo(s.msgMeta.Conn.Hostname)
if err != nil {
return s.ioError(err)
}
return s.handleAction(act)
}
return module.CheckResult{}
}
func (s *state) ioError(err error) module.CheckResult {
if s.c.failOpen {
s.skipChecks = true // silently permit processing to continue
s.c.log.Error("I/O error", err)
return module.CheckResult{}
}
return module.CheckResult{
Reject: true,
Reason: &exterrors.SMTPError{
Code: 451,
EnhancedCode: exterrors.EnhancedCode{4, 7, 1},
Message: "I/O error during policy check",
Err: err,
CheckName: "milter",
Misc: map[string]interface{}{
"milter": s.c.milterUrl,
},
},
}
}
func (s *state) CheckSender(ctx context.Context, mailFrom string) module.CheckResult {
if s.skipChecks || s.session.ProtocolOption(milter.OptNoMailFrom) {
return module.CheckResult{}
}
fields := make([]string, 0, 2)
fields = append(fields, "i", s.msgMeta.ID)
// TODO: fields = append(fields, "auth_type", s.msgMeta.???)
if s.msgMeta.Conn.AuthUser != "" {
fields = append(fields, "auth_authen", s.msgMeta.Conn.AuthUser)
}
if err := s.session.Macros(milter.CodeMail, fields...); err != nil {
return s.ioError(err)
}
esmtpArgs := make([]string, 0, 2)
if s.msgMeta.SMTPOpts.UTF8 {
esmtpArgs = append(esmtpArgs, "SMTPUTF8")
}
act, err := s.session.Mail(mailFrom, esmtpArgs)
if err != nil {
return s.ioError(err)
}
return s.handleAction(act)
}
func (s *state) CheckRcpt(ctx context.Context, rcptTo string) module.CheckResult {
if s.skipChecks {
return module.CheckResult{}
}
act, err := s.session.Rcpt(rcptTo, nil)
if err != nil {
return s.ioError(err)
}
return s.handleAction(act)
}
func (s *state) CheckBody(ctx context.Context, header textproto.Header, body buffer.Buffer) module.CheckResult {
if s.skipChecks {
return module.CheckResult{}
}
act, err := s.session.Header(header)
if err != nil {
return s.ioError(err)
}
if act.Code != milter.ActContinue {
return s.handleAction(act)
}
var modifyAct []milter.ModifyAction
if !s.session.ProtocolOption(milter.OptNoBody) {
// body.Open can be expensive for on-disk buffering.
r, err := body.Open()
if err != nil {
// Not ioError(err) because fail_open directive is applied only for external I/O.
return module.CheckResult{
Reject: true,
Reason: &exterrors.SMTPError{
Code: 451,
EnhancedCode: exterrors.EnhancedCode{4, 7, 1},
Message: "Internal error during policy check",
Err: err,
CheckName: "milter",
Misc: map[string]interface{}{
"milter": s.c.milterUrl,
},
},
}
}
modifyAct, act, err = s.session.BodyReadFrom(r)
if err != nil {
return s.ioError(err)
}
} else {
modifyAct, act, err = s.session.End()
if err != nil {
return s.ioError(err)
}
}
result := s.handleAction(act)
return s.apply(modifyAct, result)
}
func (s *state) Close() error {
return s.session.Close()
}
var (
_ module.Check = &Check{}
_ module.CheckState = &state{}
)
func init() {
module.Register(modName, New)
}