diff --git a/CHANGELOG.md b/CHANGELOG.md index 9366cd9..27be4b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,23 @@ Notable changes to Mailpit will be documented in this file. +## 0.1.2 + +### Feature +- Optional browser notifications (HTTPS only) + +### Security +- Don't allow tar files containing a ".." +- Sanitize mailbox names +- Use strconv.Atoi() for safe string to int conversions + + +## 0.1.1 + +### Bugfix +- Fix env variable for MP_UI_SSL_KEY + + ## 0.1.0 ### Feature diff --git a/README.md b/README.md index f52959b..1dc3a32 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Mailpit is inspired by [MailHog](#why-rewrite-mailhog), but much, much faster. - SMTP server (default `0.0.0.0:1025`) - Web UI to view emails (HTML format, text, source and MIME attachments, default `0.0.0.0:8025`) - Real-time web UI updates using web sockets for new mail +- Optional browser notifications for new mail (HTTPS only) - Configurable automatic email pruning (default keeps the most recent 500 emails) - Email storage either in memory or disk ([see wiki](https://github.com/axllent/mailpit/wiki/Email-storage)) - Fast SMTP processing & storing - approximately 300-600 emails per second depending on CPU, network speed & email size @@ -25,11 +26,6 @@ Mailpit is inspired by [MailHog](#why-rewrite-mailhog), but much, much faster. - Multi-architecture [Docker images](https://github.com/axllent/mailpit/wiki/Docker-images) -## Planned features - -- Browser notifications for new mail (HTTPS only) - - ## Installation Download a pre-built binary in the [releases](https://github.com/axllent/mailpit/releases/latest). The `mailpit` can be placed in your `$PATH`, or simply run as `./mailpit`. See `mailpit -h` for options. diff --git a/server/api.go b/server/api.go index c00f850..e5f5662 100644 --- a/server/api.go +++ b/server/api.go @@ -29,7 +29,7 @@ func apiListMailboxes(w http.ResponseWriter, _ *http.Request) { bytes, _ := json.Marshal(res) w.Header().Add("Content-Type", "application/json") - w.Write(bytes) + _, _ = w.Write(bytes) } func apiListMailbox(w http.ResponseWriter, r *http.Request) { @@ -62,7 +62,7 @@ func apiListMailbox(w http.ResponseWriter, r *http.Request) { bytes, _ := json.Marshal(res) w.Header().Add("Content-Type", "application/json") - w.Write(bytes) + _, _ = w.Write(bytes) } func apiSearchMailbox(w http.ResponseWriter, r *http.Request) { @@ -102,7 +102,7 @@ func apiSearchMailbox(w http.ResponseWriter, r *http.Request) { bytes, _ := json.Marshal(res) w.Header().Add("Content-Type", "application/json") - w.Write(bytes) + _, _ = w.Write(bytes) } // Open a message @@ -120,7 +120,7 @@ func apiOpenMessage(w http.ResponseWriter, r *http.Request) { bytes, _ := json.Marshal(msg) w.Header().Add("Content-Type", "application/json") - w.Write(bytes) + _, _ = w.Write(bytes) } // Download/view an attachment @@ -143,7 +143,7 @@ func apiDownloadAttachment(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", a.ContentType) w.Header().Set("Content-Disposition", "filename=\""+fileName+"\"") - w.Write(a.Content) + _, _ = w.Write(a.Content) } // View the full email source as plain text @@ -165,7 +165,7 @@ func apiDownloadSource(w http.ResponseWriter, r *http.Request) { if dl == "1" { w.Header().Set("Content-Disposition", "attachment; filename=\""+id+".eml\"") } - w.Write(data) + _, _ = w.Write(data) } // Delete all messages in the mailbox @@ -181,7 +181,7 @@ func apiDeleteAll(w http.ResponseWriter, r *http.Request) { } w.Header().Add("Content-Type", "text/plain") - w.Write([]byte("ok")) + _, _ = w.Write([]byte("ok")) } // Delete a single message @@ -198,7 +198,7 @@ func apiDeleteOne(w http.ResponseWriter, r *http.Request) { } w.Header().Add("Content-Type", "text/plain") - w.Write([]byte("ok")) + _, _ = w.Write([]byte("ok")) } // Mark single message as unread @@ -215,7 +215,7 @@ func apiUnreadOne(w http.ResponseWriter, r *http.Request) { } w.Header().Add("Content-Type", "text/plain") - w.Write([]byte("ok")) + _, _ = w.Write([]byte("ok")) } // Websocket to broadcast changes diff --git a/server/server.go b/server/server.go index 069ce67..8c202a9 100644 --- a/server/server.go +++ b/server/server.go @@ -64,7 +64,7 @@ func Listen() { func basicAuthResponse(w http.ResponseWriter) { w.Header().Set("WWW-Authenticate", `Basic realm="Login"`) w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte("Unauthorised.\n")) + _, _ = w.Write([]byte("Unauthorised.\n")) } type gzipResponseWriter struct { @@ -156,16 +156,13 @@ func getStartLimit(req *http.Request) (start int, limit int) { limit = 50 s := req.URL.Query().Get("start") - if n, e := strconv.ParseInt(s, 10, 64); e == nil && n > 0 { - start = int(n) + if n, err := strconv.Atoi(s); err == nil && n > 0 { + start = n } l := req.URL.Query().Get("limit") - if n, e := strconv.ParseInt(l, 10, 64); e == nil && n > 0 { - if n > 500 { - n = 500 - } - limit = int(n) + if n, err := strconv.Atoi(l); err == nil && n > 0 { + limit = n } return start, limit diff --git a/server/ui-src/App.vue b/server/ui-src/App.vue index 139076e..e846b83 100644 --- a/server/ui-src/App.vue +++ b/server/ui-src/App.vue @@ -21,7 +21,9 @@ export default { searching: false, isConnected: false, scrollInPlace: false, - message: false + message: false, + notificationsSupported: false, + notificationsEnabled: false } }, watch: { @@ -47,6 +49,10 @@ export default { this.currentPath = window.location.hash.slice(1); }); + this.notificationsSupported = 'https:' == document.location.protocol + && ("Notification" in window && Notification.permission !== "denied"); + this.notificationsEnabled = this.notificationsSupported && Notification.permission == "granted"; + this.connect(); this.loadMessages(); }, @@ -214,6 +220,7 @@ export default { } self.total++; self.unread++; + self.browserNotify("New mail from: " + response.Data.From.Address, response.Data.Subject); } else if (response.Type == "prune") { // messages have been deleted, reload messages to adjust self.scrollInPlace = true; @@ -252,6 +259,41 @@ export default { let d = new Date(message.Created) return moment(d).fromNow().toString(); }, + + browserNotify: function(title, message) { + if (!("Notification" in window)) { + return; + } + + if (Notification.permission === "granted") { + let b = message.Subject; + let options = { + body: message, + icon: 'mailpit.png' + } + new Notification(title, options); + } + }, + + requestNotifications: function() { + // check if the browser supports notifications + if (!("Notification" in window)) { + alert("This browser does not support desktop notification"); + } + + // we need to ask the user for permission + else if (Notification.permission !== "denied") { + let self = this; + Notification.requestPermission().then(function (permission) { + // If the user accepts, let's create a notification + if (permission === "granted") { + self.browserNotify("Notifications enabled", "You will receive notifications when new mails are received."); + self.notificationsEnabled = true; + } + }); + } + } + } } @@ -315,7 +357,7 @@ export default {