diff --git a/cmd/root.go b/cmd/root.go index e972a95..03a3878 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -103,6 +103,7 @@ func init() { rootCmd.Flags().BoolVar(&config.SMTPAuthAllowInsecure, "smtp-auth-allow-insecure", config.SMTPAuthAllowInsecure, "Enable insecure PLAIN & LOGIN authentication") rootCmd.Flags().BoolVar(&config.SMTPStrictRFCHeaders, "smtp-strict-rfc-headers", config.SMTPStrictRFCHeaders, "Return SMTP error if message headers contain ") rootCmd.Flags().IntVar(&config.SMTPMaxRecipients, "smtp-max-recipients", config.SMTPMaxRecipients, "Maximum SMTP recipients allowed") + rootCmd.Flags().StringVar(&config.SMTPAllowedRecipients, "smtp-allowed-recipients", config.SMTPAllowedRecipients, "Only allow SMTP recipients matching a regular expression (default allow all)") 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!)") @@ -170,6 +171,9 @@ func initConfigFromEnv() { if len(os.Getenv("MP_SMTP_MAX_RECIPIENTS")) > 0 { config.SMTPMaxRecipients, _ = strconv.Atoi(os.Getenv("MP_SMTP_MAX_RECIPIENTS")) } + if len(os.Getenv("MP_SMTP_ALLOWED_RECIPIENTS")) > 0 { + config.SMTPAllowedRecipients = os.Getenv("MP_SMTP_ALLOWED_RECIPIENTS") + } // Relay server config config.SMTPRelayConfigFile = os.Getenv("MP_SMTP_RELAY_CONFIG") diff --git a/config/config.go b/config/config.go index c8e7011..15aa286 100644 --- a/config/config.go +++ b/config/config.go @@ -93,6 +93,12 @@ var ( // @see https://github.com/axllent/mailpit/issues/87 & https://github.com/axllent/mailpit/issues/153 SMTPStrictRFCHeaders bool + // SMTPAllowedRecipients if set, will only accept recipients matching this regular expression + SMTPAllowedRecipients string + + // SMTPAllowedRecipientsRegexp is the compiled version of SMTPAllowedRecipients + SMTPAllowedRecipientsRegexp *regexp.Regexp + // ReleaseEnabled is whether message releases are enabled, requires a valid SMTPRelayConfigFile ReleaseEnabled = false @@ -262,6 +268,16 @@ func VerifyConfig() error { } } + if SMTPAllowedRecipients != "" { + restrictRegexp, err := regexp.Compile(SMTPAllowedRecipients) + if err != nil { + return fmt.Errorf("Failed to compile smtp-allowed-recipients regexp: %s", err.Error()) + } + + SMTPAllowedRecipientsRegexp = restrictRegexp + logger.Log().Infof("[smtp] only allowing recipients matching the following regexp: %s", SMTPAllowedRecipients) + } + if err := parseRelayConfig(SMTPRelayConfigFile); err != nil { return err } @@ -335,11 +351,11 @@ func parseRelayConfig(c string) error { if SMTPRelayConfig.RecipientAllowlist != "" { if err != nil { - return fmt.Errorf("failed to compile recipient allowlist regexp: %e", err) + return fmt.Errorf("Failed to compile relay recipient allowlist regexp: %s", err.Error()) } SMTPRelayConfig.RecipientAllowlistRegexp = allowlistRegexp - logger.Log().Infof("[smtp] recipient allowlist is active with the following regexp: %s", SMTPRelayConfig.RecipientAllowlist) + logger.Log().Infof("[smtp] relay recipient allowlist is active with the following regexp: %s", SMTPRelayConfig.RecipientAllowlist) } diff --git a/internal/htmlcheck/css.go b/internal/htmlcheck/css.go index 53a404e..84a5708 100644 --- a/internal/htmlcheck/css.go +++ b/internal/htmlcheck/css.go @@ -25,14 +25,12 @@ func runCSSTests(html string) ([]Warning, int, error) { inlined, err := inlineRemoteCSS(html) if err != nil { - // logger.Log().Warn(err) inlined = html } // merge all CSS inline merged, err := mergeInlineCSS(inlined) if err != nil { - // logger.Log().Warn(err) merged = inlined } @@ -157,7 +155,7 @@ func inlineRemoteCSS(h string) (string, error) { resp, err := downloadToBytes(a.Val) if err != nil { - logger.Log().Warningf("html check failed to download %s", a.Val) + logger.Log().Warnf("[html-check] failed to download %s", a.Val) continue } @@ -179,7 +177,7 @@ func inlineRemoteCSS(h string) (string, error) { newDoc, err := doc.Html() if err != nil { - logger.Log().Warning(err) + logger.Log().Warnf("[html-check] failed to download %s", err.Error()) return h, err } diff --git a/internal/linkcheck/status.go b/internal/linkcheck/status.go index cc3f004..102f619 100644 --- a/internal/linkcheck/status.go +++ b/internal/linkcheck/status.go @@ -79,7 +79,7 @@ func doHead(link string, followRedirects bool) (int, error) { req, err := http.NewRequest("HEAD", link, nil) if err != nil { - logger.Log().Error(err) + logger.Log().Errorf("[link-check] %s", err.Error()) return 0, err } diff --git a/internal/stats/stats.go b/internal/stats/stats.go index e31ed07..c107926 100644 --- a/internal/stats/stats.go +++ b/internal/stats/stats.go @@ -21,9 +21,9 @@ var ( mu sync.RWMutex - smtpReceived int - smtpReceivedSize int - smtpErrors int + smtpAccepted int + smtpAcceptedSize int + smtpRejected int smtpIgnored int ) @@ -52,13 +52,13 @@ type AppInformation struct { Memory uint64 // Messages deleted MessagesDeleted int - // SMTP messages received via since run - SMTPReceived int - // Total size in bytes of received messages since run - SMTPReceivedSize int - // SMTP errors since run - SMTPErrors int - // SMTP messages ignored since run (duplicate IDs) + // SMTP accepted messages since run + SMTPAccepted int + // Total size in bytes of accepted messages since run + SMTPAcceptedSize int + // SMTP rejected messages since run + SMTPRejected int + // SMTP ignored messages since run (duplicate IDs) SMTPIgnored int } } @@ -75,9 +75,9 @@ func Load() AppInformation { info.RuntimeStats.Uptime = int(time.Since(startedAt).Seconds()) info.RuntimeStats.MessagesDeleted = storage.StatsDeleted - info.RuntimeStats.SMTPReceived = smtpReceived - info.RuntimeStats.SMTPReceivedSize = smtpReceivedSize - info.RuntimeStats.SMTPErrors = smtpErrors + info.RuntimeStats.SMTPAccepted = smtpAccepted + info.RuntimeStats.SMTPAcceptedSize = smtpAcceptedSize + info.RuntimeStats.SMTPRejected = smtpRejected info.RuntimeStats.SMTPIgnored = smtpIgnored if latestVersionCache != "" { @@ -116,18 +116,18 @@ func Track() { startedAt = time.Now() } -// LogSMTPReceived logs a successfully SMTP transaction -func LogSMTPReceived(size int) { +// LogSMTPAccepted logs a successful SMTP transaction +func LogSMTPAccepted(size int) { mu.Lock() - smtpReceived = smtpReceived + 1 - smtpReceivedSize = smtpReceivedSize + size + smtpAccepted = smtpAccepted + 1 + smtpAcceptedSize = smtpAcceptedSize + size mu.Unlock() } -// LogSMTPError logs a failed SMTP transaction -func LogSMTPError() { +// LogSMTPRejected logs a rejected SMTP transaction +func LogSMTPRejected() { mu.Lock() - smtpErrors = smtpErrors + 1 + smtpRejected = smtpRejected + 1 mu.Unlock() } diff --git a/internal/storage/database.go b/internal/storage/database.go index b2d1b76..a9fc539 100644 --- a/internal/storage/database.go +++ b/internal/storage/database.go @@ -110,7 +110,7 @@ func InitDB() error { func Close() { if db != nil { if err := db.Close(); err != nil { - logger.Log().Warning("[db] error closing database, ignoring") + logger.Log().Warn("[db] error closing database, ignoring") } } @@ -128,7 +128,7 @@ func Store(body *[]byte) (string, error) { // Parse message body with enmime env, err := enmime.ReadEnvelope(bytes.NewReader(*body)) if err != nil { - logger.Log().Warningf("[db] %s", err.Error()) + logger.Log().Warnf("[message] %s", err.Error()) return "", nil } @@ -271,12 +271,12 @@ func List(start, limit int) ([]MessageSummary, error) { em := MessageSummary{} if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &snippet); err != nil { - logger.Log().Error(err) + logger.Log().Errorf("[db] %s", err.Error()) return } if err := json.Unmarshal([]byte(metadata), &em); err != nil { - logger.Log().Error(err) + logger.Log().Errorf("[json] %s", err.Error()) return } @@ -349,7 +349,7 @@ func GetMessage(id string) (*Message, error) { var created int64 if err := row.Scan(&created); err != nil { - logger.Log().Error(err) + logger.Log().Errorf("[db] %s", err.Error()) return } @@ -357,7 +357,7 @@ func GetMessage(id string) (*Message, error) { date = time.UnixMilli(created) }); err != nil { - logger.Log().Error(err) + logger.Log().Errorf("[db] %s", err.Error()) } } diff --git a/internal/storage/migrationTasks.go b/internal/storage/migrationTasks.go index 94ebff3..756a762 100644 --- a/internal/storage/migrationTasks.go +++ b/internal/storage/migrationTasks.go @@ -36,7 +36,7 @@ func migrateTagsToManyMany() { tags := []string{} if err := json.Unmarshal([]byte(jsonTags), &tags); err != nil { - logger.Log().Error(err) + logger.Log().Errorf("[json] %s", err.Error()) return } diff --git a/internal/storage/reindex.go b/internal/storage/reindex.go index bc5c3c6..282f857 100644 --- a/internal/storage/reindex.go +++ b/internal/storage/reindex.go @@ -29,7 +29,7 @@ func ReindexAll() { }) if err != nil { - logger.Log().Error(err) + logger.Log().Errorf("[db] %s", err.Error()) os.Exit(1) } @@ -59,7 +59,7 @@ func ReindexAll() { env, err := enmime.ReadEnvelope(r) if err != nil { - logger.Log().Error(err) + logger.Log().Errorf("[message] %s", err.Error()) continue } @@ -77,7 +77,7 @@ func ReindexAll() { ctx := context.Background() tx, err := db.BeginTx(ctx, nil) if err != nil { - logger.Log().Error(err) + logger.Log().Errorf("[db] %s", err.Error()) continue } @@ -88,13 +88,13 @@ func ReindexAll() { for _, u := range updates { _, err = tx.Exec("UPDATE mailbox SET SearchText = ?, Snippet = ? WHERE ID = ?", u.SearchText, u.Snippet, u.ID) if err != nil { - logger.Log().Error(err) + logger.Log().Errorf("[db] %s", err.Error()) continue } } if err := tx.Commit(); err != nil { - logger.Log().Error(err) + logger.Log().Errorf("[db] %s", err.Error()) continue } diff --git a/internal/storage/search.go b/internal/storage/search.go index 8750d78..0617275 100644 --- a/internal/storage/search.go +++ b/internal/storage/search.go @@ -43,12 +43,12 @@ func Search(search string, start, limit int) ([]MessageSummary, int, error) { em := MessageSummary{} if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &snippet, &ignore, &ignore, &ignore, &ignore); err != nil { - logger.Log().Error(err) + logger.Log().Errorf("[db] %s", err.Error()) return } if err := json.Unmarshal([]byte(metadata), &em); err != nil { - logger.Log().Error(err) + logger.Log().Errorf("[db] %s", err.Error()) return } @@ -114,7 +114,7 @@ func DeleteSearch(search string) error { var ignore string if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &snippet, &ignore, &ignore, &ignore, &ignore); err != nil { - logger.Log().Error(err) + logger.Log().Errorf("[db] %s", err.Error()) return } diff --git a/internal/storage/tags.go b/internal/storage/tags.go index 8d3ef6b..7bcffe8 100644 --- a/internal/storage/tags.go +++ b/internal/storage/tags.go @@ -149,7 +149,7 @@ func GetAllTags() []string { QueryAndClose(nil, db, func(row *sql.Rows) { tags = append(tags, name) }); err != nil { - logger.Log().Error(err) + logger.Log().Errorf("[db] %s", err.Error()) } return tags @@ -172,7 +172,7 @@ func GetAllTagsCount() map[string]int64 { tags[name] = total // tags = append(tags, name) }); err != nil { - logger.Log().Error(err) + logger.Log().Errorf("[db] %s", err.Error()) } return tags @@ -193,7 +193,7 @@ func pruneUnusedTags() error { var c int if err := row.Scan(&id, &n, &c); err != nil { - logger.Log().Error("[tags]", err) + logger.Log().Errorf("[tags] %s", err.Error()) return } diff --git a/server/apiv1/thumbnails.go b/server/apiv1/thumbnails.go index 8eb8728..8a1291e 100644 --- a/server/apiv1/thumbnails.go +++ b/server/apiv1/thumbnails.go @@ -77,7 +77,7 @@ func Thumbnail(w http.ResponseWriter, r *http.Request) { img, err := imaging.Decode(buf) if err != nil { // it's not an image, return default - logger.Log().Warning(err) + logger.Log().Warnf("[image] %s", err.Error()) blankImage(a, w) return } @@ -99,7 +99,7 @@ func Thumbnail(w http.ResponseWriter, r *http.Request) { dst = imaging.OverlayCenter(dst, dstImageFill, 1.0) if err := jpeg.Encode(foo, dst, &jpeg.Options{Quality: 70}); err != nil { - logger.Log().Warning(err) + logger.Log().Warnf("[image] %s", err.Error()) blankImage(a, w) return } @@ -120,7 +120,7 @@ func blankImage(a *enmime.Part, w http.ResponseWriter) { dstImageFill := imaging.Fill(img, thumbWidth, thumbHeight, imaging.Center, imaging.Lanczos) if err := jpeg.Encode(foo, dstImageFill, &jpeg.Options{Quality: 70}); err != nil { - logger.Log().Warning(err) + logger.Log().Warnf("[image] %s", err.Error()) } fileName := a.FileName diff --git a/server/handlers/proxy.go b/server/handlers/proxy.go index 010c4c4..0dd360b 100644 --- a/server/handlers/proxy.go +++ b/server/handlers/proxy.go @@ -95,7 +95,7 @@ func ProxyHandler(w http.ResponseWriter, r *http.Request) { address, err := absoluteURL(parts[3], uri) if err != nil { - logger.Log().Error(err) + logger.Log().Errorf("[proxy] %s", err.Error()) return []byte(parts[3]) } diff --git a/server/smtpd/smtpd.go b/server/smtpd/smtpd.go index ba09058..70d8867 100644 --- a/server/smtpd/smtpd.go +++ b/server/smtpd/smtpd.go @@ -28,7 +28,7 @@ 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("[smtpd] error parsing message: %s", err.Error()) - stats.LogSMTPError() + stats.LogSMTPRejected() return err } @@ -121,11 +121,10 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error { _, err = storage.Store(&data) if err != nil { logger.Log().Errorf("[db] error storing message: %s", err.Error()) - stats.LogSMTPError() return err } - stats.LogSMTPReceived(len(data)) + stats.LogSMTPAccepted(len(data)) data = nil // avoid memory leaks @@ -153,6 +152,22 @@ func authHandlerAny(remoteAddr net.Addr, mechanism string, username []byte, _ [] return true, nil } +// HandlerRcpt used to optionally restrict recipients based on `--smtp-allowed-recipients` +func handlerRcpt(remoteAddr net.Addr, from string, to string) bool { + if config.SMTPAllowedRecipientsRegexp == nil { + return true + } + + result := config.SMTPAllowedRecipientsRegexp.MatchString(to) + + if !result { + logger.Log().Warnf("[smtpd] rejected message to %s from %s (%s)", to, from, cleanIP(remoteAddr)) + stats.LogSMTPRejected() + } + + return result +} + // Listen starts the SMTPD server func Listen() error { if config.SMTPAuthAllowInsecure { @@ -178,6 +193,7 @@ func listenAndServe(addr string, handler smtpd.Handler, authHandler smtpd.AuthHa srv := &smtpd.Server{ Addr: addr, Handler: handler, + HandlerRcpt: handlerRcpt, Appname: "Mailpit", Hostname: "", AuthHandler: nil, diff --git a/server/ui-src/components/AboutMailpit.vue b/server/ui-src/components/AboutMailpit.vue index 06991ed..b2af888 100644 --- a/server/ui-src/components/AboutMailpit.vue +++ b/server/ui-src/components/AboutMailpit.vue @@ -240,19 +240,23 @@ export default { - SMTP messages received + SMTP messages accepted - {{ formatNumber(mailbox.appInfo.RuntimeStats.SMTPReceived) }} - ({{ getFileSize(mailbox.appInfo.RuntimeStats.SMTPReceivedSize) }}) + {{ formatNumber(mailbox.appInfo.RuntimeStats.SMTPAccepted) }} + + ({{ + getFileSize(mailbox.appInfo.RuntimeStats.SMTPAcceptedSize) + }}) + - SMTP errors + SMTP messages rejected - {{ formatNumber(mailbox.appInfo.RuntimeStats.SMTPErrors) }} + {{ formatNumber(mailbox.appInfo.RuntimeStats.SMTPRejected) }} diff --git a/server/ui/api/v1/swagger.json b/server/ui/api/v1/swagger.json index afa0773..8aa7adf 100644 --- a/server/ui/api/v1/swagger.json +++ b/server/ui/api/v1/swagger.json @@ -762,23 +762,23 @@ "type": "integer", "format": "int64" }, - "SMTPErrors": { - "description": "SMTP errors since run", + "SMTPAccepted": { + "description": "SMTP accepted messages since run", + "type": "integer", + "format": "int64" + }, + "SMTPAcceptedSize": { + "description": "Total size in bytes of accepted messages since run", "type": "integer", "format": "int64" }, "SMTPIgnored": { - "description": "SMTP messages ignored since run (duplicate IDs)", + "description": "SMTP ignored messages since run (duplicate IDs)", "type": "integer", "format": "int64" }, - "SMTPReceived": { - "description": "SMTP messages received via since run", - "type": "integer", - "format": "int64" - }, - "SMTPReceivedSize": { - "description": "Total size in bytes of received messages since run", + "SMTPRejected": { + "description": "SMTP rejected messages since run", "type": "integer", "format": "int64" }, diff --git a/server/websockets/client.go b/server/websockets/client.go index 547ea0e..c4523c0 100644 --- a/server/websockets/client.go +++ b/server/websockets/client.go @@ -132,7 +132,7 @@ func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { - logger.Log().Error(err) + logger.Log().Errorf("[websocket] %s", err.Error()) return }