1
0
mirror of https://github.com/axllent/mailpit.git synced 2025-01-26 03:52:09 +02:00

Feature: Inject/update Bcc header for missing addresses when SMTP recipients do not match messsage headers

In order to capture Bcc recipients from some platfoms (eg: Laravel) when the SMTP recipients contain Bcc recipients but are not listed in the message headers, the missing addresses are now added into the message Bcc header. If the Bcc header does not exist then it is created.

Resolves #35
This commit is contained in:
Ralph Slooten 2023-04-15 11:34:31 +12:00
parent 7c329b56f8
commit 9c8329a05c
3 changed files with 126 additions and 23 deletions

View File

@ -12,11 +12,11 @@ var (
// sendmailCmd represents the sendmail command
var sendmailCmd = &cobra.Command{
Use: "sendmail",
Short: "A sendmail command replacement",
Long: `A sendmail command replacement.
Use: "sendmail [flags] [recipients]",
Short: "A sendmail command replacement for Mailpit",
Long: `A sendmail command replacement for Mailpit.
You can optionally create a symlink called 'sendmail' to the main binary.`,
You can optionally create a symlink called 'sendmail' to the Mailpit binary.`,
Run: func(_ *cobra.Command, _ []string) {
sendmail.Run()
},
@ -26,8 +26,12 @@ func init() {
rootCmd.AddCommand(sendmailCmd)
// these are simply repeated for cli consistency
sendmailCmd.Flags().StringVarP(&fromAddr, "from", "f", fromAddr, "SMTP sender")
sendmailCmd.Flags().StringVar(&smtpAddr, "smtp-addr", smtpAddr, "SMTP server address")
sendmailCmd.Flags().StringVarP(&fromAddr, "from", "f", "", "SMTP sender")
sendmailCmd.Flags().BoolVarP(&sendmail.Verbose, "verbose", "v", false, "Verbose mode (sends debug output to stderr)")
sendmailCmd.Flags().BoolP("long-b", "b", false, "Ignored. This flag exists for sendmail compatibility.")
sendmailCmd.Flags().BoolP("long-i", "i", false, "Ignored. This flag exists for sendmail compatibility.")
sendmailCmd.Flags().BoolP("long-o", "o", false, "Ignored. This flag exists for sendmail compatibility.")
sendmailCmd.Flags().BoolP("long-s", "s", false, "Ignored. This flag exists for sendmail compatibility.")
sendmailCmd.Flags().BoolP("long-t", "t", false, "Ignored. This flag exists for sendmail compatibility.")
}

View File

@ -1,3 +1,4 @@
// Package cmd is the sendmail cli
package cmd
/**
@ -13,10 +14,16 @@ import (
"os"
"os/user"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/utils/logger"
flag "github.com/spf13/pflag"
)
var (
// Verbose flag
Verbose bool
)
// Run the Mailpit sendmail replacement.
func Run() {
host, err := os.Hostname()
@ -42,24 +49,31 @@ func Run() {
fromAddr = os.Getenv("MP_SENDMAIL_FROM")
}
var verbose bool
// override defaults from cli flags
flag.StringVarP(&fromAddr, "from", "f", fromAddr, "SMTP sender")
flag.StringVar(&smtpAddr, "smtp-addr", smtpAddr, "SMTP server address")
flag.BoolVarP(&verbose, "verbose", "v", false, "Verbose mode (sends debug output to stderr)")
flag.BoolVarP(&Verbose, "verbose", "v", false, "Verbose mode (sends debug output to stderr)")
flag.BoolP("long-b", "b", false, "Ignored. This flag exists for sendmail compatibility.")
flag.BoolP("long-i", "i", false, "Ignored. This flag exists for sendmail compatibility.")
flag.BoolP("long-o", "o", false, "Ignored. This flag exists for sendmail compatibility.")
flag.BoolP("long-s", "s", false, "Ignored. This flag exists for sendmail compatibility.")
flag.BoolP("long-t", "t", false, "Ignored. This flag exists for sendmail compatibility.")
flag.CommandLine.SortFlags = false
// set the default help
flag.Usage = func() {
fmt.Printf("A sendmail command replacement for Mailpit (%s).\n\n", config.Version)
fmt.Printf("Usage:\n %s [flags] [recipients]\n", os.Args[0])
fmt.Println("\nFlags:")
flag.PrintDefaults()
}
flag.Parse()
// allow recipient to be passed as an argument
recip = flag.Args()
if verbose {
if Verbose {
fmt.Fprintln(os.Stderr, smtpAddr, fromAddr)
}
@ -75,13 +89,30 @@ func Run() {
os.Exit(11)
}
if len(recip) == 0 {
// We only need to parse the message to get a recipient if none where
// provided on the command line.
recip = append(recip, msg.Header.Get("To"))
addresses := []string{}
if len(recip) > 0 {
addresses = recip
} else {
// get all recipients in To, Cc and Bcc
if to, err := msg.Header.AddressList("To"); err == nil {
for _, a := range to {
addresses = append(addresses, a.Address)
}
}
if cc, err := msg.Header.AddressList("Cc"); err == nil {
for _, a := range cc {
addresses = append(addresses, a.Address)
}
}
if bcc, err := msg.Header.AddressList("Bcc"); err == nil {
for _, a := range bcc {
addresses = append(addresses, a.Address)
}
}
}
err = smtp.SendMail(smtpAddr, nil, fromAddr, recip, body)
err = smtp.SendMail(smtpAddr, nil, fromAddr, addresses, body)
if err != nil {
fmt.Fprintln(os.Stderr, "error sending mail")
logger.Log().Fatal(err)

View File

@ -3,6 +3,7 @@ package smtpd
import (
"bytes"
"fmt"
"net"
"net/mail"
"regexp"
@ -17,25 +18,60 @@ import (
func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
msg, err := mail.ReadMessage(bytes.NewReader(data))
if err != nil {
logger.Log().Errorf("error parsing message: %s", err.Error())
logger.Log().Errorf("[smtp] error parsing message: %s", err.Error())
return err
}
if _, err := storage.Store(data); err != nil {
// Value with size 4800709 exceeded 1048576 limit
re := regexp.MustCompile(`(Value with size \d+ exceeded \d+ limit)`)
tooLarge := re.FindStringSubmatch(err.Error())
if len(tooLarge) > 0 {
logger.Log().Errorf("[db] error storing message: %s", tooLarge[0])
// build array of all addresses in the header to compare to the []to array
emails, hasBccHeader := scanAddressesInHeader(msg.Header)
missingAddresses := []string{}
for _, a := range to {
// loop through passed email addresses to check if they are in the headers
if _, err := mail.ParseAddress(a); err == nil {
_, ok := emails[strings.ToLower(a)]
if !ok {
missingAddresses = append(missingAddresses, a)
}
} else {
logger.Log().Errorf("[db] error storing message")
logger.Log().Errorf(err.Error())
logger.Log().Warnf("[smtp] ignoring invalid email address: %s", a)
}
}
// add missing email addresses to Bcc (eg: Laravel doesn't include these in the headers)
if len(missingAddresses) > 0 {
if hasBccHeader {
// email already has Bcc header, add to existing addresses
re := regexp.MustCompile(`(?i)(^|\n)(Bcc: )`)
replaced := false
data = re.ReplaceAllFunc(data, func(r []byte) []byte {
if replaced {
return r
}
replaced = true // only replace first occurence
return re.ReplaceAll(r, []byte("${1}Bcc: "+strings.Join(missingAddresses, ", ")+", "))
})
} else {
// prepend new Bcc header
bcc := []byte(fmt.Sprintf("Bcc: %s\r\n", strings.Join(missingAddresses, ", ")))
data = append(bcc, data...)
}
logger.Log().Debugf("[smtp] added missing addresses to Bcc header: %s", strings.Join(missingAddresses, ", "))
}
if _, err := storage.Store(data); err != nil {
logger.Log().Errorf("[db] error storing message: %d", err.Error())
return err
}
subject := msg.Header.Get("Subject")
logger.Log().Debugf("[smtp] received (%s) from:%s to:%s subject:%q", cleanIP(origin), from, to[0], subject)
return nil
}
@ -46,12 +82,14 @@ func authHandler(remoteAddr net.Addr, mechanism string, username []byte, passwor
} else {
logger.Log().Warnf("[smtp] deny %s login:%q from:%s", mechanism, string(username), cleanIP(remoteAddr))
}
return allow, nil
}
// Allow any username and password
func authHandlerAny(remoteAddr net.Addr, mechanism string, username []byte, password []byte, shared []byte) (bool, error) {
logger.Log().Debugf("[smtp] allow %s login %q from %s", mechanism, string(username), cleanIP(remoteAddr))
return true, nil
}
@ -113,3 +151,33 @@ func cleanIP(i net.Addr) string {
return parts[0]
}
// Returns a list of all lowercased emails found in To, Cc and Bcc,
// as well as whether there is a Bcc field
func scanAddressesInHeader(h mail.Header) (map[string]bool, bool) {
emails := make(map[string]bool)
hasBccHeader := false
if recipients, err := h.AddressList("To"); err == nil {
for _, r := range recipients {
emails[strings.ToLower(r.Address)] = true
}
}
if recipients, err := h.AddressList("Cc"); err == nil {
for _, r := range recipients {
emails[strings.ToLower(r.Address)] = true
}
}
recipients, err := h.AddressList("Bcc")
if err == nil {
for _, r := range recipients {
emails[strings.ToLower(r.Address)] = true
}
hasBccHeader = true
}
return emails, hasBccHeader
}