diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c5d72d..3bf496e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ Notable changes to Mailpit will be documented in this file. +## 1.1.4 + +### Feature +- Add --quiet flag to display only errors + +### Security +- Add restrictive HTTP Content-Security-Policy + +### UI +- Minor UI color change & unread count position adjustment +- Add favicon unread message counter +- Remove left & right borders (message list) + + ## 1.1.3 ### Fix diff --git a/README.md b/README.md index f1a5406..1065b75 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Mailpit is inspired by [MailHog](#why-rewrite-mailhog), but much, much faster. - Runs entirely from a single binary, no installation required - 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`) +- Web UI to view emails (formatted HTML, highlighted HTML source, text, raw source and MIME attachments including image thumbnails) - 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) @@ -44,6 +44,8 @@ Or download a pre-built binary in the [releases](https://github.com/axllent/mail To build Mailpit from source see [building from source](https://github.com/axllent/mailpit/wiki/Building-from-source). +The Mailpit web UI listens by default on `http://0.0.0.0:8025`, and the SMTP port on `0.0.0.0:1025`. + ### Configuring sendmail @@ -65,6 +67,6 @@ You can build a Mailpit-specific sendmail binary from source (see [building from I had been using MailHog for a few years to intercept and test emails generated from several projects. MailHog has a number of severe performance issues, many of the modules are horribly out of date, and other than a few accepted MRs, it is not actively developed. -Initially I started trying to upgrade a fork of MailHog (both the UI as well as the HTTP server & API), but soon discovered that it is (with all due respect) very poorly designed. It is over-engineered (split over 9 separate projects), has too many unnecessary features for my purpose, and performs exceptionally poorly when dealing with large lumbers of emails or processing any email with an attachment (a single email with a 3MB attachment can take over a minute). The API transmits a lot of duplicate and unnecessary data on every message request for all web calls, and there is no HTTP compression. +Initially I started trying to upgrade a fork of MailHog (both the UI as well as the HTTP server & API), but soon discovered that it is (with all due respect) very poorly designed. It is over-engineered (split over 9 separate projects) and has too many unnecessary features for my purpose. It performs exceptionally poorly when dealing with large amounts of emails or processing any email with an attachment (a single email with a 3MB attachment can take over a minute to ingest). The API also transmits a lot of duplicate and unnecessary data on every message request for all web calls, and there is no 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 f5e25d3..4fafb67 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -136,6 +136,7 @@ func init() { rootCmd.Flags().StringVar(&config.SMTPSSLCert, "smtp-ssl-cert", config.SMTPSSLCert, "SSL certificate for SMTP - requires smtp-ssl-key") rootCmd.Flags().StringVar(&config.SMTPSSLKey, "smtp-ssl-key", config.SMTPSSLKey, "SSL key for SMTP - requires smtp-ssl-cert") + rootCmd.Flags().BoolVarP(&config.QuietLogging, "quiet", "q", false, "Quiet logging (errors only)") rootCmd.Flags().BoolVarP(&config.VerboseLogging, "verbose", "v", false, "Verbose logging") // deprecated 2022/08/06 diff --git a/config/config.go b/config/config.go index 0fb67db..ed199d3 100644 --- a/config/config.go +++ b/config/config.go @@ -26,6 +26,9 @@ var ( // VerboseLogging for console output VerboseLogging = false + // QuietLogging for console output (errors only) + QuietLogging = false + // NoLogging for tests NoLogging = false diff --git a/logger/logger.go b/logger/logger.go index aa0e30f..63ca810 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -19,9 +19,13 @@ func Log() *logrus.Logger { log = logrus.New() log.SetLevel(logrus.InfoLevel) if config.VerboseLogging { + // verbose logging (debug) log.SetLevel(logrus.DebugLevel) - } - if config.NoLogging { + } else if config.QuietLogging { + // show errors only + log.SetLevel(logrus.ErrorLevel) + } else if config.NoLogging { + // disable all logging (tests) log.SetLevel(logrus.PanicLevel) } diff --git a/package-lock.json b/package-lock.json index bba2966..3b686c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "bootstrap-icons": "^1.9.1", "moment": "^2.29.4", "prismjs": "^1.29.0", + "tinycon": "^0.6.8", "vue": "^3.2.13" }, "devDependencies": { @@ -1124,6 +1125,11 @@ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" }, + "node_modules/tinycon": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/tinycon/-/tinycon-0.6.8.tgz", + "integrity": "sha512-bF8Lxm4JUXF6Cw0XlZdugJ44GV575OinZ0Pt8vQPr8ooNqd2yyNkoFdCHzmdpHlgoqfSLfcyk4HDP1EyllT+ug==" + }, "node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -1902,6 +1908,11 @@ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" }, + "tinycon": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/tinycon/-/tinycon-0.6.8.tgz", + "integrity": "sha512-bF8Lxm4JUXF6Cw0XlZdugJ44GV575OinZ0Pt8vQPr8ooNqd2yyNkoFdCHzmdpHlgoqfSLfcyk4HDP1EyllT+ug==" + }, "tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", diff --git a/package.json b/package.json index e05fea0..01c3771 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "bootstrap-icons": "^1.9.1", "moment": "^2.29.4", "prismjs": "^1.29.0", + "tinycon": "^0.6.8", "vue": "^3.2.13" }, "devDependencies": { diff --git a/server/server.go b/server/server.go index 6a1d428..a53b663 100644 --- a/server/server.go +++ b/server/server.go @@ -21,6 +21,8 @@ import ( //go:embed ui var embeddedFS embed.FS +var contentSecurityPolicy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self'; img-src * data: blob:; font-src 'self' data:; media-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self';" + // Listen will start the httpd func Listen() { serverRoot, err := fs.Sub(embeddedFS, "ui") @@ -85,6 +87,9 @@ func (w gzipResponseWriter) Write(b []byte) (int, error) { // and gzip compression. func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Referrer-Policy", "no-referrer") + w.Header().Set("Content-Security-Policy", contentSecurityPolicy) + if config.UIAuthFile != "" { user, pass, ok := r.BasicAuth() @@ -115,6 +120,8 @@ func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc { // and gzip compression func middlewareHandler(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Referrer-Policy", "no-referrer") + w.Header().Set("Content-Security-Policy", contentSecurityPolicy) if config.UIAuthFile != "" { user, pass, ok := r.BasicAuth() @@ -143,6 +150,8 @@ func middlewareHandler(h http.Handler) http.Handler { // FourOFour returns a basic 404 message func fourOFour(w http.ResponseWriter) { + w.Header().Set("Referrer-Policy", "no-referrer") + w.Header().Set("Content-Security-Policy", contentSecurityPolicy) w.WriteHeader(http.StatusNotFound) w.Header().Set("Content-Type", "text/plain") fmt.Fprint(w, "404 page not found") @@ -150,6 +159,8 @@ func fourOFour(w http.ResponseWriter) { // HTTPError returns a basic error message (400 response) func httpError(w http.ResponseWriter, msg string) { + w.Header().Set("Referrer-Policy", "no-referrer") + w.Header().Set("Content-Security-Policy", contentSecurityPolicy) w.WriteHeader(http.StatusBadRequest) w.Header().Set("Content-Type", "text/plain") fmt.Fprint(w, msg) diff --git a/server/ui-src/App.vue b/server/ui-src/App.vue index dd9c297..b231ce4 100644 --- a/server/ui-src/App.vue +++ b/server/ui-src/App.vue @@ -1,7 +1,8 @@