Skip to content

Commit

Permalink
Merge branch 'release/v1.6.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
axllent committed Apr 21, 2023
2 parents 26a84bc + 031b569 commit 176f128
Show file tree
Hide file tree
Showing 27 changed files with 1,307 additions and 2,278 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

Notable changes to Mailpit will be documented in this file.

## [v1.6.0]

### API
- Enable cross-origin resource sharing (CORS) configuration
- Message relay / release
- Include Return-Path in message summary data

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

### Libs
- Update Go modules
- Update node modules

### UI
- Display Return-Path if different to the From address
- Message release functionality


## [v1.5.5]

### Docker
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ Mailpit is inspired by [MailHog](#why-rewrite-mailhog), but much, much faster.
- Optional browser notifications for new mail (HTTPS only)
- Configurable automatic email pruning (default keeps the most recent 500 emails)
- Email storage either in a temporary or persistent database ([see wiki](https://github.com/axllent/mailpit/wiki/Email-storage))
- Fast SMTP processing & storing - approximately 70-100 emails per second depending on CPU, network speed & email size
- Can handle tens of thousands of emails
- Fast SMTP processing & storing - approximately 70-100 emails per second depending on CPU, network speed & email size, easily handling tens of thousands of emails
- SMTP relaying / message release - relay messages via a different SMTP server ([see wiki](https://github.com/axllent/mailpit/wiki/SMTP-relay))
- Optional SMTP with STARTTLS & SMTP authentication, including an "accept anything" mode ([see wiki](https://github.com/axllent/mailpit/wiki/SMTP-with-STARTTLS-and-authentication))
- Optional HTTPS for web UI ([see wiki](https://github.com/axllent/mailpit/wiki/HTTPS))
- Optional basic authentication for web UI ([see wiki](https://github.com/axllent/mailpit/wiki/Basic-authentication))
Expand Down Expand Up @@ -90,8 +90,8 @@ You can build a Mailpit-specific sendmail binary from source (see [building from

## Why rewrite MailHog?

I had been using MailHog for a few years to intercept and test emails generated from several projects. MailHog has a number of severe performance issues, many of the modules are horribly out of date, and other than a few accepted MRs, it is not actively developed.
I had been using MailHog for a few years to intercept and test emails generated from several projects. MailHog has a number of performance issues, many of the frontend and Go modules are horribly out of date, and it is not actively developed.

Initially I started trying to upgrade a fork of MailHog (both the UI as well as the HTTP server & API), but soon discovered that it is (with all due respect) very poorly designed. It is over-engineered (split over 9 separate projects) and has too many unnecessary features for my purpose. It performs exceptionally poorly when dealing with large amounts of emails or processing any email with an attachment (a single email with a 3MB attachment can take over a minute to ingest). The API also transmits a lot of duplicate and unnecessary data on every message request for all web calls, and there is no HTTP compression.
Initially I tried to upgrade a fork of MailHog (both the UI as well as the HTTP server & API), but soon discovered that it is (with all due respect to its authors) poorly designed. It is in my opinion over-engineered (split over 9 separate projects), and performs very poorly when dealing with large amounts of emails or processing emails with an attachments (a single email with a 3MB attachment can take over a minute to ingest). Finally, the API transmits a lot of duplicate and unnecessary data on every browser request, and there is no HTTP compression.

In order to improve it I felt it needed to be completely rewritten, and so Mailpit was born.
24 changes: 20 additions & 4 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func init() {
rootCmd.Flags().StringVarP(&config.HTTPListen, "listen", "l", config.HTTPListen, "HTTP bind interface and port for UI")
rootCmd.Flags().IntVarP(&config.MaxMessages, "max", "m", config.MaxMessages, "Max number of messages to store")
rootCmd.Flags().StringVar(&config.Webroot, "webroot", config.Webroot, "Set the webroot for web UI & API")
rootCmd.Flags().StringVar(&server.AccessControlAllowOrigin, "api-cors", server.AccessControlAllowOrigin, "Set API CORS Access-Control-Allow-Origin header")
rootCmd.Flags().BoolVar(&config.UseMessageDates, "use-message-dates", config.UseMessageDates, "Use message dates as the received dates")

rootCmd.Flags().StringVar(&config.UIAuthFile, "ui-auth-file", config.UIAuthFile, "A password file for web UI authentication")
Expand All @@ -98,8 +99,11 @@ func init() {
rootCmd.Flags().BoolVar(&config.SMTPAuthAllowInsecure, "smtp-auth-allow-insecure", config.SMTPAuthAllowInsecure, "Enable insecure PLAIN & LOGIN authentication")
rootCmd.Flags().StringVarP(&config.SMTPCLITags, "tag", "t", config.SMTPCLITags, "Tag new messages matching filters")

rootCmd.Flags().BoolVarP(&config.QuietLogging, "quiet", "q", config.QuietLogging, "Quiet logging (errors only)")
rootCmd.Flags().BoolVarP(&config.VerboseLogging, "verbose", "v", config.VerboseLogging, "Verbose logging")
rootCmd.Flags().StringVar(&config.SMTPRelayConfigFile, "smtp-relay-config", config.SMTPRelayConfigFile, "SMTP configuration file to allow releasing messages")
rootCmd.Flags().BoolVar(&config.SMTPRelayAllIncoming, "smtp-relay-all", config.SMTPRelayAllIncoming, "Relay all incoming messages via external SMTP server (caution!)")

rootCmd.Flags().BoolVarP(&logger.QuietLogging, "quiet", "q", logger.QuietLogging, "Quiet logging (errors only)")
rootCmd.Flags().BoolVarP(&logger.VerboseLogging, "verbose", "v", logger.VerboseLogging, "Verbose logging")

// deprecated flags 2022/08/06
rootCmd.Flags().StringVarP(&config.UIAuthFile, "auth-file", "a", config.UIAuthFile, "A password file for web UI authentication")
Expand Down Expand Up @@ -179,20 +183,32 @@ func initConfigFromEnv() {
config.SMTPAuthAllowInsecure = true
}

// Relay server config
if len(os.Getenv("MP_SMTP_RELAY_CONFIG")) > 0 {
config.SMTPRelayConfigFile = os.Getenv("MP_SMTP_RELAY_CONFIG")
}
if getEnabledFromEnv("MP_SMTP_RELAY_ALL") {
config.SMTPRelayAllIncoming = true
}

// Misc options
if len(os.Getenv("MP_WEBROOT")) > 0 {
config.Webroot = os.Getenv("MP_WEBROOT")
}
if len(os.Getenv("MP_API_CORS")) > 0 {
server.AccessControlAllowOrigin = os.Getenv("MP_API_CORS")
}
if getEnabledFromEnv("MP_USE_MESSAGE_DATES") {
config.UseMessageDates = true
}
if getEnabledFromEnv("MP_USE_MESSAGE_DATES") {
config.UseMessageDates = true
}
if getEnabledFromEnv("MP_QUIET") {
config.QuietLogging = true
logger.QuietLogging = true
}
if getEnabledFromEnv("MP_VERBOSE") {
config.VerboseLogging = true
logger.VerboseLogging = true
}
}

Expand Down
14 changes: 9 additions & 5 deletions cmd/sendmail.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
},
Expand All @@ -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.")
}
116 changes: 99 additions & 17 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import (
"regexp"
"strings"

"github.com/axllent/mailpit/utils/logger"
"github.com/mattn/go-shellwords"
"github.com/tg123/go-htpasswd"
"gopkg.in/yaml.v3"
)

var (
Expand All @@ -30,15 +32,6 @@ var (
// UseMessageDates sets the Created date using the message date, not the delivered date
UseMessageDates bool

// VerboseLogging for console output
VerboseLogging = false

// QuietLogging for console output (errors only)
QuietLogging = false

// NoLogging for tests
NoLogging = false

// UITLSCert file
UITLSCert string

Expand All @@ -63,8 +56,8 @@ var (
// SMTPAuthFile for SMTP authentication
SMTPAuthFile string

// SMTPAuth used for euthentication
SMTPAuth *htpasswd.File
// SMTPAuthConfig used for authentication auto-generated from SMTPAuthFile
SMTPAuthConfig *htpasswd.File

// SMTPAuthAllowInsecure allows PLAIN & LOGIN unencrypted authentication
SMTPAuthAllowInsecure bool
Expand All @@ -79,7 +72,20 @@ var (
TagRegexp = regexp.MustCompile(`^([a-zA-Z0-9\-\ \_]){3,}$`)

// SMTPTags are expressions to apply tags to new mail
SMTPTags []Tag
SMTPTags []AutoTag

// SMTPRelayConfigFile to parse a yaml file and store config of relay SMTP server
SMTPRelayConfigFile string

// SMTPRelayConfig to parse a yaml file and store config of relay SMTP server
SMTPRelayConfig smtpRelayConfigStruct

// ReleaseEnabled is whether message releases are enabled, requires a valid SMTPRelayConfigFile
ReleaseEnabled = false

// SMTPRelayAllIncoming is whether to relay all incoming messages via preconfgured SMTP server.
// Use with extreme caution!
SMTPRelayAllIncoming = false

// ContentSecurityPolicy for HTTP server
ContentSecurityPolicy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self'; img-src * data: blob:; font-src 'self' data:; media-src 'self'; connect-src 'self' ws: wss:; object-src 'none'; base-uri 'self';"
Expand All @@ -94,12 +100,25 @@ var (
RepoBinaryName = "mailpit"
)

// Tag struct
type Tag struct {
// AutoTag struct for auto-tagging
type AutoTag struct {
Tag string
Match string
}

// SMTPRelayConfigStruct struct for parsing yaml & storing variables
type smtpRelayConfigStruct struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
STARTTLS bool `yaml:"starttls"`
AllowInsecure bool `yaml:"allow-insecure"`
Auth string `yaml:"auth"` // none, plain, cram-md5
Username string `yaml:"username"` // plain & cram-md5
Password string `yaml:"password"` // plain
Secret string `yaml:"secret"` // cram-md5
ReturnPath string `yaml:"return-path"` // allows overriding the boune address
}

// VerifyConfig wil do some basic checking
func VerifyConfig() error {
if DataFile != "" && isDir(DataFile) {
Expand Down Expand Up @@ -167,7 +186,7 @@ func VerifyConfig() error {
if err != nil {
return err
}
SMTPAuth = a
SMTPAuthConfig = a
}

if SMTPTLSCert == "" && (SMTPAuthFile != "" || SMTPAuthAcceptAny) && !SMTPAuthAllowInsecure {
Expand All @@ -182,7 +201,7 @@ func VerifyConfig() error {
s := strings.TrimRight(path.Join("/", Webroot, "/"), "/") + "/"
Webroot = s

SMTPTags = []Tag{}
SMTPTags = []AutoTag{}

p := shellwords.NewParser()

Expand All @@ -203,14 +222,77 @@ func VerifyConfig() error {
if len(match) == 0 {
return fmt.Errorf("Invalid tag match (%s) - no search detected", tag)
}
SMTPTags = append(SMTPTags, Tag{Tag: tag, Match: match})
SMTPTags = append(SMTPTags, AutoTag{Tag: tag, Match: match})
} else {
return fmt.Errorf("Error parsing tags (%s)", a)
}
}
}

if err := parseRelayConfig(SMTPRelayConfigFile); err != nil {
return err
}

if !ReleaseEnabled && SMTPRelayAllIncoming {
return errors.New("SMTP relay config must be set to relay all messages")
}

if SMTPRelayAllIncoming {
// this deserves a warning
logger.Log().Warnf("[smtp] enabling automatic relay of all new messages via %s:%d", SMTPRelayConfig.Host, SMTPRelayConfig.Port)
}

return nil
}

// Parse & validate the SMTPRelayConfigFile (if set)
func parseRelayConfig(c string) error {
if c == "" {
return nil
}

if !isFile(c) {
return fmt.Errorf("SMTP relay configuration not found: %s", SMTPRelayConfigFile)
}

data, err := os.ReadFile(c)
if err != nil {
return err
}

if err := yaml.Unmarshal(data, &SMTPRelayConfig); err != nil {
return err
}

if SMTPRelayConfig.Host == "" {
return errors.New("SMTP relay host not set")
}

if SMTPRelayConfig.Port == 0 {
SMTPRelayConfig.Port = 25 // default
}

SMTPRelayConfig.Auth = strings.ToLower(SMTPRelayConfig.Auth)

if SMTPRelayConfig.Auth == "" || SMTPRelayConfig.Auth == "none" || SMTPRelayConfig.Auth == "false" {
SMTPRelayConfig.Auth = "none"
} else if SMTPRelayConfig.Auth == "plain" {
if SMTPRelayConfig.Username == "" || SMTPRelayConfig.Password == "" {
return fmt.Errorf("SMTP relay host username or password not set for PLAIN authentication (%s)", c)
}
} else if strings.HasPrefix(SMTPRelayConfig.Auth, "cram") {
SMTPRelayConfig.Auth = "cram-md5"
if SMTPRelayConfig.Username == "" || SMTPRelayConfig.Secret == "" {
return fmt.Errorf("SMTP relay host username or secret not set for CRAM-MD5 authentication (%s)", c)
}
} else {
return fmt.Errorf("SMTP relay authentication method not supported: %s", SMTPRelayConfig.Auth)
}

ReleaseEnabled = true

logger.Log().Infof("[smtp] enabling message relaying via %s:%d", SMTPRelayConfig.Host, SMTPRelayConfig.Port)

return nil
}

Expand Down
25 changes: 13 additions & 12 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@ require (
github.com/gorilla/websocket v1.5.0
github.com/jhillyerd/enmime v0.11.1
github.com/k3a/html2text v1.1.0
github.com/klauspost/compress v1.16.3
github.com/leporo/sqlf v1.3.0
github.com/klauspost/compress v1.16.5
github.com/leporo/sqlf v1.4.0
github.com/mattn/go-shellwords v1.0.12
github.com/mhale/smtpd v0.8.0
github.com/satori/go.uuid v1.2.0
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/tg123/go-htpasswd v1.2.1
golang.org/x/text v0.8.0
modernc.org/sqlite v1.21.1
golang.org/x/text v0.9.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.21.2
)

require (
Expand All @@ -43,17 +44,17 @@ require (
github.com/rivo/uniseg v0.4.4 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/image v0.6.0 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/tools v0.7.0 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/image v0.7.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/tools v0.8.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.22.3 // indirect
modernc.org/libc v1.22.4 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/opt v0.1.3 // indirect
Expand Down
Loading

0 comments on commit 176f128

Please sign in to comment.