From 7875160aa7b3d4c37bb26b3a3ef888e3a7d78c03 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Tue, 15 Aug 2023 17:13:25 +1200 Subject: [PATCH 1/7] Feature: Workaround for non-RFC-compliant message headers containing Due to a bug in some common sendmail implementations and PHP >=8.0, message headers sometimes contain `\r\r\n` which is not RFC compliant. Mailpit will now fix these non-compliant headers. This can be disabled via `--smtp-strict-rfc-headers` See #87 / #153 --- cmd/root.go | 4 ++++ config/config.go | 4 ++++ server/smtpd/smtpd.go | 6 ++++++ 3 files changed, 14 insertions(+) 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/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()) From bc4b028c398f3759f62977277d963f8d92ac154d Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Tue, 15 Aug 2023 21:31:18 +1200 Subject: [PATCH 2/7] UI: Set hostname in page meta title to identify Mailpit instance @see #154 --- server/ui-src/App.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/ui-src/App.vue b/server/ui-src/App.vue index c998064..7c2de4e 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) From 8dbc661cb78c0c3459693c22f4c96e8e3968748a Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Tue, 15 Aug 2023 21:32:12 +1200 Subject: [PATCH 3/7] Use message ID as key for Message component --- server/ui-src/App.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/ui-src/App.vue b/server/ui-src/App.vue index 7c2de4e..9749b4b 100644 --- a/server/ui-src/App.vue +++ b/server/ui-src/App.vue @@ -1010,7 +1010,7 @@ export default { - From d01fb4044e86ebb11ec5d9508d4d630ef3fb1c9c Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Wed, 16 Aug 2023 16:59:31 +1200 Subject: [PATCH 4/7] Feature: Link check to test message links @see #151 --- server/apiv1/api.go | 59 ++- server/apiv1/structs.go | 4 + server/server.go | 1 + server/ui-src/assets/styles.scss | 5 +- server/ui-src/templates/Message.vue | 67 +++- server/ui-src/templates/MessageHTMLCheck.vue | 2 +- server/ui-src/templates/MessageLinkCheck.vue | 398 +++++++++++++++++++ server/ui/api/v1/swagger.json | 86 +++- utils/htmlcheck/css.go | 3 +- utils/htmlcheck/html.go | 14 +- utils/linkcheck/main.go | 92 +++++ utils/linkcheck/status.go | 106 +++++ utils/linkcheck/structs.go | 21 + utils/tools/html.go | 19 + 14 files changed, 846 insertions(+), 31 deletions(-) create mode 100644 server/ui-src/templates/MessageLinkCheck.vue create mode 100644 utils/linkcheck/main.go create mode 100644 utils/linkcheck/status.go create mode 100644 utils/linkcheck/structs.go create mode 100644 utils/tools/html.go 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/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 {