Bump the version to 2.1.0.
[mailpopbox.git] / smtp.go
diff --git a/smtp.go b/smtp.go
index 5b6952d8a6928597f4615da320bbd04f34557961..c22728e8f29b0de18f196b306594f4d6c0761247 100644 (file)
--- a/smtp.go
+++ b/smtp.go
@@ -7,18 +7,22 @@
 package main
 
 import (
+       "bytes"
        "crypto/tls"
        "fmt"
        "net"
        "net/mail"
        "os"
        "path"
+       "regexp"
 
        "go.uber.org/zap"
 
        "src.bluestatic.org/mailpopbox/smtp"
 )
 
+var sendAsSubject = regexp.MustCompile(`(?i)\[sendas:\s*([a-zA-Z0-9\.\-_]+)\]`)
+
 func runSMTPServer(config Config, log *zap.Logger) <-chan ServerControlMessage {
        server := smtpServer{
                config:      config,
@@ -98,9 +102,15 @@ 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
 }
 
@@ -146,17 +156,93 @@ func (server *smtpServer) DeliverMessage(en smtp.Envelope) *smtp.ReplyLine {
        return nil
 }
 
-func (server *smtpServer) RelayMessage(en smtp.Envelope) {
-       go server.mta.RelayMessage(en)
-}
-
-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
+}