Skip to content

Commit 577461b

Browse files
committed
Merge branch 'release/v1.23.0'
2 parents c7d7810 + 289466b commit 577461b

File tree

16 files changed

+333
-246
lines changed

16 files changed

+333
-246
lines changed

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,28 @@
22

33
Notable changes to Mailpit will be documented in this file.
44

5+
## [v1.23.0]
6+
7+
### Feature
8+
- Add configuration to disable SQLite WAL mode for NFS compatibility
9+
- Add configuration to explicitly disable HTTP compression in web UI/API ([#448](https://github.com/axllent/mailpit/issues/448))
10+
- Add configuration to set message compression level in db (0-3) ([#447](https://github.com/axllent/mailpit/issues/447) & [#448](https://github.com/axllent/mailpit/issues/448))
11+
12+
### Chore
13+
- Update node dependencies
14+
- Update Go dependencies
15+
- Minor speed & memory improvements when storing messages
16+
- Optimize ZSTD encoder for fastest compression of messages ([#447](https://github.com/axllent/mailpit/issues/447))
17+
- Handle BLOB storage for default database differently to rqlite to reduce memory overhead ([#447](https://github.com/axllent/mailpit/issues/447))
18+
- Avoid shell in Docker health check ([#444](https://github.com/axllent/mailpit/issues/444))
19+
20+
### Fix
21+
- Display the correct STARTTLS or TLS runtime option on startup ([#446](https://github.com/axllent/mailpit/issues/446))
22+
23+
### Testing
24+
- Add tests for message compression levels
25+
26+
527
## [v1.22.3]
628

729
### Feature

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ RUN apk upgrade --no-cache && apk add --no-cache tzdata
2525

2626
EXPOSE 1025/tcp 1110/tcp 8025/tcp
2727

28-
HEALTHCHECK --interval=15s --start-period=10s --start-interval=1s CMD /mailpit readyz
28+
HEALTHCHECK --interval=15s --start-period=10s --start-interval=1s CMD ["/mailpit", "readyz"]
2929

3030
ENTRYPOINT ["/mailpit"]

cmd/root.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ func init() {
8383
initConfigFromEnv()
8484

8585
rootCmd.Flags().StringVarP(&config.Database, "database", "d", config.Database, "Database to store persistent data")
86+
rootCmd.Flags().BoolVar(&config.DisableWAL, "disable-wal", config.DisableWAL, "Disable WAL for local database (allows NFS mounted DBs)")
87+
rootCmd.Flags().IntVar(&config.Compression, "compression", config.Compression, "Compression level to store raw messages (0-3)")
8688
rootCmd.Flags().StringVar(&config.Label, "label", config.Label, "Optional label identify this Mailpit instance")
8789
rootCmd.Flags().StringVar(&config.TenantID, "tenant-id", config.TenantID, "Database tenant ID to isolate data")
8890
rootCmd.Flags().IntVarP(&config.MaxMessages, "max", "m", config.MaxMessages, "Max number of messages to store")
@@ -103,6 +105,7 @@ func init() {
103105
rootCmd.Flags().BoolVar(&config.BlockRemoteCSSAndFonts, "block-remote-css-and-fonts", config.BlockRemoteCSSAndFonts, "Block access to remote CSS & fonts")
104106
rootCmd.Flags().StringVar(&config.EnableSpamAssassin, "enable-spamassassin", config.EnableSpamAssassin, "Enable integration with SpamAssassin")
105107
rootCmd.Flags().BoolVar(&config.AllowUntrustedTLS, "allow-untrusted-tls", config.AllowUntrustedTLS, "Do not verify HTTPS certificates (link checker & screenshots)")
108+
rootCmd.Flags().BoolVar(&config.DisableHTTPCompression, "disable-http-compression", config.DisableHTTPCompression, "Disable HTTP compression support (web UI & API)")
106109

107110
// SMTP server
108111
rootCmd.Flags().StringVarP(&config.SMTPListen, "smtp", "s", config.SMTPListen, "SMTP bind interface and port")
@@ -181,6 +184,12 @@ func initConfigFromEnv() {
181184
config.Database = os.Getenv("MP_DATABASE")
182185
}
183186

187+
config.DisableWAL = getEnabledFromEnv("MP_DISABLE_WAL")
188+
189+
if len(os.Getenv("MP_COMPRESSION")) > 0 {
190+
config.Compression, _ = strconv.Atoi(os.Getenv("MP_COMPRESSION"))
191+
}
192+
184193
config.TenantID = os.Getenv("MP_TENANT_ID")
185194

186195
config.Label = os.Getenv("MP_LABEL")
@@ -232,6 +241,9 @@ func initConfigFromEnv() {
232241
if getEnabledFromEnv("MP_ALLOW_UNTRUSTED_TLS") {
233242
config.AllowUntrustedTLS = true
234243
}
244+
if getEnabledFromEnv("MP_DISABLE_HTTP_COMPRESSION") {
245+
config.DisableHTTPCompression = true
246+
}
235247

236248
// SMTP server
237249
if len(os.Getenv("MP_SMTP_BIND_ADDR")) > 0 {

config/config.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ var (
2828
// Database for mail (optional)
2929
Database string
3030

31+
// DisableWAL will disable Write-Ahead Logging in SQLite
32+
// @see https://sqlite.org/wal.html
33+
DisableWAL bool
34+
35+
// Compression is the compression level used to store raw messages in the database:
36+
// 0 = off, 1 = fastest (default), 2 = standard, 3 = best compression
37+
Compression = 1
38+
3139
// TenantID is an optional prefix to be applied to all database tables,
3240
// allowing multiple isolated instances of Mailpit to share a database.
3341
TenantID string
@@ -61,6 +69,9 @@ var (
6169
// Webroot to define the base path for the UI and API
6270
Webroot = "/"
6371

72+
// DisableHTTPCompression will explicitly disable HTTP compression in the web UI and API
73+
DisableHTTPCompression bool
74+
6475
// SMTPTLSCert file
6576
SMTPTLSCert string
6677

@@ -250,6 +261,10 @@ func VerifyConfig() error {
250261
Database = filepath.Join(Database, "mailpit.db")
251262
}
252263

264+
if Compression < 0 || Compression > 3 {
265+
return errors.New("[db] compression level must be between 0 and 3")
266+
}
267+
253268
Label = tools.Normalize(Label)
254269

255270
if err := parseMaxAge(); err != nil {

go.mod

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,22 @@ require (
1212
github.com/gorilla/mux v1.8.1
1313
github.com/gorilla/websocket v1.5.3
1414
github.com/jhillyerd/enmime v1.3.0
15-
github.com/klauspost/compress v1.17.11
15+
github.com/klauspost/compress v1.18.0
1616
github.com/kovidgoyal/imaging v1.6.4
1717
github.com/leporo/sqlf v1.4.0
1818
github.com/lithammer/shortuuid/v4 v4.2.0
1919
github.com/mneis/go-telnet v0.0.0-20221017141824-6f643e477c62
2020
github.com/rqlite/gorqlite v0.0.0-20250128004930-114c7828b55a
2121
github.com/sirupsen/logrus v1.9.3
22-
github.com/spf13/cobra v1.9.0
22+
github.com/spf13/cobra v1.9.1
2323
github.com/spf13/pflag v1.0.6
2424
github.com/tg123/go-htpasswd v1.2.3
2525
github.com/vanng822/go-premailer v1.23.0
2626
golang.org/x/net v0.35.0
2727
golang.org/x/text v0.22.0
2828
golang.org/x/time v0.10.0
2929
gopkg.in/yaml.v3 v3.0.1
30-
modernc.org/sqlite v1.35.0
30+
modernc.org/sqlite v1.36.0
3131
)
3232

3333
require (
@@ -52,8 +52,8 @@ require (
5252
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
5353
github.com/valyala/bytebufferpool v1.0.0 // indirect
5454
github.com/vanng822/css v1.0.1 // indirect
55-
golang.org/x/crypto v0.33.0 // indirect
56-
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f // indirect
55+
golang.org/x/crypto v0.35.0 // indirect
56+
golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 // indirect
5757
golang.org/x/image v0.24.0 // indirect
5858
golang.org/x/sys v0.30.0 // indirect
5959
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect

go.sum

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQykt
4343
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
4444
github.com/jhillyerd/enmime v1.3.0 h1:LV5kzfLidiOr8qRGIpYYmUZCnhrPbcFAnAFUnWn99rw=
4545
github.com/jhillyerd/enmime v1.3.0/go.mod h1:6c6jg5HdRRV2FtvVL69LjiX1M8oE0xDX9VEhV3oy4gs=
46-
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
47-
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
46+
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
47+
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
4848
github.com/kovidgoyal/imaging v1.6.4 h1:K0idhRPXnRrJBKnBYcTfI1HTWSNDeAn7hYDvf9I0dCk=
4949
github.com/kovidgoyal/imaging v1.6.4/go.mod h1:bEIgsaZmXlvFfkv/CUxr9rJook6AQkJnpB5EPosRfRY=
5050
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -92,8 +92,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
9292
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
9393
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
9494
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
95-
github.com/spf13/cobra v1.9.0 h1:Py5fIuq/lJsRYxcxfOtsJqpmwJWCMOUy2tMJYV8TNHE=
96-
github.com/spf13/cobra v1.9.0/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
95+
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
96+
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
9797
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
9898
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
9999
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
@@ -128,10 +128,10 @@ golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+
128128
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
129129
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
130130
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
131-
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
132-
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
133-
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f h1:oFMYAjX0867ZD2jcNiLBrI9BdpmEkvPyi5YrBGXbamg=
134-
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
131+
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
132+
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
133+
golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 h1:aWwlzYV971S4BXRS9AmqwDLAD85ouC6X+pocatKY58c=
134+
golang.org/x/exp v0.0.0-20250228200357-dead58393ab7/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
135135
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
136136
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
137137
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -244,8 +244,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
244244
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
245245
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
246246
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
247-
modernc.org/sqlite v1.35.0 h1:yQps4fegMnZFdphtzlfQTCNBWtS0CZv48pRpW3RFHRw=
248-
modernc.org/sqlite v1.35.0/go.mod h1:9cr2sicr7jIaWTBKQmAxQLfBv9LL0su4ZTEV+utt3ic=
247+
modernc.org/sqlite v1.36.0 h1:EQXNRn4nIS+gfsKeUTymHIz1waxuv5BzU7558dHSfH8=
248+
modernc.org/sqlite v1.36.0/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU=
249249
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
250250
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
251251
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

internal/dump/dump.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func Sync(d string) error {
5050
}
5151

5252
if !tools.IsDir(outDir) {
53-
if err := os.MkdirAll(outDir, 0755); err != nil {
53+
if err := os.MkdirAll(outDir, 0755); /* #nosec */ err != nil {
5454
return err
5555
}
5656
}
@@ -149,7 +149,7 @@ func saveMessages() error {
149149
}
150150
}
151151

152-
if err := os.WriteFile(out, b, 0644); err != nil {
152+
if err := os.WriteFile(out, b, 0644); /* #nosec */ err != nil {
153153
logger.Log().Errorf("Error writing message %s: %s", m.ID, err.Error())
154154
continue
155155
}

internal/smtpd/main.go

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) (string
3434

3535
// SaveToDatabase will attempt to save a message to the database
3636
func SaveToDatabase(origin net.Addr, from string, to []string, data []byte) (string, error) {
37-
if !config.SMTPStrictRFCHeaders {
37+
if !config.SMTPStrictRFCHeaders && bytes.Contains(data, []byte("\r\r\n")) {
3838
// replace all <CR><CR><LF> (\r\r\n) with <CR><LF> (\r\n)
3939
// @see https://github.com/axllent/mailpit/issues/87 & https://github.com/axllent/mailpit/issues/153
4040
data = bytes.ReplaceAll(data, []byte("\r\r\n"), []byte("\r\n"))
@@ -50,21 +50,9 @@ func SaveToDatabase(origin net.Addr, from string, to []string, data []byte) (str
5050
// check / set the Return-Path based on SMTP from
5151
returnPath := strings.Trim(msg.Header.Get("Return-Path"), "<>")
5252
if returnPath != from {
53-
if returnPath != "" {
54-
// replace Return-Path
55-
re := regexp.MustCompile(`(?i)(^|\n)(Return\-Path: .*\n)`)
56-
replaced := false
57-
data = re.ReplaceAllFunc(data, func(r []byte) []byte {
58-
if replaced {
59-
return r
60-
}
61-
replaced = true // only replace first occurrence
62-
63-
return re.ReplaceAll(r, []byte("${1}Return-Path: <"+from+">\r\n"))
64-
})
65-
} else {
66-
// add Return-Path
67-
data = append([]byte("Return-Path: <"+from+">\r\n"), data...)
53+
data, err = tools.SetMessageHeader(data, "Return-Path", "<"+from+">")
54+
if err != nil {
55+
return "", err
6856
}
6957
}
7058

@@ -108,23 +96,15 @@ func SaveToDatabase(origin net.Addr, from string, to []string, data []byte) (str
10896

10997
// add missing email addresses to Bcc (eg: Laravel doesn't include these in the headers)
11098
if len(missingAddresses) > 0 {
99+
bccVal := strings.Join(missingAddresses, ", ")
111100
if hasBccHeader {
112-
// email already has Bcc header, add to existing addresses
113-
re := regexp.MustCompile(`(?i)(^|\n)(Bcc: )`)
114-
replaced := false
115-
data = re.ReplaceAllFunc(data, func(r []byte) []byte {
116-
if replaced {
117-
return r
118-
}
119-
replaced = true // only replace first occurrence
120-
121-
return re.ReplaceAll(r, []byte("${1}Bcc: "+strings.Join(missingAddresses, ", ")+", "))
122-
})
101+
b := msg.Header.Get("Bcc")
102+
bccVal = ", " + b
103+
}
123104

124-
} else {
125-
// prepend new Bcc header
126-
bcc := []byte(fmt.Sprintf("Bcc: %s\r\n", strings.Join(missingAddresses, ", ")))
127-
data = append(bcc, data...)
105+
data, err = tools.SetMessageHeader(data, "Bcc", bccVal)
106+
if err != nil {
107+
return "", err
128108
}
129109

130110
logger.Log().Debugf("[smtpd] added missing addresses to Bcc header: %s", strings.Join(missingAddresses, ", "))
@@ -282,10 +262,10 @@ func listenAndServe(addr string, handler MsgIDHandler, authHandler AuthHandler)
282262
smtpType := "no encryption"
283263

284264
if config.SMTPTLSCert != "" {
285-
if config.SMTPRequireSTARTTLS {
286-
smtpType = "STARTTLS required"
287-
} else if config.SMTPRequireTLS {
265+
if config.SMTPRequireTLS {
288266
smtpType = "SSL/TLS required"
267+
} else if config.SMTPRequireSTARTTLS {
268+
smtpType = "STARTTLS required"
289269
} else {
290270
smtpType = "STARTTLS optional"
291271
if !config.SMTPAuthAllowInsecure && auth.SMTPCredentials != nil {

internal/storage/database.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,39 @@ var (
3232
dbLastAction time.Time
3333

3434
// zstd compression encoder & decoder
35-
dbEncoder, _ = zstd.NewWriter(nil)
35+
dbEncoder *zstd.Encoder
3636
dbDecoder, _ = zstd.NewReader(nil)
3737

3838
temporaryFiles = []string{}
3939
)
4040

4141
// InitDB will initialise the database
4242
func InitDB() error {
43+
// dbEncoder
4344
var (
4445
dsn string
4546
err error
4647
)
4748

49+
if config.Compression > 0 {
50+
var compression zstd.EncoderLevel
51+
switch config.Compression {
52+
case 1:
53+
compression = zstd.SpeedFastest
54+
case 2:
55+
compression = zstd.SpeedDefault
56+
case 3:
57+
compression = zstd.SpeedBestCompression
58+
}
59+
dbEncoder, err = zstd.NewWriter(nil, zstd.WithEncoderLevel(compression))
60+
if err != nil {
61+
return err
62+
}
63+
logger.Log().Debugf("[db] storing messages with compression: %s", compression.String())
64+
} else {
65+
logger.Log().Debug("[db] storing messages with no compression")
66+
}
67+
4868
p := config.Database
4969

5070
if p == "" {
@@ -100,8 +120,13 @@ func InitDB() error {
100120
db.SetMaxOpenConns(1)
101121

102122
if sqlDriver == "sqlite" {
103-
// SQLite performance tuning (https://phiresky.github.io/blog/2020/sqlite-performance-tuning/)
104-
_, err = db.Exec("PRAGMA journal_mode = WAL; PRAGMA synchronous = normal;")
123+
if config.DisableWAL {
124+
// disable WAL mode for SQLite, allows NFS mounted DBs
125+
_, err = db.Exec("PRAGMA journal_mode=DELETE; PRAGMA synchronous=NORMAL;")
126+
} else {
127+
// SQLite performance tuning (https://phiresky.github.io/blog/2020/sqlite-performance-tuning/)
128+
_, err = db.Exec("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")
129+
}
105130
if err != nil {
106131
return err
107132
}

0 commit comments

Comments
 (0)