diff --git a/server/pop3/functions.go b/server/pop3/functions.go index 2ccc45d73..b0049fd48 100644 --- a/server/pop3/functions.go +++ b/server/pop3/functions.go @@ -9,6 +9,7 @@ import ( "github.com/axllent/mailpit/internal/auth" "github.com/axllent/mailpit/internal/logger" "github.com/axllent/mailpit/internal/storage" + "github.com/axllent/mailpit/server/websockets" ) func authUser(username, password string) bool { @@ -19,6 +20,11 @@ func authUser(username, password string) bool { func sendResponse(c net.Conn, m string) { fmt.Fprintf(c, "%s\r\n", m) logger.Log().Debugf("[pop3] response: %s", m) + + if strings.HasPrefix(m, "-ERR ") { + sub, _ := strings.CutPrefix(m, "-ERR ") + websockets.BroadCastClientError("error", "pop3", c.RemoteAddr().String(), sub) + } } // Send a response without debug logging (for data) diff --git a/server/smtpd/smtpd.go b/server/smtpd/smtpd.go index 711cc4304..1fb375c6e 100644 --- a/server/smtpd/smtpd.go +++ b/server/smtpd/smtpd.go @@ -14,6 +14,7 @@ import ( "github.com/axllent/mailpit/internal/logger" "github.com/axllent/mailpit/internal/stats" "github.com/axllent/mailpit/internal/storage" + "github.com/axllent/mailpit/server/websockets" "github.com/lithammer/shortuuid/v4" "github.com/mhale/smtpd" ) @@ -22,7 +23,8 @@ var ( // DisableReverseDNS allows rDNS to be disabled DisableReverseDNS bool - errorResponse = regexp.MustCompile(`^[45]\d\d `) + warningResponse = regexp.MustCompile(`^4\d\d `) + errorResponse = regexp.MustCompile(`^5\d\d `) ) // MailHandler handles the incoming message to store in the database @@ -237,8 +239,12 @@ func listenAndServe(addr string, handler smtpd.MsgIDHandler, authHandler smtpd.A logger.Log().Debugf("[smtpd] %s (%s) %s", verbLogTranslator(verb), remoteIP, line) }, LogWrite: func(remoteIP, verb, line string) { - if errorResponse.MatchString(line) { + if warningResponse.MatchString(line) { logger.Log().Warnf("[smtpd] %s (%s) %s", verbLogTranslator(verb), remoteIP, line) + websockets.BroadCastClientError("warning", "smtpd", remoteIP, line) + } else if errorResponse.MatchString(line) { + logger.Log().Errorf("[smtpd] %s (%s) %s", verbLogTranslator(verb), remoteIP, line) + websockets.BroadCastClientError("error", "smtpd", remoteIP, line) } else { logger.Log().Debugf("[smtpd] %s (%s) %s", verbLogTranslator(verb), remoteIP, line) } diff --git a/server/ui-src/components/Notifications.vue b/server/ui-src/components/Notifications.vue index 2e647e450..8135255bf 100644 --- a/server/ui-src/components/Notifications.vue +++ b/server/ui-src/components/Notifications.vue @@ -21,6 +21,7 @@ export default { socketBreaks: 0, // to track sockets that continually connect & disconnect, reset every 15s pauseNotifications: false, // prevent spamming version: false, + clientErrors: [], // errors received via websocket } }, @@ -39,6 +40,8 @@ export default { mailbox.notificationsSupported = window.isSecureContext && ("Notification" in window && Notification.permission !== "denied") mailbox.notificationsEnabled = mailbox.notificationsSupported && Notification.permission == "granted" + + this.errorNotificationCron() }, methods: { @@ -99,6 +102,9 @@ export default { } else if (response.Type == "truncate") { // broadcast for components this.eventBus.emit("truncate") + } else if (response.Type == "error") { + // broadcast for components + this.addClientError(response.Data) } } @@ -195,12 +201,43 @@ export default { Toast.getOrCreateInstance(el).hide() } }, + + addClientError(d) { + d.expire = Date.now() + 5000 // expire after 5s + this.clientErrors.push(d) + }, + + errorNotificationCron() { + window.setTimeout(() => { + this.clientErrors.forEach((err, idx) => { + if (err.expire < Date.now()) { + this.clientErrors.splice(idx, 1) + } + }) + this.errorNotificationCron() + }, 1000) + } }, }