1
0
mirror of https://github.com/axllent/mailpit.git synced 2025-03-17 21:18:19 +02:00

Merge branch 'feature/smtp-allowed-recipients' into develop

This commit is contained in:
Ralph Slooten 2024-01-03 12:21:30 +13:00
commit d705571cb5
16 changed files with 106 additions and 68 deletions

View File

@ -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 <CR><CR><LF>")
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")

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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())
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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])
}

View File

@ -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,

View File

@ -240,19 +240,23 @@ export default {
</tr>
<tr>
<td>
SMTP messages received
SMTP messages accepted
</td>
<td>
{{ formatNumber(mailbox.appInfo.RuntimeStats.SMTPReceived) }}
({{ getFileSize(mailbox.appInfo.RuntimeStats.SMTPReceivedSize) }})
{{ formatNumber(mailbox.appInfo.RuntimeStats.SMTPAccepted) }}
<small class="text-secondary">
({{
getFileSize(mailbox.appInfo.RuntimeStats.SMTPAcceptedSize)
}})
</small>
</td>
</tr>
<tr>
<td>
SMTP errors
SMTP messages rejected
</td>
<td>
{{ formatNumber(mailbox.appInfo.RuntimeStats.SMTPErrors) }}
{{ formatNumber(mailbox.appInfo.RuntimeStats.SMTPRejected) }}
</td>
</tr>
<tr>

View File

@ -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"
},

View File

@ -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
}