mirror of
https://github.com/axllent/mailpit.git
synced 2026-03-03 03:57:01 +00:00
Feature: Optionally propagate SMTP errors (#588)
* forward smtp errors * lint and formatting * forward smtp errors in forward-impl
This commit is contained in:
@@ -6,6 +6,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/internal/auth"
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
@@ -16,7 +18,6 @@ import (
|
||||
"github.com/axllent/mailpit/internal/tools"
|
||||
"github.com/axllent/mailpit/server"
|
||||
"github.com/axllent/mailpit/server/webhook"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
@@ -332,6 +333,7 @@ func initConfigFromEnv() {
|
||||
config.SMTPRelayConfig.AllowedRecipients = os.Getenv("MP_SMTP_RELAY_ALLOWED_RECIPIENTS")
|
||||
config.SMTPRelayConfig.BlockedRecipients = os.Getenv("MP_SMTP_RELAY_BLOCKED_RECIPIENTS")
|
||||
config.SMTPRelayConfig.PreserveMessageIDs = getEnabledFromEnv("MP_SMTP_RELAY_PRESERVE_MESSAGE_IDS")
|
||||
config.SMTPRelayConfig.ForwardSMTPErrors = getEnabledFromEnv("MP_SMTP_RELAY_FWD_SMTP_ERRORS")
|
||||
|
||||
// SMTP forwarding
|
||||
config.SMTPForwardConfigFile = os.Getenv("MP_SMTP_FORWARD_CONFIG")
|
||||
@@ -350,6 +352,7 @@ func initConfigFromEnv() {
|
||||
config.SMTPForwardConfig.ReturnPath = os.Getenv("MP_SMTP_FORWARD_RETURN_PATH")
|
||||
config.SMTPForwardConfig.OverrideFrom = os.Getenv("MP_SMTP_FORWARD_OVERRIDE_FROM")
|
||||
config.SMTPForwardConfig.To = os.Getenv("MP_SMTP_FORWARD_TO")
|
||||
config.SMTPForwardConfig.ForwardSMTPErrors = getEnabledFromEnv("MP_SMTP_FORWARD_FWD_SMTP_ERRORS")
|
||||
|
||||
// Chaos
|
||||
chaos.Enabled = getEnabledFromEnv("MP_ENABLE_CHAOS")
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/axllent/ghru/v2"
|
||||
|
||||
"github.com/axllent/mailpit/internal/auth"
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/axllent/mailpit/internal/smtpd/chaos"
|
||||
@@ -252,6 +253,7 @@ type SMTPRelayConfigStruct struct {
|
||||
BlockedRecipients string `yaml:"blocked-recipients"` // regex, if set prevents relating to these addresses
|
||||
BlockedRecipientsRegexp *regexp.Regexp // compiled regexp using BlockedRecipients
|
||||
PreserveMessageIDs bool `yaml:"preserve-message-ids"` // preserve the original Message-ID when relaying
|
||||
ForwardSMTPErrors bool `yaml:"forward-smtp-errors"` // whether to log smtp-errors or forward them to upstream-client
|
||||
|
||||
// DEPRECATED 2024/03/12
|
||||
RecipientAllowlist string `yaml:"recipient-allowlist"`
|
||||
@@ -259,18 +261,19 @@ type SMTPRelayConfigStruct struct {
|
||||
|
||||
// SMTPForwardConfigStruct struct for parsing yaml & storing variables
|
||||
type SMTPForwardConfigStruct struct {
|
||||
To string `yaml:"to"` // comma-separated list of email addresses
|
||||
Host string `yaml:"host"` // SMTP host
|
||||
Port int `yaml:"port"` // SMTP port
|
||||
STARTTLS bool `yaml:"starttls"` // whether to use STARTTLS
|
||||
TLS bool `yaml:"tls"` // whether to use TLS
|
||||
AllowInsecure bool `yaml:"allow-insecure"` // allow insecure authentication, ignore TLS validation
|
||||
Auth string `yaml:"auth"` // none, plain, login, 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"` // allow overriding the bounce address
|
||||
OverrideFrom string `yaml:"override-from"` // allow overriding of the from address
|
||||
To string `yaml:"to"` // comma-separated list of email addresses
|
||||
Host string `yaml:"host"` // SMTP host
|
||||
Port int `yaml:"port"` // SMTP port
|
||||
STARTTLS bool `yaml:"starttls"` // whether to use STARTTLS
|
||||
TLS bool `yaml:"tls"` // whether to use TLS
|
||||
AllowInsecure bool `yaml:"allow-insecure"` // allow insecure authentication, ignore TLS validation
|
||||
Auth string `yaml:"auth"` // none, plain, login, 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"` // allow overriding the bounce address
|
||||
OverrideFrom string `yaml:"override-from"` // allow overriding of the from address
|
||||
ForwardSMTPErrors bool `yaml:"forward-smtp-errors"` // whether to log smtp-errors or forward them to upstream-client
|
||||
}
|
||||
|
||||
// VerifyConfig wil do some basic checking
|
||||
@@ -283,7 +286,8 @@ func VerifyConfig() error {
|
||||
// The default Content Security Policy is updates on every application page load to replace script-src 'self'
|
||||
// with a random nonce ID to prevent XSS. This applies to the Mailpit app & API.
|
||||
// See server.middleWareFunc()
|
||||
ContentSecurityPolicy = fmt.Sprintf("default-src 'self'; script-src 'self'; style-src %s 'unsafe-inline'; frame-src 'self'; img-src * data: blob:; font-src %s data:; media-src 'self'; connect-src 'self' ws: wss:; object-src 'none'; base-uri 'self';",
|
||||
ContentSecurityPolicy = fmt.Sprintf(
|
||||
"default-src 'self'; script-src 'self'; style-src %s 'unsafe-inline'; frame-src 'self'; img-src * data: blob:; font-src %s data:; media-src 'self'; connect-src 'self' ws: wss:; object-src 'none'; base-uri 'self';",
|
||||
cssFontRestriction, cssFontRestriction,
|
||||
)
|
||||
|
||||
@@ -615,8 +619,10 @@ func VerifyConfig() error {
|
||||
}
|
||||
|
||||
SMTPRelayMatchingRegexp = re
|
||||
logger.Log().Infof("[relay] auto-relaying new messages to recipients matching \"%s\" via %s:%d",
|
||||
SMTPRelayMatching, SMTPRelayConfig.Host, SMTPRelayConfig.Port)
|
||||
logger.Log().Infof(
|
||||
"[relay] auto-relaying new messages to recipients matching \"%s\" via %s:%d",
|
||||
SMTPRelayMatching, SMTPRelayConfig.Host, SMTPRelayConfig.Port,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
go.mod
2
go.mod
@@ -17,6 +17,7 @@ require (
|
||||
github.com/leporo/sqlf v1.4.0
|
||||
github.com/lithammer/shortuuid/v4 v4.2.0
|
||||
github.com/mneis/go-telnet v0.0.0-20221017141824-6f643e477c62
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/rqlite/gorqlite v0.0.0-20250609141355-ac86a4a1c9a8
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
@@ -58,7 +59,6 @@ require (
|
||||
github.com/olekukonko/errors v1.1.0 // indirect
|
||||
github.com/olekukonko/ll v0.1.2 // indirect
|
||||
github.com/olekukonko/tablewriter v1.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.2 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
|
||||
@@ -7,23 +7,29 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/axllent/mailpit/internal/tools"
|
||||
)
|
||||
|
||||
// Wrapper to forward messages if configured
|
||||
func autoForwardMessage(from string, data *[]byte) {
|
||||
func autoForwardMessage(from string, data *[]byte) error {
|
||||
if config.SMTPForwardConfig.Host == "" {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := forward(from, *data); err != nil {
|
||||
logger.Log().Errorf("[forward] error: %s", err.Error())
|
||||
} else {
|
||||
logger.Log().Debugf("[forward] message from %s to %s via %s:%d",
|
||||
from, config.SMTPForwardConfig.To, config.SMTPForwardConfig.Host, config.SMTPForwardConfig.Port)
|
||||
return errors.WithMessage(err, "[forward] error: %s")
|
||||
}
|
||||
|
||||
logger.Log().Debugf(
|
||||
"[forward] message from %s to %s via %s:%d",
|
||||
from, config.SMTPForwardConfig.To, config.SMTPForwardConfig.Host, config.SMTPForwardConfig.Port,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createForwardingSMTPClient(config config.SMTPForwardConfigStruct, addr string) (*smtp.Client, error) {
|
||||
@@ -108,6 +114,9 @@ func forward(from string, msg []byte) error {
|
||||
for _, addr := range to {
|
||||
if err = c.Rcpt(addr); err != nil {
|
||||
logger.Log().Warnf("error response to RCPT command for %s: %s", addr, err.Error())
|
||||
if config.SMTPForwardConfig.ForwardSMTPErrors {
|
||||
return errors.WithMessagef(err, "error response to RCPT command for %s", addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/lithammer/shortuuid/v4"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/internal/auth"
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
@@ -16,7 +19,6 @@ import (
|
||||
"github.com/axllent/mailpit/internal/storage"
|
||||
"github.com/axllent/mailpit/internal/tools"
|
||||
"github.com/axllent/mailpit/server/websockets"
|
||||
"github.com/lithammer/shortuuid/v4"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -73,10 +75,36 @@ func SaveToDatabase(origin net.Addr, from string, to []string, data []byte, smtp
|
||||
}
|
||||
|
||||
// if enabled, this may conditionally relay the email through to the preconfigured smtp server
|
||||
autoRelayMessage(from, to, &data)
|
||||
if relayErr := autoRelayMessage(from, to, &data); relayErr != nil {
|
||||
logger.Log().Errorf("%s", relayErr.Error())
|
||||
|
||||
if config.SMTPRelayConfig.ForwardSMTPErrors {
|
||||
for {
|
||||
unwrappedErr := errors.Unwrap(relayErr)
|
||||
if unwrappedErr == nil {
|
||||
break
|
||||
}
|
||||
relayErr = unwrappedErr
|
||||
}
|
||||
return "", relayErr
|
||||
}
|
||||
}
|
||||
|
||||
// if enabled, this will forward a copy to preconfigured addresses
|
||||
autoForwardMessage(from, &data)
|
||||
if forwardErr := autoForwardMessage(from, &data); forwardErr != nil {
|
||||
logger.Log().Errorf("%s", forwardErr.Error())
|
||||
|
||||
if config.SMTPForwardConfig.ForwardSMTPErrors {
|
||||
for {
|
||||
unwrappedErr := errors.Unwrap(forwardErr)
|
||||
if unwrappedErr == nil {
|
||||
break
|
||||
}
|
||||
forwardErr = unwrappedErr
|
||||
}
|
||||
return "", forwardErr
|
||||
}
|
||||
}
|
||||
|
||||
// build array of all addresses in the header to compare to the []to array
|
||||
emails, hasBccHeader := scanAddressesInHeader(msg.Header)
|
||||
@@ -225,15 +253,27 @@ func listenAndServe(addr string, handler MsgIDHandler, authHandler AuthHandler)
|
||||
}
|
||||
|
||||
if config.SMTPAuthAllowInsecure {
|
||||
srv.AuthMechs = map[string]bool{"CRAM-MD5": false, "PLAIN": true, "LOGIN": true}
|
||||
srv.AuthMechs = map[string]bool{
|
||||
"CRAM-MD5": false,
|
||||
"PLAIN": true,
|
||||
"LOGIN": true,
|
||||
}
|
||||
}
|
||||
|
||||
if auth.SMTPCredentials != nil {
|
||||
srv.AuthMechs = map[string]bool{"CRAM-MD5": false, "PLAIN": true, "LOGIN": true}
|
||||
srv.AuthMechs = map[string]bool{
|
||||
"CRAM-MD5": false,
|
||||
"PLAIN": true,
|
||||
"LOGIN": true,
|
||||
}
|
||||
srv.AuthHandler = authHandler
|
||||
srv.AuthRequired = true
|
||||
} else if config.SMTPAuthAcceptAny {
|
||||
srv.AuthMechs = map[string]bool{"CRAM-MD5": false, "PLAIN": true, "LOGIN": true}
|
||||
srv.AuthMechs = map[string]bool{
|
||||
"CRAM-MD5": false,
|
||||
"PLAIN": true,
|
||||
"LOGIN": true,
|
||||
}
|
||||
srv.AuthHandler = authHandlerAny
|
||||
}
|
||||
|
||||
|
||||
@@ -2,19 +2,20 @@ package smtpd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/axllent/mailpit/internal/tools"
|
||||
)
|
||||
|
||||
// Wrapper to auto relay messages if configured
|
||||
func autoRelayMessage(from string, to []string, data *[]byte) {
|
||||
func autoRelayMessage(from string, to []string, data *[]byte) error {
|
||||
if config.SMTPRelayConfig.BlockedRecipientsRegexp != nil {
|
||||
filteredTo := []string{}
|
||||
for _, address := range to {
|
||||
@@ -29,15 +30,17 @@ func autoRelayMessage(from string, to []string, data *[]byte) {
|
||||
}
|
||||
|
||||
if len(to) == 0 {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if config.SMTPRelayAll {
|
||||
if err := Relay(from, to, *data); err != nil {
|
||||
logger.Log().Errorf("[relay] error: %s", err.Error())
|
||||
return errors.WithMessage(err, "[relay] error")
|
||||
} else {
|
||||
logger.Log().Debugf("[relay] sent message to %s from %s via %s:%d",
|
||||
strings.Join(to, ", "), from, config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port)
|
||||
logger.Log().Debugf(
|
||||
"[relay] sent message to %s from %s via %s:%d",
|
||||
strings.Join(to, ", "), from, config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port,
|
||||
)
|
||||
}
|
||||
} else if config.SMTPRelayMatchingRegexp != nil {
|
||||
filtered := []string{}
|
||||
@@ -48,16 +51,20 @@ func autoRelayMessage(from string, to []string, data *[]byte) {
|
||||
}
|
||||
|
||||
if len(filtered) == 0 {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := Relay(from, filtered, *data); err != nil {
|
||||
logger.Log().Errorf("[relay] error: %s", err.Error())
|
||||
return errors.WithMessage(err, "[relay] error")
|
||||
} else {
|
||||
logger.Log().Debugf("[relay] auto-relay message to %s from %s via %s:%d",
|
||||
strings.Join(filtered, ", "), from, config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port)
|
||||
logger.Log().Debugf(
|
||||
"[relay] auto-relay message to %s from %s via %s:%d",
|
||||
strings.Join(filtered, ", "), from, config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createRelaySMTPClient(config config.SMTPRelayConfigStruct, addr string) (*smtp.Client, error) {
|
||||
@@ -134,26 +141,29 @@ func Relay(from string, to []string, msg []byte) error {
|
||||
}
|
||||
|
||||
if err = c.Mail(from); err != nil {
|
||||
return fmt.Errorf("error response to MAIL command: %s", err.Error())
|
||||
return errors.WithMessage(err, "error sending MAIL command")
|
||||
}
|
||||
|
||||
for _, addr := range to {
|
||||
if err = c.Rcpt(addr); err != nil {
|
||||
logger.Log().Warnf("error response to RCPT command for %s: %s", addr, err.Error())
|
||||
if config.SMTPRelayConfig.ForwardSMTPErrors {
|
||||
return errors.WithMessagef(err, "error response to RCPT command for %s", addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error response to DATA command: %s", err.Error())
|
||||
return errors.WithMessage(err, "error response to DATA command")
|
||||
}
|
||||
|
||||
if _, err := w.Write(msg); err != nil {
|
||||
return fmt.Errorf("error sending message: %s", err.Error())
|
||||
return errors.WithMessage(err, "error sending message")
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return fmt.Errorf("error closing connection: %s", err.Error())
|
||||
return errors.WithMessage(err, "error closing connection")
|
||||
}
|
||||
|
||||
return c.Quit()
|
||||
@@ -186,7 +196,10 @@ type loginAuth struct {
|
||||
|
||||
// LoginAuth authentication
|
||||
func LoginAuth(username, password string) smtp.Auth {
|
||||
return &loginAuth{username, password}
|
||||
return &loginAuth{
|
||||
username,
|
||||
password,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *loginAuth) Start(_ *smtp.ServerInfo) (string, []byte, error) {
|
||||
|
||||
Reference in New Issue
Block a user