Bump the version to 2.1.0.
[mailpopbox.git] / smtp.go
diff --git a/smtp.go b/smtp.go
index 81aae4404124dde452faeec973c197121ff28db4..c22728e8f29b0de18f196b306594f4d6c0761247 100644 (file)
--- a/smtp.go
+++ b/smtp.go
@@ -1,24 +1,35 @@
+// mailpopbox
+// Copyright 2020 Blue Static <https://www.bluestatic.org>
+// This program is free software licensed under the GNU General Public License,
+// version 3.0. The full text of the license can be found in LICENSE.txt.
+// SPDX-License-Identifier: GPL-3.0-only
+
 package main
 
 import (
+       "bytes"
        "crypto/tls"
        "fmt"
        "net"
        "net/mail"
        "os"
        "path"
+       "regexp"
 
-       "github.com/uber-go/zap"
+       "go.uber.org/zap"
 
        "src.bluestatic.org/mailpopbox/smtp"
 )
 
-func runSMTPServer(config Config, log zap.Logger) <-chan ServerControlMessage {
+var sendAsSubject = regexp.MustCompile(`(?i)\[sendas:\s*([a-zA-Z0-9\.\-_]+)\]`)
+
+func runSMTPServer(config Config, log *zap.Logger) <-chan ServerControlMessage {
        server := smtpServer{
                config:      config,
                controlChan: make(chan ServerControlMessage),
                log:         log.With(zap.String("server", "smtp")),
        }
+       server.mta = smtp.NewDefaultMTA(&server, server.log)
        go server.run()
        return server.controlChan
 }
@@ -27,7 +38,9 @@ type smtpServer struct {
        config    Config
        tlsConfig *tls.Config
 
-       log zap.Logger
+       mta smtp.MTA
+
+       log *zap.Logger
 
        controlChan chan ServerControlMessage
 }
@@ -89,26 +102,52 @@ func (server *smtpServer) TLSConfig() *tls.Config {
 }
 
 func (server *smtpServer) VerifyAddress(addr mail.Address) smtp.ReplyLine {
-       if server.maildropForAddress(addr) == "" {
+       s := server.configForAddress(addr)
+       if s == nil {
                return smtp.ReplyBadMailbox
        }
+       for _, blocked := range s.BlockedAddresses {
+               if blocked == addr.Address {
+                       return smtp.ReplyMailboxUnallowed
+               }
+       }
        return smtp.ReplyOK
 }
 
-func (server *smtpServer) OnEHLO() *smtp.ReplyLine {
-       return nil
+func (server *smtpServer) Authenticate(authz, authc, passwd string) bool {
+       authcAddr, err := mail.ParseAddress(authc)
+       if err != nil {
+               return false
+       }
+
+       authzAddr, err := mail.ParseAddress(authz)
+       if authz != "" && err != nil {
+               return false
+       }
+
+       domain := smtp.DomainForAddress(*authcAddr)
+       for _, s := range server.config.Servers {
+               if domain == s.Domain {
+                       authOk := authc == MailboxAccount+s.Domain && passwd == s.MailboxPassword
+                       if authzAddr != nil {
+                               authOk = authOk && smtp.DomainForAddress(*authzAddr) == domain
+                       }
+                       return authOk
+               }
+       }
+       return false
 }
 
-func (server *smtpServer) OnMessageDelivered(en smtp.Envelope) *smtp.ReplyLine {
+func (server *smtpServer) DeliverMessage(en smtp.Envelope) *smtp.ReplyLine {
        maildrop := server.maildropForAddress(en.RcptTo[0])
        if maildrop == "" {
-               // TODO: log error
+               server.log.Error("faild to open maildrop to deliver message", zap.String("id", en.ID))
                return &smtp.ReplyBadMailbox
        }
 
        f, err := os.Create(path.Join(maildrop, en.ID+".msg"))
        if err != nil {
-               // TODO: log error
+               server.log.Error("failed to create message file", zap.String("id", en.ID), zap.Error(err))
                return &smtp.ReplyBadMailbox
        }
 
@@ -117,13 +156,93 @@ func (server *smtpServer) OnMessageDelivered(en smtp.Envelope) *smtp.ReplyLine {
        return nil
 }
 
-func (server *smtpServer) maildropForAddress(addr mail.Address) string {
+func (server *smtpServer) configForAddress(addr mail.Address) *Server {
        domain := smtp.DomainForAddress(addr)
        for _, s := range server.config.Servers {
                if domain == s.Domain {
-                       return s.MaildropPath
+                       return &s
                }
        }
+       return nil
+}
 
+func (server *smtpServer) maildropForAddress(addr mail.Address) string {
+       s := server.configForAddress(addr)
+       if s != nil {
+               return s.MaildropPath
+       }
        return ""
 }
+
+func (server *smtpServer) RelayMessage(en smtp.Envelope, authc string) {
+       go func() {
+               log := server.log.With(zap.String("id", en.ID))
+               server.handleSendAs(log, &en, authc)
+               server.mta.RelayMessage(en)
+       }()
+}
+
+func (server *smtpServer) handleSendAs(log *zap.Logger, en *smtp.Envelope, authc string) {
+       // Find the separator between the message header and body.
+       headerIdx := bytes.Index(en.Data, []byte("\n\n"))
+       if headerIdx == -1 {
+               log.Error("send-as: could not find headers index")
+               return
+       }
+
+       var buf bytes.Buffer
+
+       headers := bytes.SplitAfter(en.Data[:headerIdx], []byte("\n"))
+
+       var fromIdx, subjectIdx int
+       for i, header := range headers {
+               if bytes.HasPrefix(header, []byte("From:")) {
+                       fromIdx = i
+                       continue
+               }
+               if bytes.HasPrefix(header, []byte("Subject:")) {
+                       subjectIdx = i
+                       continue
+               }
+       }
+
+       if subjectIdx == -1 {
+               log.Error("send-as: could not find Subject header")
+               return
+       }
+       if fromIdx == -1 {
+               log.Error("send-as: could not find From header")
+               return
+       }
+
+       sendAs := sendAsSubject.FindSubmatchIndex(headers[subjectIdx])
+       if sendAs == nil {
+               // No send-as modification.
+               return
+       }
+
+       // Submatch 0 is the whole sendas magic. Submatch 1 is the address prefix.
+       sendAsUser := headers[subjectIdx][sendAs[2]:sendAs[3]]
+       sendAsAddress := string(sendAsUser) + "@" + smtp.DomainForAddressString(authc)
+
+       log.Info("handling send-as", zap.String("address", sendAsAddress))
+
+       for i, header := range headers {
+               if i == subjectIdx {
+                       buf.Write(header[:sendAs[0]])
+                       buf.Write(header[sendAs[1]:])
+               } else if i == fromIdx {
+                       addressStart := bytes.LastIndexByte(header, byte('<'))
+                       buf.Write(header[:addressStart+1])
+                       buf.WriteString(sendAsAddress)
+                       buf.WriteString(">\n")
+               } else {
+                       buf.Write(header)
+               }
+       }
+
+       buf.Write(en.Data[headerIdx:])
+
+       en.Data = buf.Bytes()
+       en.MailFrom.Address = sendAsAddress
+}