mirror of
https://github.com/axllent/mailpit.git
synced 2025-08-13 20:04:49 +02:00
Feature: Web UI notifications of smtpd & POP3 errors (#347)
This commit is contained in:
@@ -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)
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
||||
<div v-for="error in clientErrors" class="toast show" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<svg class="bd-placeholder-img rounded me-2" width="20" height="20" xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true" preserveAspectRatio="xMidYMid slice" focusable="false">
|
||||
<rect width="100%" height="100%" :fill="error.Level == 'warning' ? '#ffc107' : '#dc3545'"></rect>
|
||||
</svg>
|
||||
<strong class="me-auto">{{ error.Type }}</strong>
|
||||
<small class="text-body-secondary">{{ error.IP }}</small>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body">
|
||||
{{ error.Message }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="messageToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header" v-if="toastMessage">
|
||||
<i class="bi bi-envelope-exclamation-fill me-2"></i>
|
||||
|
@@ -90,3 +90,20 @@ func Broadcast(t string, msg interface{}) {
|
||||
|
||||
go func() { MessageHub.Broadcast <- b }()
|
||||
}
|
||||
|
||||
// BroadCastClientError is a wrapper to broadcast client errors to the web UI
|
||||
func BroadCastClientError(severity, errorType, ip, message string) {
|
||||
msg := struct {
|
||||
Level string
|
||||
Type string
|
||||
IP string
|
||||
Message string
|
||||
}{
|
||||
severity,
|
||||
errorType,
|
||||
ip,
|
||||
message,
|
||||
}
|
||||
|
||||
Broadcast("error", msg)
|
||||
}
|
||||
|
Reference in New Issue
Block a user