mirror of
https://github.com/axllent/mailpit.git
synced 2025-03-19 21:28:07 +02:00
Merge branch 'feature/message-release' into develop
This commit is contained in:
commit
584d94b8e7
19
cmd/root.go
19
cmd/root.go
@ -98,8 +98,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")
|
||||
@ -179,6 +182,14 @@ 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
|
||||
}
|
||||
|
||||
if len(os.Getenv("MP_WEBROOT")) > 0 {
|
||||
config.Webroot = os.Getenv("MP_WEBROOT")
|
||||
}
|
||||
@ -189,10 +200,10 @@ func initConfigFromEnv() {
|
||||
config.UseMessageDates = true
|
||||
}
|
||||
if getEnabledFromEnv("MP_QUIET") {
|
||||
config.QuietLogging = true
|
||||
logger.QuietLogging = true
|
||||
}
|
||||
if getEnabledFromEnv("MP_VERBOSE") {
|
||||
config.VerboseLogging = true
|
||||
logger.VerboseLogging = true
|
||||
}
|
||||
}
|
||||
|
||||
|
118
config/config.go
118
config/config.go
@ -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 (
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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';"
|
||||
@ -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) {
|
||||
@ -167,7 +186,7 @@ func VerifyConfig() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
SMTPAuth = a
|
||||
SMTPAuthConfig = a
|
||||
}
|
||||
|
||||
if SMTPTLSCert == "" && (SMTPAuthFile != "" || SMTPAuthAcceptAny) && !SMTPAuthAllowInsecure {
|
||||
@ -182,7 +201,7 @@ func VerifyConfig() error {
|
||||
s := strings.TrimRight(path.Join("/", Webroot, "/"), "/") + "/"
|
||||
Webroot = s
|
||||
|
||||
SMTPTags = []Tag{}
|
||||
SMTPTags = []AutoTag{}
|
||||
|
||||
p := shellwords.NewParser()
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
25
go.mod
25
go.mod
@ -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 (
|
||||
@ -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
|
||||
|
55
go.sum
55
go.sum
@ -53,7 +53,6 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
|
||||
@ -66,8 +65,8 @@ github.com/k3a/html2text v1.1.0 h1:ks4hKSTdiTRsLr0DM771mI5TvsoG6zH7m1Ulv7eJRHw=
|
||||
github.com/k3a/html2text v1.1.0/go.mod h1:ieEXykM67iT8lTvEWBh6fhpH4B23kB9OMKPdIBmgUqA=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY=
|
||||
github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
|
||||
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
@ -75,8 +74,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leporo/sqlf v1.3.0 h1:nAkuPYxMIJg/sUmcd1h4avV5iYo8tBTGEGOIR4BIZO8=
|
||||
github.com/leporo/sqlf v1.3.0/go.mod h1:f4dHqIi1+nLl6k1IsNQ8QIEbGWK49th2ei1IxTXk+2E=
|
||||
github.com/leporo/sqlf v1.4.0 h1:SyWnX/8GSGOzVmanG0Ub1c04mR9nNl6Tq3IeFKX2/4c=
|
||||
github.com/leporo/sqlf v1.4.0/go.mod h1:pgN9yKsAnQ+2ewhbZogr98RcasUjPsHF3oXwPPhHvBw=
|
||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
@ -84,8 +83,8 @@ github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWV
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mhale/smtpd v0.8.0 h1:5JvdsehCg33PQrZBvFyDMMUDQmvbzVpZgKob7eYBJc0=
|
||||
github.com/mhale/smtpd v0.8.0/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
@ -111,15 +110,20 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykE
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/tg123/go-htpasswd v1.2.1 h1:i4wfsX1KvvkyoMiHZzjS0VzbAPWfxzI8INcZAKtutoU=
|
||||
github.com/tg123/go-htpasswd v1.2.1/go.mod h1:erHp1B86KXdwQf1X5ZrLb7erXZnWueEQezb2dql4q58=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
@ -127,22 +131,22 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
|
||||
golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
|
||||
golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
|
||||
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
|
||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
@ -154,8 +158,9 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@ -163,15 +168,15 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
|
||||
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
||||
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
|
||||
golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@ -189,16 +194,16 @@ modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
|
||||
modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
|
||||
modernc.org/libc v1.22.4 h1:wymSbZb0AlrjdAVX3cjreCHTPCpPARbQXNz6BHPzdwQ=
|
||||
modernc.org/libc v1.22.4/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.21.1 h1:GyDFqNnESLOhwwDRaHGdp2jKLDzpyT/rNLglX3ZkMSU=
|
||||
modernc.org/sqlite v1.21.1/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
|
||||
modernc.org/sqlite v1.21.2 h1:ixuUG0QS413Vfzyx6FWx6PYTmHaOegTY+hjzhn7L+a0=
|
||||
modernc.org/sqlite v1.21.2/go.mod h1:cxbLkB5WS32DnQqeH4h4o1B0eMr8W/y8/RGuxQ3JsC0=
|
||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/tcl v1.15.1 h1:mOQwiEK4p7HruMZcwKTZPw/aqtGM4aY00uzWhlKKYws=
|
||||
|
2422
package-lock.json
generated
2422
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -22,6 +22,8 @@ import (
|
||||
var (
|
||||
// Verbose flag
|
||||
Verbose bool
|
||||
|
||||
fromAddr string
|
||||
)
|
||||
|
||||
// Run the Mailpit sendmail replacement.
|
||||
@ -37,7 +39,10 @@ func Run() {
|
||||
username = user.Username
|
||||
}
|
||||
|
||||
fromAddr := username + "@" + host
|
||||
if fromAddr == "" {
|
||||
fromAddr = username + "@" + host
|
||||
}
|
||||
|
||||
smtpAddr := "localhost:1025"
|
||||
var recip []string
|
||||
|
||||
@ -50,7 +55,7 @@ func Run() {
|
||||
}
|
||||
|
||||
// override defaults from cli flags
|
||||
flag.StringVarP(&fromAddr, "from", "f", fromAddr, "SMTP sender")
|
||||
flag.StringVarP(&fromAddr, "from", "f", fromAddr, "SMTP sender address")
|
||||
flag.StringVar(&smtpAddr, "smtp-addr", smtpAddr, "SMTP server address")
|
||||
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.")
|
||||
@ -74,7 +79,7 @@ func Run() {
|
||||
recip = flag.Args()
|
||||
|
||||
if Verbose {
|
||||
fmt.Fprintln(os.Stderr, smtpAddr, fromAddr)
|
||||
fmt.Fprintln(os.Stdout, smtpAddr, fromAddr)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(os.Stdin)
|
||||
|
@ -10,8 +10,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/server/smtpd"
|
||||
"github.com/axllent/mailpit/storage"
|
||||
"github.com/axllent/mailpit/utils/logger"
|
||||
"github.com/axllent/mailpit/utils/tools"
|
||||
"github.com/gorilla/mux"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
// GetMessages returns a paginated list of messages as JSON
|
||||
@ -491,6 +495,121 @@ func SetTags(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
// ReleaseMessage (method: POST) will release a message via a preconfigured external SMTP server.
|
||||
// If no IDs are provided then all messages are updated.
|
||||
func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:route POST /api/v1/message/{ID}/release message Release
|
||||
//
|
||||
// # Release message
|
||||
//
|
||||
// Release a message via a preconfigured external SMTP server..
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
//
|
||||
// Produces:
|
||||
// - text/plain
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
// Parameters:
|
||||
// + name: ID
|
||||
// in: path
|
||||
// description: message id
|
||||
// required: true
|
||||
// type: string
|
||||
// + name: To
|
||||
// in: body
|
||||
// description: Array of email addresses to release message to
|
||||
// required: true
|
||||
// type: ReleaseMessageRequest
|
||||
//
|
||||
// Responses:
|
||||
// 200: OKResponse
|
||||
// default: ErrorResponse
|
||||
|
||||
vars := mux.Vars(r)
|
||||
|
||||
id := vars["id"]
|
||||
|
||||
msg, err := storage.GetMessageRaw(id)
|
||||
if err != nil {
|
||||
fourOFour(w)
|
||||
return
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
|
||||
data := releaseMessageRequest{}
|
||||
|
||||
if err := decoder.Decode(&data); err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tos := data.To
|
||||
if len(tos) == 0 {
|
||||
httpError(w, "No valid addresses found")
|
||||
return
|
||||
}
|
||||
|
||||
for _, to := range tos {
|
||||
if _, err := mail.ParseAddress(to); err != nil {
|
||||
httpError(w, "Invalid email address: "+to)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
reader := bytes.NewReader(msg)
|
||||
m, err := mail.ReadMessage(reader)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
froms, err := m.Header.AddressList("From")
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
from := froms[0].Address
|
||||
|
||||
// if sender is used, then change from to the sender
|
||||
if senders, err := m.Header.AddressList("Sender"); err == nil {
|
||||
from = senders[0].Address
|
||||
}
|
||||
|
||||
msg, err = tools.RemoveMessageHeaders(msg, []string{"Bcc", "Message-Id"})
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if config.SMTPRelayConfig.ReturnPath != "" && m.Header.Get("Return-Path") != "<"+config.SMTPRelayConfig.ReturnPath+">" {
|
||||
msg, err = tools.RemoveMessageHeaders(msg, []string{"Return-Path"})
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
msg = append([]byte("Return-Path: <"+config.SMTPRelayConfig.ReturnPath+">\r\n"), msg...)
|
||||
}
|
||||
|
||||
// generate unique ID
|
||||
uid := uuid.NewV4().String() + "@mailpit"
|
||||
// add unique ID
|
||||
msg = append([]byte("Message-Id: <"+uid+">\r\n"), msg...)
|
||||
|
||||
if err := smtpd.Send(from, tos, msg); err != nil {
|
||||
logger.Log().Errorf("[smtp] error sending message: %s", err.Error())
|
||||
httpError(w, "SMTP error: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
// FourOFour returns a basic 404 message
|
||||
func fourOFour(w http.ResponseWriter) {
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
"github.com/axllent/mailpit/utils/updater"
|
||||
)
|
||||
|
||||
// Response includes the current and latest Mailpit versions, database info, and memory usage
|
||||
// Response includes the current and latest Mailpit version, database info, and memory usage
|
||||
//
|
||||
// swagger:model AppInformation
|
||||
type appInformation struct {
|
||||
@ -33,12 +33,12 @@ type appInformation struct {
|
||||
func AppInfo(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:route GET /api/v1/info application AppInformation
|
||||
//
|
||||
// # Get the application information
|
||||
// # Get application information
|
||||
//
|
||||
// Returns basic runtime information, message totals and latest release version.
|
||||
//
|
||||
// Produces:
|
||||
// - application/octet-stream
|
||||
// - application/json
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
|
@ -9,6 +9,13 @@ type infoResponse struct {
|
||||
Body appInformation
|
||||
}
|
||||
|
||||
// Web UI configuration
|
||||
// swagger:response WebUIConfigurationResponse
|
||||
type webUIConfigurationResponse struct {
|
||||
// Web UI configuration settings
|
||||
Body webUIConfiguration
|
||||
}
|
||||
|
||||
// Message summary
|
||||
// swagger:response MessagesSummaryResponse
|
||||
type messagesSummaryResponse struct {
|
||||
@ -47,11 +54,19 @@ type setTagsRequest struct {
|
||||
// in:body
|
||||
Tags []string `json:"tags"`
|
||||
|
||||
// ids
|
||||
// IDs
|
||||
// in:body
|
||||
IDs []string `json:"ids"`
|
||||
}
|
||||
|
||||
// Release request
|
||||
// swagger:model ReleaseMessageRequest
|
||||
type releaseMessageRequest struct {
|
||||
// To
|
||||
// in:body
|
||||
To []string `json:"to"`
|
||||
}
|
||||
|
||||
// Binary data reponse inherits the attachment's content type
|
||||
// swagger:response BinaryResponse
|
||||
type binaryResponse struct {
|
||||
|
54
server/apiv1/webui.go
Normal file
54
server/apiv1/webui.go
Normal file
@ -0,0 +1,54 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
)
|
||||
|
||||
// Response includes global web UI settings
|
||||
//
|
||||
// swagger:model WebUIConfiguration
|
||||
type webUIConfiguration struct {
|
||||
// Message Relay information
|
||||
MessageRelay struct {
|
||||
// Whether message relaying (release) is enabled
|
||||
Enabled bool
|
||||
// The configured SMTP server address
|
||||
SMTPServer string
|
||||
// Enforced Return-Path (if set) for relay bounces
|
||||
ReturnPath string
|
||||
}
|
||||
}
|
||||
|
||||
// WebUIConfig returns configuration settings for the web UI.
|
||||
func WebUIConfig(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:route GET /api/v1/webui application WebUIConfiguration
|
||||
//
|
||||
// # Get web UI configuration
|
||||
//
|
||||
// Returns configuration settings for the web UI.
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
// Responses:
|
||||
// 200: WebUIConfigurationResponse
|
||||
// default: ErrorResponse
|
||||
conf := webUIConfiguration{}
|
||||
|
||||
conf.MessageRelay.Enabled = config.ReleaseEnabled
|
||||
if config.ReleaseEnabled {
|
||||
conf.MessageRelay.SMTPServer = fmt.Sprintf("%s:%d", config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port)
|
||||
conf.MessageRelay.ReturnPath = config.SMTPRelayConfig.ReturnPath
|
||||
}
|
||||
|
||||
bytes, _ := json.Marshal(conf)
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
_, _ = w.Write(bytes)
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
// Package server is the HTTP daemon
|
||||
package server
|
||||
|
||||
import (
|
||||
@ -87,6 +88,7 @@ func defaultRoutes() *mux.Router {
|
||||
r.HandleFunc(config.Webroot+"api/v1/message/{id}/headers", middleWareFunc(apiv1.GetHeaders)).Methods("GET")
|
||||
r.HandleFunc(config.Webroot+"api/v1/message/{id}", middleWareFunc(apiv1.GetMessage)).Methods("GET")
|
||||
r.HandleFunc(config.Webroot+"api/v1/info", middleWareFunc(apiv1.AppInfo)).Methods("GET")
|
||||
r.HandleFunc(config.Webroot+"api/v1/webui", middleWareFunc(apiv1.WebUIConfig)).Methods("GET")
|
||||
|
||||
return r
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/server/apiv1"
|
||||
"github.com/axllent/mailpit/storage"
|
||||
"github.com/axllent/mailpit/utils/logger"
|
||||
"github.com/jhillyerd/enmime"
|
||||
)
|
||||
|
||||
@ -150,7 +151,7 @@ func Test_APIv1(t *testing.T) {
|
||||
}
|
||||
|
||||
func setup() {
|
||||
config.NoLogging = true
|
||||
logger.NoLogging = true
|
||||
config.MaxMessages = 0
|
||||
config.DataFile = ""
|
||||
|
||||
|
71
server/smtpd/smtp.go
Normal file
71
server/smtpd/smtp.go
Normal file
@ -0,0 +1,71 @@
|
||||
package smtpd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
)
|
||||
|
||||
// Send will connect to a pre-configured SMTP server and send a message to one or more recipients.
|
||||
func Send(from string, to []string, msg []byte) error {
|
||||
addr := fmt.Sprintf("%s:%d", config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port)
|
||||
|
||||
c, err := smtp.Dial(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer c.Close()
|
||||
|
||||
if config.SMTPRelayConfig.STARTTLS {
|
||||
conf := &tls.Config{ServerName: config.SMTPRelayConfig.Host}
|
||||
|
||||
conf.InsecureSkipVerify = config.SMTPRelayConfig.AllowInsecure
|
||||
|
||||
if err = c.StartTLS(conf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var a smtp.Auth
|
||||
|
||||
if config.SMTPRelayConfig.Auth == "plain" {
|
||||
a = smtp.PlainAuth("", config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Password, config.SMTPRelayConfig.Host)
|
||||
}
|
||||
|
||||
if config.SMTPRelayConfig.Auth == "cram-md5" {
|
||||
a = smtp.CRAMMD5Auth(config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Secret)
|
||||
}
|
||||
|
||||
if a != nil {
|
||||
if err = c.Auth(a); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = c.Mail(from); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, addr := range to {
|
||||
if err = c.Rcpt(addr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := w.Write(msg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Quit()
|
||||
}
|
@ -13,16 +13,34 @@ import (
|
||||
"github.com/axllent/mailpit/storage"
|
||||
"github.com/axllent/mailpit/utils/logger"
|
||||
"github.com/mhale/smtpd"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
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("[smtp] error parsing message: %s", err.Error())
|
||||
logger.Log().Errorf("[smtpd] error parsing message: %s", err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// add a message ID if not set
|
||||
if msg.Header.Get("Message-Id") == "" {
|
||||
// generate unique ID
|
||||
uid := uuid.NewV4().String() + "@mailpit"
|
||||
// add unique ID
|
||||
data = append([]byte("Message-Id: <"+uid+">\r\n"), data...)
|
||||
}
|
||||
|
||||
// if enabled, this will route the email 1:1 through to the preconfigured smtp server
|
||||
if config.SMTPRelayAllIncoming {
|
||||
if err := Send(from, to, data); err != nil {
|
||||
logger.Log().Errorf("[smtp] error relaying message: %s", err.Error())
|
||||
} else {
|
||||
logger.Log().Debugf("[smtp] relayed message from %s via %s:%d", from, config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port)
|
||||
}
|
||||
}
|
||||
|
||||
// build array of all addresses in the header to compare to the []to array
|
||||
emails, hasBccHeader := scanAddressesInHeader(msg.Header)
|
||||
|
||||
@ -35,7 +53,7 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
|
||||
missingAddresses = append(missingAddresses, a)
|
||||
}
|
||||
} else {
|
||||
logger.Log().Warnf("[smtp] ignoring invalid email address: %s", a)
|
||||
logger.Log().Warnf("[smtpd] ignoring invalid email address: %s", a)
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +78,7 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
|
||||
data = append(bcc, data...)
|
||||
}
|
||||
|
||||
logger.Log().Debugf("[smtp] added missing addresses to Bcc header: %s", strings.Join(missingAddresses, ", "))
|
||||
logger.Log().Debugf("[smtpd] added missing addresses to Bcc header: %s", strings.Join(missingAddresses, ", "))
|
||||
}
|
||||
|
||||
if _, err := storage.Store(data); err != nil {
|
||||
@ -70,17 +88,17 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
|
||||
}
|
||||
|
||||
subject := msg.Header.Get("Subject")
|
||||
logger.Log().Debugf("[smtp] received (%s) from:%s to:%s subject:%q", cleanIP(origin), from, to[0], subject)
|
||||
logger.Log().Debugf("[smtpd] received (%s) from:%s subject:%q", cleanIP(origin), from, subject)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func authHandler(remoteAddr net.Addr, mechanism string, username []byte, password []byte, shared []byte) (bool, error) {
|
||||
allow := config.SMTPAuth.Match(string(username), string(password))
|
||||
allow := config.SMTPAuthConfig.Match(string(username), string(password))
|
||||
if allow {
|
||||
logger.Log().Debugf("[smtp] allow %s login:%q from:%s", mechanism, string(username), cleanIP(remoteAddr))
|
||||
logger.Log().Debugf("[smtpd] allow %s login:%q from:%s", mechanism, string(username), cleanIP(remoteAddr))
|
||||
} else {
|
||||
logger.Log().Warnf("[smtp] deny %s login:%q from:%s", mechanism, string(username), cleanIP(remoteAddr))
|
||||
logger.Log().Warnf("[smtpd] deny %s login:%q from:%s", mechanism, string(username), cleanIP(remoteAddr))
|
||||
}
|
||||
|
||||
return allow, nil
|
||||
@ -88,7 +106,7 @@ func authHandler(remoteAddr net.Addr, mechanism string, username []byte, passwor
|
||||
|
||||
// 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))
|
||||
logger.Log().Debugf("[smtpd] allow %s login %q from %s", mechanism, string(username), cleanIP(remoteAddr))
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@ -97,19 +115,19 @@ func authHandlerAny(remoteAddr net.Addr, mechanism string, username []byte, pass
|
||||
func Listen() error {
|
||||
if config.SMTPAuthAllowInsecure {
|
||||
if config.SMTPAuthFile != "" {
|
||||
logger.Log().Infof("[smtp] enabling login auth via %s (insecure)", config.SMTPAuthFile)
|
||||
logger.Log().Infof("[smtpd] enabling login auth via %s (insecure)", config.SMTPAuthFile)
|
||||
} else if config.SMTPAuthAcceptAny {
|
||||
logger.Log().Info("[smtp] enabling all auth (insecure)")
|
||||
logger.Log().Info("[smtpd] enabling all auth (insecure)")
|
||||
}
|
||||
} else {
|
||||
if config.SMTPAuthFile != "" {
|
||||
logger.Log().Infof("[smtp] enabling login auth via %s (TLS)", config.SMTPAuthFile)
|
||||
logger.Log().Infof("[smtpd] enabling login auth via %s (TLS)", config.SMTPAuthFile)
|
||||
} else if config.SMTPAuthAcceptAny {
|
||||
logger.Log().Info("[smtp] enabling any auth (TLS)")
|
||||
logger.Log().Info("[smtpd] enabling any auth (TLS)")
|
||||
}
|
||||
}
|
||||
|
||||
logger.Log().Infof("[smtp] starting on %s", logger.CleanIP(config.SMTPListen))
|
||||
logger.Log().Infof("[smtpd] starting on %s", logger.CleanIP(config.SMTPListen))
|
||||
|
||||
return listenAndServe(config.SMTPListen, mailHandler, authHandler)
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
import commonMixins from './mixins.js';
|
||||
import Message from './templates/Message.vue';
|
||||
import MessageSummary from './templates/MessageSummary.vue';
|
||||
import MessageRelease from './templates/MessageRelease.vue';
|
||||
import moment from 'moment';
|
||||
import Tinycon from 'tinycon';
|
||||
|
||||
@ -10,7 +11,8 @@ export default {
|
||||
|
||||
components: {
|
||||
Message,
|
||||
MessageSummary
|
||||
MessageSummary,
|
||||
MessageRelease
|
||||
},
|
||||
|
||||
data() {
|
||||
@ -36,7 +38,9 @@ export default {
|
||||
selected: [],
|
||||
tcStatus: 0,
|
||||
appInfo: false,
|
||||
lastLoaded: false
|
||||
lastLoaded: false,
|
||||
relayConfig: {},
|
||||
releaseAddresses: false,
|
||||
}
|
||||
},
|
||||
|
||||
@ -87,6 +91,7 @@ export default {
|
||||
});
|
||||
|
||||
this.connect();
|
||||
this.getUISettings();
|
||||
this.loadMessages();
|
||||
},
|
||||
|
||||
@ -147,6 +152,13 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
getUISettings: function () {
|
||||
let self = this;
|
||||
self.get('api/v1/webui', null, function (response) {
|
||||
self.relayConfig = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
doSearch: function (e) {
|
||||
e.preventDefault();
|
||||
this.loadMessages();
|
||||
@ -192,6 +204,7 @@ export default {
|
||||
openMessage: function (id) {
|
||||
let self = this;
|
||||
self.selected = [];
|
||||
self.releaseAddresses = false;
|
||||
self.existingTags = JSON.parse(JSON.stringify(self.tags));
|
||||
|
||||
let uri = 'api/v1/message/' + self.currentPath
|
||||
@ -550,6 +563,34 @@ export default {
|
||||
dl.target = '_blank';
|
||||
dl.download = this.message.ID + '.' + ext;
|
||||
dl.click();
|
||||
},
|
||||
|
||||
initReleaseModal: function () {
|
||||
this.releaseAddresses = false;
|
||||
let addresses = [];
|
||||
for (let i in this.message.To) {
|
||||
addresses.push(this.message.To[i].Address)
|
||||
}
|
||||
for (let i in this.message.Cc) {
|
||||
addresses.push(this.message.Cc[i].Address)
|
||||
}
|
||||
for (let i in this.message.Bcc) {
|
||||
addresses.push(this.message.Bcc[i].Address)
|
||||
}
|
||||
|
||||
// include only unique email addresses, regardless of casing
|
||||
let uAddresses = new Map(addresses.map(a => [a.toLowerCase(), a]));
|
||||
this.releaseAddresses = [...uAddresses.values()];
|
||||
|
||||
let self = this;
|
||||
window.setTimeout(function () {
|
||||
// delay to allow elements to load
|
||||
self.modal('ReleaseModal').show();
|
||||
|
||||
window.setTimeout(function () {
|
||||
document.querySelector('#ReleaseModal input[role="combobox"]').focus()
|
||||
}, 500);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -572,6 +613,10 @@ export default {
|
||||
<button class="btn btn-outline-light me-2" title="Mark unread" v-on:click="markUnread">
|
||||
<i class="bi bi-eye-slash"></i> <span class="d-none d-md-inline">Mark unread</span>
|
||||
</button>
|
||||
<button class="btn btn-outline-light me-2" title="Release message"
|
||||
v-if="relayConfig.MessageRelay && relayConfig.MessageRelay.Enabled" v-on:click="initReleaseModal">
|
||||
<i class="bi bi-send"></i> <span class="d-none d-md-inline">Release</span>
|
||||
</button>
|
||||
<button class="btn btn-outline-light me-2" title="Delete message" v-on:click="deleteMessages">
|
||||
<i class="bi bi-trash-fill"></i> <span class="d-none d-md-inline">Delete</span>
|
||||
</button>
|
||||
@ -963,4 +1008,10 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="ReleaseModal" tabindex="-1" aria-labelledby="AppInfoModalLabel" aria-hidden="true">
|
||||
<MessageRelease v-if="releaseAddresses" :message="message" :relayConfig="relayConfig"
|
||||
:releaseAddresses="releaseAddresses"></MessageRelease>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -251,6 +251,10 @@ body.blur {
|
||||
input {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
div {
|
||||
cursor: text; // html5-tags
|
||||
}
|
||||
}
|
||||
|
||||
#DownloadBtn {
|
||||
@ -264,6 +268,14 @@ body.blur {
|
||||
}
|
||||
}
|
||||
|
||||
#ReleaseModal {
|
||||
.form-control.dropdown {
|
||||
div {
|
||||
@extend .form-control;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* PrismJS 1.29.0 - modified!
|
||||
https://prismjs.com/download.html#themes=prism-coy&languages=markup+css */
|
||||
code[class*="language-"],
|
||||
|
@ -211,7 +211,8 @@ export default {
|
||||
<td class="privacy">
|
||||
<span v-for="(t, i) in message.Bcc">
|
||||
<template v-if="i > 0">,</template>
|
||||
{{ t.Name + " <" + t.Address + ">" }} </span>
|
||||
{{ t.Name + " <" + t.Address + ">" }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="message.ReplyTo && message.ReplyTo.length" class="small">
|
||||
@ -222,9 +223,16 @@ export default {
|
||||
{{ t.Name + " <" + t.Address + ">" }} </span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="message.ReturnPath && message.ReturnPath != message.From.Address" class="small">
|
||||
<th class="text-nowrap">Return-Path</th>
|
||||
<td class="privacy text-muted"><{{ message.ReturnPath }}></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="small">Subject</th>
|
||||
<td><strong>{{ message.Subject }}</strong></td>
|
||||
<td>
|
||||
<strong v-if="message.Subject != ''">{{ message.Subject }}</strong>
|
||||
<small class="text-muted" v-else>[ no subject ]</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="d-md-none small">
|
||||
<th class="small">Date</th>
|
||||
@ -262,7 +270,7 @@ export default {
|
||||
<div class="nav nav-tabs my-3" id="nav-tab" role="tablist">
|
||||
<button class="nav-link" id="nav-html-tab" data-bs-toggle="tab" data-bs-target="#nav-html" type="button"
|
||||
role="tab" aria-controls="nav-html" aria-selected="true" v-if="message.HTML"
|
||||
v-on:click="showMobileBtns = true">HTML</button>
|
||||
v-on:click="showMobileBtns = true; resizeIframes()">HTML</button>
|
||||
<button class="nav-link" id="nav-html-source-tab" data-bs-toggle="tab" data-bs-target="#nav-html-source"
|
||||
type="button" role="tab" aria-controls="nav-html-source" aria-selected="false" v-if="message.HTML"
|
||||
v-on:click="showMobileBtns = false">
|
||||
|
109
server/ui-src/templates/MessageRelease.vue
Normal file
109
server/ui-src/templates/MessageRelease.vue
Normal file
@ -0,0 +1,109 @@
|
||||
|
||||
<script>
|
||||
import Tags from "bootstrap5-tags";
|
||||
import commonMixins from '../mixins.js';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
message: Object,
|
||||
relayConfig: Object,
|
||||
releaseAddresses: Array
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
addresses: []
|
||||
}
|
||||
},
|
||||
|
||||
mixins: [commonMixins],
|
||||
|
||||
mounted() {
|
||||
this.addresses = JSON.parse(JSON.stringify(this.releaseAddresses));
|
||||
this.$nextTick(function () {
|
||||
Tags.init("select[multiple]");
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
releaseMessage: function () {
|
||||
let self = this;
|
||||
// set timeout to allow for user clicking send before the tag filter has applied the tag
|
||||
window.setTimeout(function () {
|
||||
if (!self.addresses.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let data = {
|
||||
to: self.addresses
|
||||
}
|
||||
|
||||
self.post('api/v1/message/' + self.message.ID + '/release', data, function (response) {
|
||||
self.modal("ReleaseModal").hide();
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="modal-dialog modal-lg" v-if="message">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="AppInfoModalLabel">Release email</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h6>Send this message to one or more addresses specified below.</h6>
|
||||
<div class="row">
|
||||
<label class="col-sm-2 col-form-label text-muted">From</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" :value="message.From.Address">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class=" col-sm-2 col-form-label text-muted">Subject</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" readonly class="form-control-plaintext" :value="message.Subject">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 col-form-label text-muted">Send to</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-select tag-selector" v-model="addresses" multiple data-allow-new="true"
|
||||
data-clear-end="true" data-allow-clear="true" data-placeholder="Enter email addresses..."
|
||||
data-add-on-blur="true" data-badge-style="primary"
|
||||
data-regex='^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$'
|
||||
data-separator="|,|">
|
||||
<option value="">Enter email addresses...</option>
|
||||
<!-- you need at least one option with the placeholder -->
|
||||
<option v-for="t in releaseAddresses" :value="t">{{ t }}</option>
|
||||
</select>
|
||||
<div class="invalid-feedback">Invalid email address</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text text-center">
|
||||
Note: For testing purposes, a unique Message-Id will be generated on send.
|
||||
<br class="d-none d-md-inline">
|
||||
SMTP delivery failures will bounce back to
|
||||
<b v-if="relayConfig.MessageRelay.ReturnPath != ''">{{ relayConfig.MessageRelay.ReturnPath }}</b>
|
||||
<b v-else>{{ message.ReturnPath }}</b>.
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" :disabled="!addresses.length"
|
||||
v-on:click="releaseMessage">Release</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loading" v-if="loading">
|
||||
<div class="d-flex justify-content-center align-items-center h-100">
|
||||
<div class="spinner-border text-secondary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -27,7 +27,7 @@
|
||||
"get": {
|
||||
"description": "Returns basic runtime information, message totals and latest release version.",
|
||||
"produces": [
|
||||
"application/octet-stream"
|
||||
"application/json"
|
||||
],
|
||||
"schemes": [
|
||||
"http",
|
||||
@ -36,7 +36,7 @@
|
||||
"tags": [
|
||||
"application"
|
||||
],
|
||||
"summary": "Get the application information",
|
||||
"summary": "Get application information",
|
||||
"operationId": "AppInformation",
|
||||
"responses": {
|
||||
"200": {
|
||||
@ -240,6 +240,54 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/message/{ID}/release": {
|
||||
"post": {
|
||||
"description": "Release a message via a preconfigured external SMTP server..",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"message"
|
||||
],
|
||||
"summary": "Release message",
|
||||
"operationId": "Release",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "message id",
|
||||
"name": "ID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Array of email addresses to release message to",
|
||||
"name": "To",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"description": "Array of email addresses to release message to",
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/ReleaseMessageRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/OKResponse"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/responses/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/messages": {
|
||||
"get": {
|
||||
"description": "Returns messages from the mailbox ordered from newest to oldest.",
|
||||
@ -438,6 +486,31 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/webui": {
|
||||
"get": {
|
||||
"description": "Returns configuration settings for the web UI.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"application"
|
||||
],
|
||||
"summary": "Get web UI configuration",
|
||||
"operationId": "WebUIConfiguration",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/WebUIConfigurationResponse"
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/responses/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
@ -456,7 +529,7 @@
|
||||
"x-go-package": "net/mail"
|
||||
},
|
||||
"AppInformation": {
|
||||
"description": "Response includes the current and latest Mailpit versions, database info, and memory usage",
|
||||
"description": "Response includes the current and latest Mailpit version, database info, and memory usage",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Database": {
|
||||
@ -593,6 +666,10 @@
|
||||
"$ref": "#/definitions/Address"
|
||||
}
|
||||
},
|
||||
"ReturnPath": {
|
||||
"description": "ReturnPath is the Return-Path",
|
||||
"type": "string"
|
||||
},
|
||||
"Size": {
|
||||
"description": "Message size in bytes",
|
||||
"type": "integer",
|
||||
@ -747,6 +824,22 @@
|
||||
},
|
||||
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
||||
},
|
||||
"ReleaseMessageRequest": {
|
||||
"description": "Release request",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"to": {
|
||||
"description": "To\nin:body",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-go-name": "To"
|
||||
}
|
||||
},
|
||||
"x-go-name": "releaseMessageRequest",
|
||||
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
||||
},
|
||||
"SetReadStatusRequest": {
|
||||
"description": "Set read status request",
|
||||
"type": "object",
|
||||
@ -773,7 +866,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ids": {
|
||||
"description": "ids\nin:body",
|
||||
"description": "IDs\nin:body",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
@ -791,6 +884,32 @@
|
||||
},
|
||||
"x-go-name": "setTagsRequest",
|
||||
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
||||
},
|
||||
"WebUIConfiguration": {
|
||||
"description": "Response includes global web UI settings",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"MessageRelay": {
|
||||
"description": "Message Relay information",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Enabled": {
|
||||
"description": "Whether message relaying (release) is enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
"ReturnPath": {
|
||||
"description": "Enforced Return-Path (if set) for relay bounces",
|
||||
"type": "string"
|
||||
},
|
||||
"SMTPServer": {
|
||||
"description": "The configured SMTP server address",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-go-name": "webUIConfiguration",
|
||||
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
@ -822,6 +941,17 @@
|
||||
},
|
||||
"TextResponse": {
|
||||
"description": "Plain text response"
|
||||
},
|
||||
"WebUIConfigurationResponse": {
|
||||
"description": "Web UI configuration",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/WebUIConfiguration"
|
||||
},
|
||||
"headers": {
|
||||
"Body": {
|
||||
"description": "Web UI configuration settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -404,6 +404,11 @@ func GetMessage(id string) (*Message, error) {
|
||||
from = &mail.Address{Name: env.GetHeader("From")}
|
||||
}
|
||||
|
||||
returnPath := strings.Trim(env.GetHeader("Return-Path"), "<>")
|
||||
if returnPath == "" {
|
||||
returnPath = from.Address
|
||||
}
|
||||
|
||||
date, err := env.Date()
|
||||
if err != nil {
|
||||
// return received datetime when message does not contain a date header
|
||||
@ -435,18 +440,19 @@ func GetMessage(id string) (*Message, error) {
|
||||
}
|
||||
|
||||
obj := Message{
|
||||
ID: id,
|
||||
Read: true,
|
||||
From: from,
|
||||
Date: date,
|
||||
To: addressToSlice(env, "To"),
|
||||
Cc: addressToSlice(env, "Cc"),
|
||||
Bcc: addressToSlice(env, "Bcc"),
|
||||
ReplyTo: addressToSlice(env, "Reply-To"),
|
||||
Subject: env.GetHeader("Subject"),
|
||||
Tags: getMessageTags(id),
|
||||
Size: len(raw),
|
||||
Text: env.Text,
|
||||
ID: id,
|
||||
Read: true,
|
||||
From: from,
|
||||
Date: date,
|
||||
To: addressToSlice(env, "To"),
|
||||
Cc: addressToSlice(env, "Cc"),
|
||||
Bcc: addressToSlice(env, "Bcc"),
|
||||
ReplyTo: addressToSlice(env, "Reply-To"),
|
||||
ReturnPath: returnPath,
|
||||
Subject: env.GetHeader("Subject"),
|
||||
Tags: getMessageTags(id),
|
||||
Size: len(raw),
|
||||
Text: env.Text,
|
||||
}
|
||||
|
||||
// strip base tags
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/utils/logger"
|
||||
"github.com/jhillyerd/enmime"
|
||||
)
|
||||
|
||||
@ -230,7 +231,7 @@ func BenchmarkImportMime(b *testing.B) {
|
||||
}
|
||||
|
||||
func setup() {
|
||||
config.NoLogging = true
|
||||
logger.NoLogging = true
|
||||
config.MaxMessages = 0
|
||||
config.DataFile = ""
|
||||
|
||||
|
@ -25,6 +25,8 @@ type Message struct {
|
||||
Bcc []*mail.Address
|
||||
// ReplyTo addresses
|
||||
ReplyTo []*mail.Address
|
||||
// ReturnPath is the Return-Path
|
||||
ReturnPath string
|
||||
// Message subject
|
||||
Subject string
|
||||
// Message date if set, else date received
|
||||
|
@ -7,12 +7,17 @@ import (
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
log *logrus.Logger
|
||||
// VerboseLogging for verbose logging
|
||||
VerboseLogging bool
|
||||
// QuietLogging shows only errors
|
||||
QuietLogging bool
|
||||
// NoLogging shows only fatal errors
|
||||
NoLogging bool
|
||||
)
|
||||
|
||||
// Log returns the logger instance
|
||||
@ -20,13 +25,13 @@ func Log() *logrus.Logger {
|
||||
if log == nil {
|
||||
log = logrus.New()
|
||||
log.SetLevel(logrus.InfoLevel)
|
||||
if config.VerboseLogging {
|
||||
if VerboseLogging {
|
||||
// verbose logging (debug)
|
||||
log.SetLevel(logrus.DebugLevel)
|
||||
} else if config.QuietLogging {
|
||||
} else if QuietLogging {
|
||||
// show errors only
|
||||
log.SetLevel(logrus.ErrorLevel)
|
||||
} else if config.NoLogging {
|
||||
} else if NoLogging {
|
||||
// disable all logging (tests)
|
||||
log.SetLevel(logrus.PanicLevel)
|
||||
}
|
||||
|
59
utils/tools/message.go
Normal file
59
utils/tools/message.go
Normal file
@ -0,0 +1,59 @@
|
||||
// Package tools provides various methods for variouws things
|
||||
package tools
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"net/mail"
|
||||
"regexp"
|
||||
|
||||
"github.com/axllent/mailpit/utils/logger"
|
||||
)
|
||||
|
||||
// RemoveMessageHeaders scans a message for headers, if found them removes them.
|
||||
// It will only remove a single instance of any header, and is intended to remove
|
||||
// Bcc & Message-Id.
|
||||
func RemoveMessageHeaders(msg []byte, headers []string) ([]byte, error) {
|
||||
reader := bytes.NewReader(msg)
|
||||
m, err := mail.ReadMessage(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reBlank := regexp.MustCompile(`^\s+`)
|
||||
|
||||
for _, hdr := range headers {
|
||||
// case-insentitive
|
||||
reHdr := regexp.MustCompile(`(?i)^` + regexp.QuoteMeta(hdr+":"))
|
||||
|
||||
// header := []byte(hdr + ":")
|
||||
if m.Header.Get(hdr) != "" {
|
||||
scanner := bufio.NewScanner(bytes.NewReader(msg))
|
||||
found := false
|
||||
hdr := []byte("")
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
if !found && reHdr.Match(line) {
|
||||
// add the first line starting with <header>:
|
||||
hdr = append(hdr, line...)
|
||||
hdr = append(hdr, []byte("\r\n")...)
|
||||
found = true
|
||||
} else if found && reBlank.Match(line) {
|
||||
// add any following lines starting with a whitespace (tab or space)
|
||||
hdr = append(hdr, line...)
|
||||
hdr = append(hdr, []byte("\r\n")...)
|
||||
} else if found {
|
||||
// stop scanning, we have the full <header>
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(hdr) > 0 {
|
||||
logger.Log().Debugf("[release] removing %s header", hdr)
|
||||
msg = bytes.Replace(msg, hdr, []byte(""), 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user