1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819package milter2021import (22 "context"23 "crypto/tls"24 "errors"25 "fmt"26 "net"27 "time"2829 "github.com/emersion/go-message/textproto"30 "github.com/emersion/go-milter"31 "github.com/foxcpp/maddy/framework/buffer"32 "github.com/foxcpp/maddy/framework/config"33 "github.com/foxcpp/maddy/framework/exterrors"34 "github.com/foxcpp/maddy/framework/log"35 "github.com/foxcpp/maddy/framework/module"36 "github.com/foxcpp/maddy/internal/target"37)3839const modName = "check.milter"4041type Check struct {42 cl *milter.Client43 milterUrl string44 failOpen bool45 instName string46 log log.Logger47}4849func New(_, instName string, _, inlineArgs []string) (module.Module, error) {50 c := &Check{51 instName: instName,52 log: log.Logger{Name: modName, Debug: log.DefaultLogger.Debug},53 }54 switch len(inlineArgs) {55 case 1:56 c.milterUrl = inlineArgs[0]57 case 0:58 default:59 return nil, fmt.Errorf("%s: unexpected amount of arguments, want 1 or 0", modName)60 }61 return c, nil62}6364func (c *Check) Name() string {65 return modName66}6768func (c *Check) InstanceName() string {69 return c.instName70}7172func (c *Check) Init(cfg *config.Map) error {73 cfg.String("endpoint", false, false, c.milterUrl, &c.milterUrl)74 cfg.Bool("fail_open", false, false, &c.failOpen)75 if _, err := cfg.Process(); err != nil {76 return err77 }7879 if c.milterUrl == "" {80 return fmt.Errorf("%s: milter endpoint is not set", modName)81 }8283 endp, err := config.ParseEndpoint(c.milterUrl)84 if err != nil {85 return fmt.Errorf("%s: %v", modName, err)86 }8788 switch endp.Scheme {89 case "tcp", "unix":90 default:91 return fmt.Errorf("%s: scheme unsupported: %v", modName, endp.Scheme)92 }9394 c.cl = milter.NewClientWithOptions(endp.Network(), endp.Address(), milter.ClientOptions{95 Dialer: &net.Dialer{96 Timeout: 10 * time.Second,97 },98 ReadTimeout: 10 * time.Second,99 WriteTimeout: 10 * time.Second,100 ActionMask: milter.OptAddHeader | milter.OptQuarantine,101 ProtocolMask: 0,102 })103104 return nil105}106107type state struct {108 c *Check109 session *milter.ClientSession110 msgMeta *module.MsgMetadata111 skipChecks bool112 log log.Logger113}114115func (c *Check) CheckStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.CheckState, error) {116 session, err := c.cl.Session()117 if err != nil {118 return nil, err119 }120 return &state{121 c: c,122 session: session,123 msgMeta: msgMeta,124 log: target.DeliveryLogger(c.log, msgMeta),125 }, nil126}127128func (s *state) handleAction(act *milter.Action) module.CheckResult {129 switch act.Code {130 case milter.ActAccept:131 s.skipChecks = true132 return module.CheckResult{}133 case milter.ActContinue:134 return module.CheckResult{}135 case milter.ActReplyCode:136 return module.CheckResult{137 Reject: true,138 Reason: &exterrors.SMTPError{139 Code: act.SMTPCode,140 EnhancedCode: exterrors.EnhancedCode{5, 7, 1},141 Message: "Message rejected due to local policy",142 Reason: "reply code action",143 CheckName: "milter",144 Misc: map[string]interface{}{145 "milter": s.c.milterUrl,146 },147 },148 }149 case milter.ActDiscard:150 s.log.Msg("silent discard is not supported, rejecting message")151 fallthrough152 case milter.ActTempFail:153 return module.CheckResult{154 Reject: true,155 Reason: &exterrors.SMTPError{156 Code: 450,157 EnhancedCode: exterrors.EnhancedCode{4, 7, 1},158 Message: "Message rejected due to local policy",159 Reason: "reject action",160 CheckName: "milter",161 Misc: map[string]interface{}{162 "milter": s.c.milterUrl,163 },164 },165 }166 case milter.ActReject:167 return module.CheckResult{168 Reject: true,169 Reason: &exterrors.SMTPError{170 Code: 550,171 EnhancedCode: exterrors.EnhancedCode{5, 7, 1},172 Message: "Message rejected due to local policy",173 Reason: "reject action",174 CheckName: "milter",175 Misc: map[string]interface{}{176 "milter": s.c.milterUrl,177 },178 },179 }180 default:181 s.log.Msg("unknown action code ignored", "code", act.Code, "milter", s.c.milterUrl)182 return module.CheckResult{}183 }184}185186// apply applies the modification actions returned by milter to the check results object.187func (s *state) apply(modifyActs []milter.ModifyAction, res module.CheckResult) module.CheckResult {188 out := res189 for _, act := range modifyActs {190 switch act.Code {191 case milter.ActAddRcpt, milter.ActDelRcpt:192 s.log.Msg("envelope changes are not supported", "rcpt", act.Rcpt, "code", act.Code, "milter", s.c.milterUrl)193 case milter.ActChangeFrom:194 s.log.Msg("envelope changes are not supported", "from", act.From, "code", act.Code, "milter", s.c.milterUrl)195 case milter.ActChangeHeader:196 s.log.Msg("header field changes are not supported", "field", act.HeaderName, "milter", s.c.milterUrl)197 case milter.ActInsertHeader:198 if act.HeaderIndex != 1 {199 s.log.Msg("header inserting not on top is not supported, prepending instead", "field", act.HeaderName, "milter", s.c.milterUrl)200 }201 fallthrough202 case milter.ActAddHeader:203 // Header field might be arbitarly folded by the caller and we want204 // to preserve that exact format in case it is important (DKIM205 // signature is added by milter).206 field := make([]byte, 0, len(act.HeaderName)+2+len(act.HeaderValue)+2)207 field = append(field, act.HeaderName...)208 field = append(field, ':', ' ')209 field = append(field, act.HeaderValue...)210 field = append(field, '\r', '\n')211 out.Header.AddRaw(field)212 case milter.ActQuarantine:213 out.Quarantine = true214 out.Reason = exterrors.WithFields(errors.New("milter quarantine action"), map[string]interface{}{215 "check": "milter",216 "milter": s.c.milterUrl,217 "reason": act.Reason,218 })219 }220 }221 return out222}223224func (s *state) CheckConnection(ctx context.Context) module.CheckResult {225 if s.msgMeta.Conn == nil {226 // Submit some dummy values as the message is likely generated locally.227228 act, err := s.session.Conn("localhost", milter.FamilyInet, 25, "127.0.0.1")229 if err != nil {230 return s.ioError(err)231 }232 if act.Code != milter.ActContinue {233 return s.handleAction(act)234 }235236 act, err = s.session.Helo("localhost")237 if err != nil {238 return s.ioError(err)239 }240 return s.handleAction(act)241 }242243 if !s.session.ProtocolOption(milter.OptNoConnect) {244 if err := s.session.Macros(milter.CodeConn,245 "daemon_name", "maddy",246 "if_name", "unknown",247 "if_addr", "0.0.0.0",248 // TODO: $j249 // TODO: $_250 ); err != nil {251 return s.ioError(err)252 }253254 var (255 protoFamily milter.ProtoFamily256 port uint16257 addr string258 )259 switch rAddr := s.msgMeta.Conn.RemoteAddr.(type) {260 case *net.TCPAddr:261 port = uint16(rAddr.Port)262 if v4 := rAddr.IP.To4(); v4 != nil {263 // Make sure to not accidentally send IPv6-mapped IPv4 address.264 protoFamily = milter.FamilyInet265 addr = v4.String()266 } else {267 protoFamily = milter.FamilyInet6268 addr = rAddr.IP.String()269 }270 case *net.UnixAddr:271 protoFamily = milter.FamilyUnix272 addr = rAddr.Name273 default:274 protoFamily = milter.FamilyUnknown275 }276277 act, err := s.session.Conn(s.msgMeta.Conn.Hostname, protoFamily, port, addr)278 if err != nil {279 return s.ioError(err)280 }281 if act.Code != milter.ActContinue {282 return s.handleAction(act)283 }284 }285286 if !s.session.ProtocolOption(milter.OptNoHelo) {287 if s.msgMeta.Conn.TLS.HandshakeComplete {288 fields := make([]string, 0, 4*2)289 tlsState := s.msgMeta.Conn.TLS290291 switch tlsState.Version {292 case tls.VersionTLS10:293 fields = append(fields, "tls_version", "TLSv1")294 case tls.VersionTLS11:295 fields = append(fields, "tls_version", "TLSv1.1")296 case tls.VersionTLS12:297 fields = append(fields, "tls_version", "TLSv1.2")298 case tls.VersionTLS13:299 fields = append(fields, "tls_version", "TLSv1.3")300 }301 fields = append(fields, "cipher", tls.CipherSuiteName(tlsState.CipherSuite))302303 if len(tlsState.PeerCertificates) != 0 {304 fields = append(fields, "cert_subject",305 tlsState.PeerCertificates[len(tlsState.PeerCertificates)-1].Subject.String())306 fields = append(fields, "cert_issuer",307 tlsState.PeerCertificates[len(tlsState.PeerCertificates)-1].Issuer.String())308 }309310 if err := s.session.Macros(milter.CodeHelo, fields...); err != nil {311 return s.ioError(err)312 }313 }314 act, err := s.session.Helo(s.msgMeta.Conn.Hostname)315 if err != nil {316 return s.ioError(err)317 }318 return s.handleAction(act)319 }320321 return module.CheckResult{}322}323324func (s *state) ioError(err error) module.CheckResult {325 if s.c.failOpen {326 s.skipChecks = true // silently permit processing to continue327 s.c.log.Error("I/O error", err)328 return module.CheckResult{}329 }330331 return module.CheckResult{332 Reject: true,333 Reason: &exterrors.SMTPError{334 Code: 451,335 EnhancedCode: exterrors.EnhancedCode{4, 7, 1},336 Message: "I/O error during policy check",337 Err: err,338 CheckName: "milter",339 Misc: map[string]interface{}{340 "milter": s.c.milterUrl,341 },342 },343 }344}345346func (s *state) CheckSender(ctx context.Context, mailFrom string) module.CheckResult {347 if s.skipChecks || s.session.ProtocolOption(milter.OptNoMailFrom) {348 return module.CheckResult{}349 }350351 fields := make([]string, 0, 2)352 fields = append(fields, "i", s.msgMeta.ID)353 // TODO: fields = append(fields, "auth_type", s.msgMeta.???)354 if s.msgMeta.Conn.AuthUser != "" {355 fields = append(fields, "auth_authen", s.msgMeta.Conn.AuthUser)356 }357 if err := s.session.Macros(milter.CodeMail, fields...); err != nil {358 return s.ioError(err)359 }360361 esmtpArgs := make([]string, 0, 2)362 if s.msgMeta.SMTPOpts.UTF8 {363 esmtpArgs = append(esmtpArgs, "SMTPUTF8")364 }365366 act, err := s.session.Mail(mailFrom, esmtpArgs)367 if err != nil {368 return s.ioError(err)369 }370 return s.handleAction(act)371}372373func (s *state) CheckRcpt(ctx context.Context, rcptTo string) module.CheckResult {374 if s.skipChecks {375 return module.CheckResult{}376 }377378 act, err := s.session.Rcpt(rcptTo, nil)379 if err != nil {380 return s.ioError(err)381 }382 return s.handleAction(act)383}384385func (s *state) CheckBody(ctx context.Context, header textproto.Header, body buffer.Buffer) module.CheckResult {386 if s.skipChecks {387 return module.CheckResult{}388 }389390 act, err := s.session.Header(header)391 if err != nil {392 return s.ioError(err)393 }394 if act.Code != milter.ActContinue {395 return s.handleAction(act)396 }397398 var modifyAct []milter.ModifyAction399400 if !s.session.ProtocolOption(milter.OptNoBody) {401 // body.Open can be expensive for on-disk buffering.402 r, err := body.Open()403 if err != nil {404 // Not ioError(err) because fail_open directive is applied only for external I/O.405 return module.CheckResult{406 Reject: true,407 Reason: &exterrors.SMTPError{408 Code: 451,409 EnhancedCode: exterrors.EnhancedCode{4, 7, 1},410 Message: "Internal error during policy check",411 Err: err,412 CheckName: "milter",413 Misc: map[string]interface{}{414 "milter": s.c.milterUrl,415 },416 },417 }418 }419420 modifyAct, act, err = s.session.BodyReadFrom(r)421 if err != nil {422 return s.ioError(err)423 }424 } else {425 modifyAct, act, err = s.session.End()426 if err != nil {427 return s.ioError(err)428 }429 }430431 result := s.handleAction(act)432 return s.apply(modifyAct, result)433}434435func (s *state) Close() error {436 return s.session.Close()437}438439var (440 _ module.Check = &Check{}441 _ module.CheckState = &state{}442)443444func init() {445 module.Register(modName, New)446}