diff --git a/CHANGELOG.md b/CHANGELOG.md index b5c9e6b..0be2a2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ Notable changes to Mailpit will be documented in this file. +## [v1.8.2] + +### Feature +- Link check to test message links +- Workaround for non-RFC-compliant message headers containing + +### Libs +- Update Go libs + +### UI +- Set hostname in page meta title to identify Mailpit instance + + ## [v1.8.1] ### Docs 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/go.mod b/go.mod index dcc00e5..75c663d 100644 --- a/go.mod +++ b/go.mod @@ -57,15 +57,15 @@ require ( golang.org/x/image v0.11.0 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/sys v0.11.0 // indirect - golang.org/x/tools v0.11.1 // indirect + golang.org/x/tools v0.12.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect lukechampine.com/uint128 v1.3.0 // indirect modernc.org/cc/v3 v3.41.0 // indirect - modernc.org/ccgo/v3 v3.16.14 // indirect + modernc.org/ccgo/v3 v3.16.15 // indirect modernc.org/libc v1.24.1 // indirect modernc.org/mathutil v1.6.0 // indirect - modernc.org/memory v1.6.0 // indirect + modernc.org/memory v1.7.0 // indirect modernc.org/opt v0.1.3 // indirect - modernc.org/strutil v1.1.3 // indirect + modernc.org/strutil v1.2.0 // indirect modernc.org/token v1.1.0 // indirect ) diff --git a/go.sum b/go.sum index 4a61b0e..0d54902 100644 --- a/go.sum +++ b/go.sum @@ -212,8 +212,8 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.11.1 h1:ojD5zOW8+7dOGzdnNgersm8aPfcDjhMp12UfG93NIMc= -golang.org/x/tools v0.11.1/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= +golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -227,22 +227,22 @@ lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q= modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= -modernc.org/ccgo/v3 v3.16.14 h1:af6KNtFgsVmnDYrWk3PQCS9XT6BXe7o3ZFJKkIKvXNQ= -modernc.org/ccgo/v3 v3.16.14/go.mod h1:mPDSujUIaTNWQSG4eqKw+atqLOEbma6Ncsa94WbC9zo= +modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0= +modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI= modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM= modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/memory v1.6.0 h1:i6mzavxrE9a30whzMfwf7XWVODx2r5OYXvU46cirX7o= -modernc.org/memory v1.6.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/memory v1.7.0 h1:2pXdbgdP5hIyDp2JqIwkHNZ1sAjEbh8GnRpcqFWBf7E= +modernc.org/memory v1.7.0/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sqlite v1.25.0 h1:AFweiwPNd/b3BoKnBOfFm+Y260guGMF+0UFk0savqeA= modernc.org/sqlite v1.25.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU= -modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= -modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 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 {