+// 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
}
config Config
tlsConfig *tls.Config
- log zap.Logger
+ mta smtp.MTA
+
+ log *zap.Logger
controlChan chan ServerControlMessage
}
}
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
}
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
}
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
+}