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 authorize_sender2021import (22 "context"23 "net/mail"2425 "github.com/emersion/go-message/textproto"26 "github.com/foxcpp/maddy/framework/buffer"27 "github.com/foxcpp/maddy/framework/config"28 modconfig "github.com/foxcpp/maddy/framework/config/module"29 "github.com/foxcpp/maddy/framework/exterrors"30 "github.com/foxcpp/maddy/framework/log"31 "github.com/foxcpp/maddy/framework/module"32 "github.com/foxcpp/maddy/internal/authz"33 "github.com/foxcpp/maddy/internal/table"34 "github.com/foxcpp/maddy/internal/target"35)3637const modName = "check.authorize_sender"3839type Check struct {40 instName string41 log log.Logger4243 checkHeader bool44 emailPrepare module.Table45 userToEmail module.Table4647 unauthAction modconfig.FailAction48 noMatchAction modconfig.FailAction49 errAction modconfig.FailAction5051 fromNorm authz.NormalizeFunc52 authNorm authz.NormalizeFunc53}5455func New(_, instName string, _, inlineArgs []string) (module.Module, error) {56 return &Check{57 instName: instName,58 }, nil59}6061func (c *Check) Name() string {62 return modName63}6465func (c *Check) InstanceName() string {66 return c.instName67}6869func (c *Check) Init(cfg *config.Map) error {70 cfg.Bool("debug", true, false, &c.log.Debug)7172 cfg.Bool("check_header", false, true, &c.checkHeader)7374 cfg.Custom("prepare_email", false, false, func() (interface{}, error) {75 return &table.Identity{}, nil76 }, modconfig.TableDirective, &c.emailPrepare)77 cfg.Custom("user_to_email", false, false, func() (interface{}, error) {78 return &table.Identity{}, nil79 }, modconfig.TableDirective, &c.userToEmail)8081 cfg.Custom("unauth_action", false, false, func() (interface{}, error) {82 return modconfig.FailAction{Reject: true}, nil83 }, modconfig.FailActionDirective, &c.unauthAction)84 cfg.Custom("no_match_action", false, false, func() (interface{}, error) {85 return modconfig.FailAction{Reject: true}, nil86 }, modconfig.FailActionDirective, &c.noMatchAction)87 cfg.Custom("err_action", false, false, func() (interface{}, error) {88 return modconfig.FailAction{Reject: true}, nil89 }, modconfig.FailActionDirective, &c.errAction)9091 config.EnumMapped(cfg, "auth_normalize", true, false, authz.NormalizeFuncs, authz.NormalizeAuto,92 &c.authNorm)93 config.EnumMapped(cfg, "from_normalize", true, false, authz.NormalizeFuncs, authz.NormalizeAuto,94 &c.fromNorm)9596 if _, err := cfg.Process(); err != nil {97 return err98 }99100 return nil101}102103type state struct {104 c *Check105 msgMeta *module.MsgMetadata106 log log.Logger107}108109func (c *Check) CheckStateForMsg(_ context.Context, msgMeta *module.MsgMetadata) (module.CheckState, error) {110 return &state{111 c: c,112 msgMeta: msgMeta,113 log: target.DeliveryLogger(c.log, msgMeta),114 }, nil115}116117func (s *state) authzSender(ctx context.Context, authName, email string) module.CheckResult {118 if authName == "" {119 return s.c.unauthAction.Apply(module.CheckResult{120 Reason: &exterrors.SMTPError{121 Code: 530,122 EnhancedCode: exterrors.EnhancedCode{5, 7, 0},123 Message: "Authentication required",124 CheckName: modName,125 }})126 }127128 fromEmailNorm, err := s.c.fromNorm(email)129 if err != nil {130 return s.c.errAction.Apply(module.CheckResult{131 Reason: &exterrors.SMTPError{132 Code: 553,133 EnhancedCode: exterrors.EnhancedCode{5, 1, 7},134 Message: "Unable to normalize sender address",135 CheckName: modName,136 Err: err,137 }})138 }139 authNameNorm, err := s.c.authNorm(authName)140 if err != nil {141 return s.c.errAction.Apply(module.CheckResult{142 Reason: &exterrors.SMTPError{143 Code: 535,144 EnhancedCode: exterrors.EnhancedCode{5, 7, 8},145 Message: "Unable to normalize authorization username",146 CheckName: modName,147 }})148 }149150 var preparedEmail []string151 var ok bool152 s.log.DebugMsg("normalized names", "from", fromEmailNorm, "auth", authNameNorm)153 if emailPrepareMulti, isMulti := s.c.emailPrepare.(module.MultiTable); isMulti {154 preparedEmail, err = emailPrepareMulti.LookupMulti(ctx, fromEmailNorm)155 ok = len(preparedEmail) > 0156 } else {157 var preparedEmail_single string158 preparedEmail_single, ok, err = s.c.emailPrepare.Lookup(ctx, fromEmailNorm)159 preparedEmail = []string{preparedEmail_single}160 }161 s.log.DebugMsg("authorized emails", "preparedEmail", preparedEmail, "ok", ok)162 if err != nil {163 return s.c.errAction.Apply(module.CheckResult{164 Reason: &exterrors.SMTPError{165 Code: 454,166 EnhancedCode: exterrors.EnhancedCode{4, 7, 0},167 Message: "Internal error during policy check",168 CheckName: modName,169 Err: err,170 }})171 }172 if !ok {173 preparedEmail = []string{fromEmailNorm}174 }175176 ok, err = authz.AuthorizeEmailUse(ctx, authNameNorm, preparedEmail, s.c.userToEmail)177 if err != nil {178 return s.c.errAction.Apply(module.CheckResult{179 Reason: &exterrors.SMTPError{180 Code: 454,181 EnhancedCode: exterrors.EnhancedCode{4, 7, 0},182 Message: "Internal error during policy check",183 CheckName: modName,184 Err: err,185 }})186 }187 if !ok {188 return s.c.noMatchAction.Apply(module.CheckResult{189 Reason: &exterrors.SMTPError{190 Code: 553,191 EnhancedCode: exterrors.EnhancedCode{5, 7, 0},192 Message: "Unauthorized use of sender address",193 CheckName: modName,194 }})195 }196197 return module.CheckResult{}198}199200func (s *state) CheckConnection(_ context.Context) module.CheckResult {201 return module.CheckResult{}202}203204func (s *state) CheckSender(ctx context.Context, fromEmail string) module.CheckResult {205 if s.msgMeta.Conn == nil {206 s.log.Msg("skipping locally generated message")207 return module.CheckResult{}208 }209 authName := s.msgMeta.Conn.AuthUser210211 return s.authzSender(ctx, authName, fromEmail)212}213214func (s *state) CheckRcpt(_ context.Context, _ string) module.CheckResult {215 return module.CheckResult{}216}217218func (s *state) CheckBody(ctx context.Context, hdr textproto.Header, _ buffer.Buffer) module.CheckResult {219 if !s.c.checkHeader {220 return module.CheckResult{}221 }222 if s.msgMeta.Conn == nil {223 s.log.Msg("skipping locally generated message")224 return module.CheckResult{}225 }226 authName := s.msgMeta.Conn.AuthUser227228 fromHdr := hdr.Get("From")229 if fromHdr == "" {230 return s.c.errAction.Apply(module.CheckResult{231 Reason: &exterrors.SMTPError{232 Code: 550,233 EnhancedCode: exterrors.EnhancedCode{5, 7, 0},234 Message: "Missing From header",235 CheckName: modName,236 }})237 }238 list, err := mail.ParseAddressList(fromHdr)239 if err != nil || len(list) == 0 {240 return s.c.errAction.Apply(module.CheckResult{241 Reason: &exterrors.SMTPError{242 Code: 550,243 EnhancedCode: exterrors.EnhancedCode{5, 7, 0},244 Message: "Malformed From header",245 CheckName: modName,246 Err: err,247 }})248 }249 fromEmail := list[0].Address250 if len(list) > 1 {251 return s.c.errAction.Apply(module.CheckResult{252 Reason: &exterrors.SMTPError{253 Code: 550,254 EnhancedCode: exterrors.EnhancedCode{5, 7, 0},255 Message: "Multiple From addresses are not allowed",256 CheckName: modName,257 Err: err,258 }})259 }260261 var senderAddr string262 if senderHdr := hdr.Get("Sender"); senderHdr != "" {263 sender, err := mail.ParseAddress(senderHdr)264 if err != nil {265 return s.c.errAction.Apply(module.CheckResult{266 Reason: &exterrors.SMTPError{267 Code: 550,268 EnhancedCode: exterrors.EnhancedCode{5, 7, 0},269 Message: "Malformed Sender header",270 CheckName: modName,271 Err: err,272 }})273 }274 senderAddr = sender.Address275 }276277 res := s.authzSender(ctx, authName, fromEmail)278 if res.Reason == nil {279 return res280 }281282 if senderAddr != "" && senderAddr != fromEmail {283 res = s.authzSender(ctx, authName, senderAddr)284 if res.Reason == nil {285 return res286 }287 }288289 // Neither matched.290 return s.c.noMatchAction.Apply(module.CheckResult{291 Reason: &exterrors.SMTPError{292 Code: 553,293 EnhancedCode: exterrors.EnhancedCode{5, 7, 0},294 Message: "Unauthorized use of sender address",295 CheckName: modName,296 }})297}298299func (s *state) Close() error {300 return nil301}302303func init() {304 module.Register(modName, New)305}