mirror of
https://github.com/foxcpp/maddy.git
synced 2025-04-06 22:47:37 +03:00
A remnant of d0e7df023c
when milter.NewClient() didn't accept path.
Signed-off-by: Martin Matous <m@matous.dev>
446 lines
12 KiB
Go
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)
|
|
}
|