maddy/internal/check/milter/milter.go
fox.cpp 08f98f15d9
check/milter: Linter warnings
Unused variable and missing error check.
2020-05-04 15:59:13 +03:00

395 lines
10 KiB
Go

package milter
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"github.com/emersion/go-message/textproto"
"github.com/emersion/go-milter"
"github.com/foxcpp/maddy/internal/buffer"
"github.com/foxcpp/maddy/internal/config"
"github.com/foxcpp/maddy/internal/exterrors"
"github.com/foxcpp/maddy/internal/log"
"github.com/foxcpp/maddy/internal/module"
"github.com/foxcpp/maddy/internal/target"
)
const modName = "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)
}
if endp.Path != "" {
return fmt.Errorf("%s: stray path in endpoint: %v", modName, endp)
}
c.cl = milter.NewClient(endp.Scheme, endp.Host)
return nil
}
type state struct {
c *Check
session *milter.ClientSession
msgMeta *module.MsgMetadata
skipChecks bool
log log.Logger
}
func (c *Check) CheckStateForMsg(msgMeta *module.MsgMetadata) (module.CheckState, error) {
const supportedActions = milter.OptAddHeader | milter.OptQuarantine
protocolOpts := milter.OptProtocol(0)
if msgMeta.Conn == nil {
protocolOpts = milter.OptNoConnect | milter.OptNoHelo
}
session, err := c.cl.Session(supportedActions, protocolOpts)
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.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.HdrName, "milter", s.c.milterUrl)
case milter.ActInsertHeader:
if act.HdrIndex != 1 {
s.log.Msg("header inserting not on top is not supported, prepending instead", "field", act.HdrName, "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.HdrName)+2+len(act.HdrValue)+2)
field = append(field, act.HdrName...)
field = append(field, ':', ' ')
field = append(field, act.HdrValue...)
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 {
return module.CheckResult{}
}
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.Body(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()
}
func init() {
module.Register(modName, New)
}