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 smtp2021import (22 "bufio"23 "context"24 "errors"25 "fmt"26 "io"27 "net"28 "runtime/trace"29 "strconv"30 "strings"31 "sync"3233 "github.com/emersion/go-message/textproto"34 "github.com/emersion/go-sasl"35 "github.com/emersion/go-smtp"36 "github.com/foxcpp/maddy/framework/address"37 "github.com/foxcpp/maddy/framework/buffer"38 "github.com/foxcpp/maddy/framework/dns"39 "github.com/foxcpp/maddy/framework/exterrors"40 "github.com/foxcpp/maddy/framework/log"41 "github.com/foxcpp/maddy/framework/module"42 "github.com/foxcpp/maddy/internal/auth"43)4445func limitReader(r io.Reader, n int64, err error) *limitedReader {46 return &limitedReader{R: r, N: n, E: err, Enabled: true}47}4849type limitedReader struct {50 R io.Reader51 N int6452 E error53 Enabled bool54}5556// same as io.LimitedReader.Read except returning the custom error and the option57// to be disabled58func (l *limitedReader) Read(p []byte) (n int, err error) {59 if !l.Enabled {60 return l.R.Read(p)61 }62 if l.N <= 0 {63 return 0, l.E64 }65 if int64(len(p)) > l.N {66 p = p[0:l.N]67 }68 n, err = l.R.Read(p)69 l.N -= int64(n)70 return71}7273type Session struct {74 endp *Endpoint7576 // Specific for this session.77 // sessionCtx is not used for cancellation or timeouts, only for tracing.78 sessionCtx context.Context79 cancelRDNS func()80 connState module.ConnState81 repeatedMailErrs int82 loggedRcptErrors int8384 // Specific for the currently handled message.85 // msgCtx is not used for cancellation or timeouts, only for tracing.86 // It is the subcontext of sessionCtx.87 // Mutex is used to prevent Close from accessing inconsistent state when it88 // is called asynchronously to any SMTP command.89 msgLock sync.Mutex90 msgCtx context.Context91 msgTask *trace.Task92 mailFrom string93 opts smtp.MailOptions94 msgMeta *module.MsgMetadata95 delivery module.Delivery96 deliveryErr error9798 log log.Logger99}100101func (s *Session) AuthMechanisms() []string {102 return s.endp.saslAuth.SASLMechanisms()103}104105func (s *Session) Auth(mech string) (sasl.Server, error) {106 return s.endp.saslAuth.CreateSASL(mech, s.connState.RemoteAddr, func(identity string, data auth.ContextData) error {107 s.connState.AuthUser = identity108 s.connState.AuthPassword = data.Password109 return nil110 }), nil111}112113func (s *Session) Reset() {114 s.msgLock.Lock()115 defer s.msgLock.Unlock()116117 if s.delivery != nil {118 s.abort(s.msgCtx)119 }120 s.endp.Log.DebugMsg("reset")121}122123func (s *Session) releaseLimits() {124 domain := ""125 if s.mailFrom != "" {126 var err error127 _, domain, err = address.Split(s.mailFrom)128 if err != nil {129 return130 }131 }132133 addr, ok := s.msgMeta.Conn.RemoteAddr.(*net.TCPAddr)134 if !ok {135 addr = &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1)}136 }137 s.endp.limits.ReleaseMsg(addr.IP, domain)138}139140func (s *Session) abort(ctx context.Context) {141 if err := s.delivery.Abort(ctx); err != nil {142 s.endp.Log.Error("delivery abort failed", err)143 }144 s.log.Msg("aborted", "msg_id", s.msgMeta.ID)145 abortedSMTPTransactions.WithLabelValues(s.endp.name).Inc()146 s.cleanSession()147}148149func (s *Session) cleanSession() {150 s.releaseLimits()151152 s.mailFrom = ""153 s.opts = smtp.MailOptions{}154 s.msgMeta = nil155 s.delivery = nil156 s.deliveryErr = nil157 s.msgCtx = nil158 s.msgTask.End()159}160161func (s *Session) AuthPlain(username, password string) error {162 // Executed before authentication and session initialization.163 if err := s.endp.pipeline.RunEarlyChecks(context.TODO(), &s.connState); err != nil {164 return s.endp.wrapErr("", true, "AUTH", err)165 }166167 // saslAuth will handle AuthMap and AuthNormalize.168 err := s.endp.saslAuth.AuthPlain(username, password)169 if err != nil {170 s.endp.Log.Error("authentication failed", err, "username", username, "src_ip", s.connState.RemoteAddr)171172 failedLogins.WithLabelValues(s.endp.name).Inc()173174 if exterrors.IsTemporary(err) {175 return &smtp.SMTPError{176 Code: 454,177 EnhancedCode: smtp.EnhancedCode{4, 7, 0},178 Message: "Temporary authentication failure",179 }180 }181182 return &smtp.SMTPError{183 Code: 535,184 EnhancedCode: smtp.EnhancedCode{5, 7, 8},185 Message: "Invalid credentials",186 }187 }188189 s.connState.AuthUser = username190 s.connState.AuthPassword = password191192 return nil193}194195func (s *Session) startDelivery(ctx context.Context, from string, opts smtp.MailOptions) (string, error) {196 var err error197 msgMeta := &module.MsgMetadata{198 Conn: &s.connState,199 SMTPOpts: opts,200 }201 msgMeta.ID, err = module.GenerateMsgID()202 if err != nil {203 return "", err204 }205206 if s.connState.AuthUser != "" {207 s.log.Msg("incoming message",208 "src_host", msgMeta.Conn.Hostname,209 "src_ip", msgMeta.Conn.RemoteAddr.String(),210 "sender", from,211 "msg_id", msgMeta.ID,212 "username", s.connState.AuthUser,213 )214 } else {215 s.log.Msg("incoming message",216 "src_host", msgMeta.Conn.Hostname,217 "src_ip", msgMeta.Conn.RemoteAddr.String(),218 "sender", from,219 "msg_id", msgMeta.ID,220 )221 }222223 // INTERNATIONALIZATION: Do not permit non-ASCII addresses unless SMTPUTF8 is224 // used.225 if !opts.UTF8 {226 for _, ch := range from {227 if ch > 128 {228 return "", &exterrors.SMTPError{229 Code: 550,230 EnhancedCode: exterrors.EnhancedCode{5, 6, 7},231 Message: "SMTPUTF8 is required for non-ASCII senders",232 }233 }234 }235 }236237 // Decode punycode, normalize to NFC and case-fold address.238 cleanFrom := from239 if from != "" {240 cleanFrom, err = address.CleanDomain(from)241 if err != nil {242 return "", &exterrors.SMTPError{243 Code: 553,244 EnhancedCode: exterrors.EnhancedCode{5, 1, 7},245 Message: "Unable to normalize the sender address",246 }247 }248 }249250 msgMeta.OriginalFrom = from251252 domain := ""253 if cleanFrom != "" {254 _, domain, err = address.Split(cleanFrom)255 if err != nil {256 return "", err257 }258 }259 remoteIP, ok := msgMeta.Conn.RemoteAddr.(*net.TCPAddr)260 if !ok {261 remoteIP = &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1)}262 }263 if err := s.endp.limits.TakeMsg(context.Background(), remoteIP.IP, domain); err != nil {264 return "", err265 }266267 s.msgCtx, s.msgTask = trace.NewTask(ctx, "Incoming Message")268269 mailCtx, mailTask := trace.NewTask(s.msgCtx, "MAIL FROM")270 defer mailTask.End()271272 delivery, err := s.endp.pipeline.Start(mailCtx, msgMeta, cleanFrom)273 if err != nil {274 s.msgCtx = nil275 s.msgTask.End()276 s.endp.limits.ReleaseMsg(remoteIP.IP, domain)277 return msgMeta.ID, err278 }279280 startedSMTPTransactions.WithLabelValues(s.endp.name).Inc()281282 s.msgMeta = msgMeta283 s.mailFrom = cleanFrom284 s.delivery = delivery285286 return msgMeta.ID, nil287}288289func (s *Session) Mail(from string, opts *smtp.MailOptions) error {290 if s.endp.authAlwaysRequired && s.connState.AuthUser == "" {291 return smtp.ErrAuthRequired292 }293294 s.msgLock.Lock()295 defer s.msgLock.Unlock()296297 if !s.endp.deferServerReject {298 // Will initialize s.msgCtx.299 msgID, err := s.startDelivery(s.sessionCtx, from, *opts)300 if err != nil {301 if !errors.Is(err, context.DeadlineExceeded) {302 s.log.Error("MAIL FROM error", err, "msg_id", msgID)303 }304 return s.endp.wrapErr(msgID, !opts.UTF8, "MAIL", err)305 }306 }307308 // Keep the MAIL FROM argument for deferred startDelivery.309 s.mailFrom = from310 s.opts = *opts311312 return nil313}314315func (s *Session) fetchRDNSName(ctx context.Context) {316 defer trace.StartRegion(ctx, "rDNS fetch").End()317318 tcpAddr, ok := s.connState.RemoteAddr.(*net.TCPAddr)319 if !ok {320 s.connState.RDNSName.Set(nil, nil)321 return322 }323324 name, err := dns.LookupAddr(ctx, s.endp.resolver, tcpAddr.IP)325 if err != nil {326 dnsErr, ok := err.(*net.DNSError)327 if ok && dnsErr.IsNotFound {328 s.connState.RDNSName.Set(nil, nil)329 return330 }331332 if !errors.Is(err, context.Canceled) {333 // Often occurs when transaction completes before rDNS lookup and334 // rDNS name was not actually needed. So do not log cancelation335 // error if that's the case.336337 reason, misc := exterrors.UnwrapDNSErr(err)338 misc["reason"] = reason339 s.log.Error("rDNS error", exterrors.WithFields(err, misc), "src_ip", s.connState.RemoteAddr)340 }341 s.connState.RDNSName.Set(nil, err)342 return343 }344345 s.connState.RDNSName.Set(name, nil)346}347348func (s *Session) Rcpt(to string, opts *smtp.RcptOptions) error {349 s.msgLock.Lock()350 defer s.msgLock.Unlock()351352 // deferServerReject = true and this is the first RCPT TO command.353 if s.delivery == nil {354 // If we already attempted to initialize the delivery -355 // fail again.356 if s.deliveryErr != nil {357 s.repeatedMailErrs++358 // The deliveryErr is already wrapped.359 return s.deliveryErr360 }361362 // It will initialize s.msgCtx.363 msgID, err := s.startDelivery(s.sessionCtx, s.mailFrom, s.opts)364 if err != nil {365 if !errors.Is(err, context.DeadlineExceeded) {366 s.log.Error("MAIL FROM error (deferred)", err, "rcpt", to, "msg_id", msgID)367 }368 s.deliveryErr = s.endp.wrapErr(msgID, !s.opts.UTF8, "RCPT", err)369 return s.deliveryErr370 }371 }372373 rcptCtx, rcptTask := trace.NewTask(s.msgCtx, "RCPT TO")374 defer rcptTask.End()375376 if err := s.rcpt(rcptCtx, to, opts); err != nil {377 if s.loggedRcptErrors < s.endp.maxLoggedRcptErrors {378 s.log.Error("RCPT error", err, "rcpt", to, "msg_id", s.msgMeta.ID)379 s.loggedRcptErrors++380 if s.loggedRcptErrors == s.endp.maxLoggedRcptErrors {381 s.log.Msg("too many RCPT errors, possible dictonary attack", "src_ip", s.connState.RemoteAddr, "msg_id", s.msgMeta.ID)382 }383 }384 return s.endp.wrapErr(s.msgMeta.ID, !s.opts.UTF8, "RCPT", err)385 }386 s.endp.Log.Msg("RCPT ok", "rcpt", to, "msg_id", s.msgMeta.ID)387 return nil388}389390func (s *Session) rcpt(ctx context.Context, to string, opts *smtp.RcptOptions) error {391 // INTERNATIONALIZATION: Do not permit non-ASCII addresses unless SMTPUTF8 is392 // used.393 if !address.IsASCII(to) && !s.opts.UTF8 {394 return &exterrors.SMTPError{395 Code: 553,396 EnhancedCode: exterrors.EnhancedCode{5, 6, 7},397 Message: "SMTPUTF8 is required for non-ASCII recipients",398 }399 }400 cleanTo, err := address.CleanDomain(to)401 if err != nil {402 return &exterrors.SMTPError{403 Code: 501,404 EnhancedCode: exterrors.EnhancedCode{5, 1, 2},405 Message: "Unable to normalize the recipient address",406 }407 }408409 return s.delivery.AddRcpt(ctx, cleanTo, *opts)410}411412func (s *Session) Logout() error {413 s.msgLock.Lock()414 defer s.msgLock.Unlock()415416 if s.delivery != nil {417 s.abort(s.msgCtx)418419 if s.repeatedMailErrs > s.endp.maxLoggedRcptErrors {420 s.log.Msg("MAIL FROM repeated error a lot of times, possible dictonary attack", "count", s.repeatedMailErrs, "src_ip", s.connState.RemoteAddr)421 }422 }423 if s.cancelRDNS != nil {424 s.cancelRDNS()425 }426427 s.endp.sessionCnt.Add(-1)428429 return nil430}431432func (s *Session) prepareBody(r io.Reader) (textproto.Header, buffer.Buffer, error) {433 limitr := limitReader(r, s.endp.maxHeaderBytes, &exterrors.SMTPError{434 Code: 552,435 EnhancedCode: exterrors.EnhancedCode{5, 3, 4},436 Message: "Message header size exceeds limit",437 })438439 bufr := bufio.NewReader(limitr)440 header, err := textproto.ReadHeader(bufr)441 if err != nil {442 return textproto.Header{}, nil, fmt.Errorf("I/O error while parsing header: %w", err)443 }444445 if s.endp.submission {446 // The MsgMetadata is passed by pointer all the way down.447 if err := s.submissionPrepare(s.msgMeta, &header); err != nil {448 return textproto.Header{}, nil, err449 }450 }451452 // the header size check is done. The message size will be checked by go-smtp453 limitr.Enabled = false454455 buf, err := s.endp.buffer(bufr)456 if err != nil {457 return textproto.Header{}, nil, fmt.Errorf("I/O error while writing buffer: %w", err)458 }459460 return header, buf, nil461}462463func (s *Session) Data(r io.Reader) error {464 s.msgLock.Lock()465 defer s.msgLock.Unlock()466467 bodyCtx, bodyTask := trace.NewTask(s.msgCtx, "DATA")468 defer bodyTask.End()469470 wrapErr := func(err error) error {471 s.log.Error("DATA error", err, "msg_id", s.msgMeta.ID)472 return s.endp.wrapErr(s.msgMeta.ID, !s.opts.UTF8, "DATA", err)473 }474475 header, buf, err := s.prepareBody(r)476 if err != nil {477 return wrapErr(err)478 }479 defer func() {480 if err := buf.Remove(); err != nil {481 s.log.Error("failed to remove buffered body", err)482 }483484 // go-smtp will call Reset, but it will call Abort if delivery is non-nil.485 s.cleanSession()486 }()487488 if err := s.checkRoutingLoops(header); err != nil {489 return wrapErr(err)490 }491492 if strings.EqualFold(header.Get("TLS-Required"), "No") {493 s.msgMeta.TLSRequireOverride = true494 }495496 if err := s.delivery.Body(bodyCtx, header, buf); err != nil {497 return wrapErr(err)498 }499500 if err := s.delivery.Commit(bodyCtx); err != nil {501 return wrapErr(err)502 }503504 s.log.Msg("accepted", "msg_id", s.msgMeta.ID)505506 return nil507}508509type statusWrapper struct {510 sc smtp.StatusCollector511 s *Session512}513514func (sw statusWrapper) SetStatus(rcpt string, err error) {515 sw.sc.SetStatus(rcpt, sw.s.endp.wrapErr(sw.s.msgMeta.ID, !sw.s.opts.UTF8, "DATA", err))516}517518func (s *Session) LMTPData(r io.Reader, sc smtp.StatusCollector) error {519 s.msgLock.Lock()520 defer s.msgLock.Unlock()521522 bodyCtx, bodyTask := trace.NewTask(s.msgCtx, "DATA")523 defer bodyTask.End()524525 wrapErr := func(err error) error {526 s.log.Error("DATA error", err, "msg_id", s.msgMeta.ID)527 return s.endp.wrapErr(s.msgMeta.ID, !s.opts.UTF8, "DATA", err)528 }529530 header, buf, err := s.prepareBody(r)531 if err != nil {532 return wrapErr(err)533 }534 defer func() {535 if err := buf.Remove(); err != nil {536 s.log.Error("failed to remove buffered body", err)537 }538539 // go-smtp will call Reset, but it will call Abort if delivery is non-nil.540 s.cleanSession()541 }()542543 if strings.EqualFold(header.Get("TLS-Required"), "No") {544 s.msgMeta.TLSRequireOverride = true545 }546547 if err := s.checkRoutingLoops(header); err != nil {548 return wrapErr(err)549 }550551 s.delivery.(module.PartialDelivery).BodyNonAtomic(bodyCtx, statusWrapper{sc, s}, header, buf)552553 // We can't really tell whether it is failed completely or succeeded554 // so always commit. Should be harmless, anyway.555 if err := s.delivery.Commit(bodyCtx); err != nil {556 return wrapErr(err)557 }558559 s.log.Msg("accepted", "msg_id", s.msgMeta.ID)560561 return nil562}563564func (s *Session) checkRoutingLoops(header textproto.Header) error {565 // RFC 5321 Section 6.3:566 // >Simple counting of the number of "Received:" header fields in a567 // >message has proven to be an effective, although rarely optimal,568 // >method of detecting loops in mail systems.569 receivedCount := 0570 for f := header.FieldsByKey("Received"); f.Next(); {571 receivedCount++572 }573 if receivedCount > s.endp.maxReceived {574 return &exterrors.SMTPError{575 Code: 554,576 EnhancedCode: exterrors.EnhancedCode{5, 4, 6},577 Message: fmt.Sprintf("Too many Received header fields (%d), possible forwarding loop", receivedCount),578 }579 }580581 return nil582}583584func (endp *Endpoint) wrapErr(msgId string, mangleUTF8 bool, command string, err error) error {585 if err == nil {586 return nil587 }588589 if errors.Is(err, context.DeadlineExceeded) {590 return &smtp.SMTPError{591 Code: 451,592 EnhancedCode: smtp.EnhancedCode{4, 4, 5},593 Message: "High load, try again later",594 }595 }596597 res := &smtp.SMTPError{598 Code: 554,599 EnhancedCode: smtp.EnhancedCodeNotSet,600 // Err on the side of caution if the error lacks SMTP annotations. If601 // we just pass the error text through, we might accidenetally disclose602 // details of server configuration.603 Message: "Internal server error",604 }605606 if exterrors.IsTemporary(err) {607 res.Code = 451608 }609610 ctxInfo := exterrors.Fields(err)611 ctxCode, ok := ctxInfo["smtp_code"].(int)612 if ok {613 res.Code = ctxCode614 }615 ctxEnchCode, ok := ctxInfo["smtp_enchcode"].(exterrors.EnhancedCode)616 if ok {617 res.EnhancedCode = smtp.EnhancedCode(ctxEnchCode)618 }619 ctxMsg, ok := ctxInfo["smtp_msg"].(string)620 if ok {621 res.Message = ctxMsg622 }623624 if smtpErr, ok := err.(*smtp.SMTPError); ok {625 endp.Log.Printf("plain SMTP error returned, this is deprecated")626 res.Code = smtpErr.Code627 res.EnhancedCode = smtpErr.EnhancedCode628 res.Message = smtpErr.Message629 }630631 if msgId != "" {632 res.Message += " (msg ID = " + msgId + ")"633 }634635 failedCmds.WithLabelValues(endp.name, command, strconv.Itoa(res.Code),636 fmt.Sprintf("%d.%d.%d",637 res.EnhancedCode[0],638 res.EnhancedCode[1],639 res.EnhancedCode[2])).Inc()640641 // INTERNATIONALIZATION: See RFC 6531 Section 3.7.4.1.642 if mangleUTF8 {643 b := strings.Builder{}644 b.Grow(len(res.Message))645 for _, ch := range res.Message {646 if ch > 128 {647 b.WriteRune('?')648 } else {649 b.WriteRune(ch)650 }651 }652 res.Message = b.String()653 }654655 return res656}