diff --git a/README.md b/README.md index 1a54f60..496537d 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ ![CodeQL](https://github.com/axllent/mailpit/actions/workflows/codeql-analysis.yml/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/axllent/mailpit)](https://goreportcard.com/report/github.com/axllent/mailpit) -Mailpit is a multi-platform email testing tool & API for developers. +Mailpit is a small, fast, low memory, zero-dependency, multi-platform email testing tool & API for developers. -It acts as both an SMTP server, and provides a web interface to view all captured emails. It also contains an API for automated integration testing. +It acts as an SMTP server, provides a modern web interface to view & test captured emails, and contains an API for automated integration testing. -Mailpit is inspired by [MailHog](#why-rewrite-mailhog), but modern and much, much faster. +Mailpit was originally **inspired** by MailHog which is now [no longer maintained](https://github.com/mailhog/MailHog/issues/442#issuecomment-1493415258) and hasn't seen active development for a few years now. ![Mailpit](https://raw.githubusercontent.com/axllent/mailpit/develop/docs/screenshot.png) @@ -21,6 +21,7 @@ Mailpit is inspired by [MailHog](#why-rewrite-mailhog), but modern and much, muc - SMTP server (default `0.0.0.0:1025`) - Web UI to view emails (formatted HTML, highlighted HTML source, text, headers, raw source and MIME attachments including image thumbnails) - HTML check to test & score mail client compatibility with HTML emails +- Link check to test message links (HTML & text) & linked images - Light & dark web UI theme with auto-detect - Mobile and tablet HTML preview toggle in desktop mode - Advanced mail search ([see wiki](https://github.com/axllent/mailpit/wiki/Mail-search)) @@ -88,12 +89,3 @@ Please refer to [the documentation](https://github.com/axllent/mailpit/wiki/Test ### Configuring sendmail Mailpit's SMTP server (by default on port 1025), so you will likely need to configure your sending application to deliver mail via that port. A common MTA (Mail Transfer Agent) that delivers system emails to a SMTP server is `sendmail`, used by many applications including PHP. Mailpit can also act as substitute for sendmail. For instructions of how to set this up, please refer to the [sendmail documentation](https://github.com/axllent/mailpit/wiki/Configuring-sendmail). - - -## Why rewrite MailHog? - -I had been using MailHog for a few years to intercept and test emails, but experienced a number of severe performance issues. Many of the frontend and Go libraries are very out of date, and the project [is no longer maintained](https://github.com/mailhog/MailHog/issues/442#issuecomment-1493415258). - -Initially I tried to upgrade a fork of MailHog (the UI, the HTTP server and the API), but discovered that it is (with all due respect to its authors) far too complex. I found it over-engineered (split over 9 separate projects), and performs very poorly when dealing with large amounts of emails or emails with attachments (a single email with a 3MB attachment can take over a minute to ingest). Finally the API transmits a lot of duplicate & irrelevant data on every browser request, all without any HTTP compression. - -In order to improve it I felt it needed to be completely rewritten, and so Mailpit was born. diff --git a/cmd/root.go b/cmd/root.go index 338efaf..7483e4c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -100,6 +100,7 @@ func init() { rootCmd.Flags().StringVar(&config.SMTPTLSCert, "smtp-tls-cert", config.SMTPTLSCert, "TLS certificate for SMTP (STARTTLS) - requires smtp-tls-key") rootCmd.Flags().StringVar(&config.SMTPTLSKey, "smtp-tls-key", config.SMTPTLSKey, "TLS key for SMTP (STARTTLS) - requires smtp-tls-cert") 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().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!)") @@ -185,6 +186,9 @@ func initConfigFromEnv() { if getEnabledFromEnv("MP_SMTP_AUTH_ALLOW_INSECURE") { config.SMTPAuthAllowInsecure = true } + if getEnabledFromEnv("MP_STRICT_RFC_HEADERS") { + config.SMTPStrictRFCHeaders = true + } // Relay server config if len(os.Getenv("MP_SMTP_RELAY_CONFIG")) > 0 { diff --git a/config/config.go b/config/config.go index 072b0f9..fe18ea9 100644 --- a/config/config.go +++ b/config/config.go @@ -90,6 +90,10 @@ var ( // SMTPRelayConfig to parse a yaml file and store config of relay SMTP server SMTPRelayConfig smtpRelayConfigStruct + // SMTPStrictRFCHeaders will return an error if the email headers contain (\r\r\n) + // @see https://github.com/axllent/mailpit/issues/87 & https://github.com/axllent/mailpit/issues/153 + SMTPStrictRFCHeaders bool + // ReleaseEnabled is whether message releases are enabled, requires a valid SMTPRelayConfigFile ReleaseEnabled = false diff --git a/server/apiv1/api.go b/server/apiv1/api.go index d5fd63d..59d564e 100644 --- a/server/apiv1/api.go +++ b/server/apiv1/api.go @@ -14,6 +14,7 @@ import ( "github.com/axllent/mailpit/server/smtpd" "github.com/axllent/mailpit/storage" "github.com/axllent/mailpit/utils/htmlcheck" + "github.com/axllent/mailpit/utils/linkcheck" "github.com/axllent/mailpit/utils/logger" "github.com/axllent/mailpit/utils/tools" "github.com/gorilla/mux" @@ -638,7 +639,7 @@ func HTMLCheck(w http.ResponseWriter, r *http.Request) { // // # HTML check (beta) // - // Returns the summary of HTML check. + // Returns the summary of the message HTML checker. // // NOTE: This feature is currently in beta and is documented for reference only. // Please do not integrate with it (yet) as there may be changes. @@ -684,6 +685,62 @@ func HTMLCheck(w http.ResponseWriter, r *http.Request) { _, _ = w.Write(bytes) } +// LinkCheck returns a summary of links in the email +func LinkCheck(w http.ResponseWriter, r *http.Request) { + // swagger:route GET /api/v1/message/{ID}/link-check Other LinkCheckResponse + // + // # Link check (beta) + // + // Returns the summary of the message Link checker. + // + // NOTE: This feature is currently in beta and is documented for reference only. + // Please do not integrate with it (yet) as there may be changes. + // + // Produces: + // - application/json + // + // Schemes: http, https + // + // Parameters: + // + name: ID + // in: path + // description: Database ID + // required: true + // type: string + // + name: follow + // in: query + // description: Follow redirects + // required: false + // type: boolean + // default: false + // + // Responses: + // 200: LinkCheckResponse + // default: ErrorResponse + + vars := mux.Vars(r) + id := vars["id"] + + msg, err := storage.GetMessage(id) + if err != nil { + fourOFour(w) + return + } + + f := r.URL.Query().Get("follow") + followRedirects := f == "true" || f == "1" + + summary, err := linkcheck.RunTests(msg, followRedirects) + if err != nil { + httpError(w, err.Error()) + return + } + + bytes, _ := json.Marshal(summary) + w.Header().Add("Content-Type", "application/json") + _, _ = w.Write(bytes) +} + // FourOFour returns a basic 404 message func fourOFour(w http.ResponseWriter) { w.Header().Set("Referrer-Policy", "no-referrer") diff --git a/server/apiv1/structs.go b/server/apiv1/structs.go index 95f0bd3..7bdb5a0 100644 --- a/server/apiv1/structs.go +++ b/server/apiv1/structs.go @@ -3,6 +3,7 @@ package apiv1 import ( "github.com/axllent/mailpit/storage" "github.com/axllent/mailpit/utils/htmlcheck" + "github.com/axllent/mailpit/utils/linkcheck" ) // MessagesSummary is a summary of a list of messages @@ -49,3 +50,6 @@ type Attachment = storage.Attachment // HTMLCheckResponse summary type HTMLCheckResponse = htmlcheck.Response + +// LinkCheckResponse summary +type LinkCheckResponse = linkcheck.Response diff --git a/server/server.go b/server/server.go index b90c06f..b1c6946 100644 --- a/server/server.go +++ b/server/server.go @@ -95,6 +95,7 @@ func defaultRoutes() *mux.Router { if !config.DisableHTMLCheck { r.HandleFunc(config.Webroot+"api/v1/message/{id}/html-check", middleWareFunc(apiv1.HTMLCheck)).Methods("GET") } + r.HandleFunc(config.Webroot+"api/v1/message/{id}/link-check", middleWareFunc(apiv1.LinkCheck)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/message/{id}", middleWareFunc(apiv1.GetMessage)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/info", middleWareFunc(apiv1.AppInfo)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/webui", middleWareFunc(apiv1.WebUIConfig)).Methods("GET") diff --git a/server/smtpd/smtpd.go b/server/smtpd/smtpd.go index 67b2aa2..d78f4a5 100644 --- a/server/smtpd/smtpd.go +++ b/server/smtpd/smtpd.go @@ -17,6 +17,12 @@ import ( ) func mailHandler(origin net.Addr, from string, to []string, data []byte) error { + if !config.SMTPStrictRFCHeaders { + // replace all (\r\r\n) with (\r\n) + // @see https://github.com/axllent/mailpit/issues/87 & https://github.com/axllent/mailpit/issues/153 + data = bytes.ReplaceAll(data, []byte("\r\r\n"), []byte("\r\n")) + } + msg, err := mail.ReadMessage(bytes.NewReader(data)) if err != nil { logger.Log().Errorf("[smtpd] error parsing message: %s", err.Error()) diff --git a/server/ui-src/App.vue b/server/ui-src/App.vue index c998064..9749b4b 100644 --- a/server/ui-src/App.vue +++ b/server/ui-src/App.vue @@ -88,6 +88,10 @@ export default { }, mounted() { + + let title = document.title + ' - ' + location.hostname + document.title = title + this.currentPath = window.location.hash.slice(1) window.addEventListener('hashchange', () => { this.currentPath = window.location.hash.slice(1) @@ -1006,7 +1010,7 @@ export default { - diff --git a/server/ui-src/assets/styles.scss b/server/ui-src/assets/styles.scss index 7695651..0cf6341 100644 --- a/server/ui-src/assets/styles.scss +++ b/server/ui-src/assets/styles.scss @@ -39,14 +39,13 @@ } .nav-tabs .nav-link { - @include media-breakpoint-down(sm) { - // font-size: 14px; + @include media-breakpoint-down(xl) { padding-left: 10px; padding-right: 10px; } } -:not(.text-view) > a { +:not(.text-view) > a:not(.no-icon) { &[href^="http://"], &[href^="https://"] { diff --git a/server/ui-src/templates/Message.vue b/server/ui-src/templates/Message.vue index 5795c62..224a5cf 100644 --- a/server/ui-src/templates/Message.vue +++ b/server/ui-src/templates/Message.vue @@ -6,6 +6,7 @@ import Tags from "bootstrap5-tags" import Attachments from './Attachments.vue' import Headers from './Headers.vue' import HTMLCheck from './MessageHTMLCheck.vue' +import LinkCheck from './MessageLinkCheck.vue' export default { props: { @@ -18,6 +19,7 @@ export default { Attachments, Headers, HTMLCheck, + LinkCheck, }, mixins: [commonMixins], @@ -33,6 +35,7 @@ export default { loadHeaders: false, htmlScore: false, htmlScoreColor: false, + linkCheckErrors: false, showMobileButtons: false, scaleHTMLPreview: 'display', // keys names match bootstrap icon names @@ -219,7 +222,7 @@ export default { let html = s // full links with http(s) - let re = /(\b(https?|ftp):\/\/[\-\w@:%_\+.~#?,&\/\/=;]+)\b/gim + let re = /(\b(https?|ftp):\/\/[\-\w@:%_\+'!.~#?,&\/\/=;]+)/gim html = html.replace(re, '˱˱˱a href=ˠˠˠ$&ˠˠˠ target=_blank rel=noopener˲˲˲$&˱˱˱/a˲˲˲') // plain www links without https?:// prefix @@ -366,15 +369,51 @@ export default { role="tab" aria-controls="nav-raw" aria-selected="false"> Raw - + + + +
diff --git a/server/ui-src/templates/MessageHTMLCheck.vue b/server/ui-src/templates/MessageHTMLCheck.vue index 32ab463..376f67c 100644 --- a/server/ui-src/templates/MessageHTMLCheck.vue +++ b/server/ui-src/templates/MessageHTMLCheck.vue @@ -640,7 +640,7 @@ export default {