1
0
mirror of https://github.com/axllent/mailpit.git synced 2025-03-17 21:18:19 +02:00

Merge branch 'release/1.1.4'

This commit is contained in:
Ralph Slooten 2022-09-15 21:54:16 +12:00
commit 9219b2d411
10 changed files with 83 additions and 11 deletions

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -26,6 +26,9 @@ var (
// VerboseLogging for console output
VerboseLogging = false
// QuietLogging for console output (errors only)
QuietLogging = false
// NoLogging for tests
NoLogging = false

View File

@ -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)
}

11
package-lock.json generated
View File

@ -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",

View File

@ -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": {

View File

@ -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)

View File

@ -1,7 +1,8 @@
<script>
import commonMixins from './mixins.js'
import commonMixins from './mixins.js';
import Message from './templates/Message.vue';
import moment from 'moment'
import moment from 'moment';
import Tinycon from 'tinycon';
export default {
mixins: [commonMixins],
@ -26,7 +27,8 @@ export default {
messageNext: false,
notificationsSupported: false,
notificationsEnabled: false,
selected: []
selected: [],
tcStatus: 0
}
},
watch: {
@ -36,6 +38,17 @@ export default {
} else {
this.message = false;
}
},
unread(v, old) {
if (v == this.tcStatus) {
return;
}
this.tcStatus = v;
if (v == 0) {
Tinycon.reset();
} else {
Tinycon.setBubble(v);
}
}
},
computed: {
@ -56,6 +69,12 @@ export default {
&& ("Notification" in window && Notification.permission !== "denied");
this.notificationsEnabled = this.notificationsSupported && Notification.permission == "granted";
Tinycon.setOptions({
height: 11,
background: '#dd0000',
fallback: false
});
this.connect();
this.loadMessages();
},
@ -192,6 +211,7 @@ export default {
let self = this;
let uri = 'api/delete'
self.get(uri, false, function(response) {
window.location.hash = "";
self.reloadMessages();
});
},
@ -206,7 +226,6 @@ export default {
window.location.hash = "";
self.scrollInPlace = true;
self.loadMessages();
});
},
@ -388,6 +407,12 @@ export default {
let selecting = false;
let lastSelected = this.selected.length > 0 && this.selected[this.selected.length - 1];
if (lastSelected == id) {
this.selected = this.selected.filter(function(ele){
return ele != id;
});
return
}
if (lastSelected === false) {
this.selected.push(id);
@ -508,7 +533,7 @@ export default {
<i class="bi bi-envelope me-1" v-if="isConnected"></i>
<i class="bi bi-arrow-clockwise me-1" v-else></i>
Inbox
<span style="margin-top: -5px; margin-left: 5px;" class="position-absolute badge rounded-pill text-bg-secondary" title="Unread messages" v-if="unread">
<span class="badge rounded-pill text-bg-primary ms-1" title="Unread messages" v-if="unread">
{{ formatNumber(unread) }}
</span>
</a>
@ -569,14 +594,13 @@ export default {
<div class="list-group" v-if="items.length">
<a v-for="message in items" :href="'#'+message.ID"
v-on:click.ctrl="toggleSelected($event, message.ID)" v-on:click.shift="selectRange($event, message.ID)"
class="row message d-flex small list-group-item list-group-item-action"
class="row message d-flex small list-group-item list-group-item-action border-start-0 border-end-0"
:class="message.Read ? 'read':'', isSelected(message.ID) ? 'selected':''">
<div class="col-lg-3">
<div class="d-lg-none float-end text-muted text-nowrap small">
<i class="bi bi-paperclip h6 me-1" v-if="message.Attachments"></i>
{{ getRelativeCreated(message) }}
</div>
<div class="text-truncate d-lg-none privacy">
<span v-if="message.From" :title="message.From.Address">{{ message.From.Name ? message.From.Name : message.From.Address }}</span>
</div>

View File

@ -1 +1,2 @@
$link-decoration: none;
$primary: #3465b5;