1
0
mirror of https://github.com/axllent/mailpit.git synced 2025-08-13 20:04:49 +02:00

Merge branch 'release/v1.27.2'

This commit is contained in:
Ralph Slooten
2025-07-27 12:36:30 +12:00
34 changed files with 1029 additions and 3604 deletions

View File

@@ -2,6 +2,28 @@
Notable changes to Mailpit will be documented in this file.
## [v1.27.2]
### Feature
- Add ability to generate self-signed (snakeoil) certificates for UI, SMTP and POP3 ([#539](https://github.com/axllent/mailpit/issues/539))
### Chore
- Allow sendmail to send to untrusted TLS server
- Update eslint config, remove neostandard
- Refactor JS functions and remove unused parameters
- Update Go dependencies
- Update node dependencies
### Fix
- Use MaxMessages to determine pruning ([#536](https://github.com/axllent/mailpit/issues/536))
- Support angle brackets for text/plain URLs with spaces ([#535](https://github.com/axllent/mailpit/issues/535))
- Do not check latest release for Prometheus statistics ([#522](https://github.com/axllent/mailpit/issues/522))
### Security
- Prevent integer overflow conversion to uint64
- Add ReadHeaderTimeout to Prometheus metrics server
## [v1.27.1]
### Chore

View File

@@ -15,6 +15,7 @@ import (
"github.com/axllent/mailpit/internal/auth"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/smtpd/chaos"
"github.com/axllent/mailpit/internal/snakeoil"
"github.com/axllent/mailpit/internal/spamassassin"
"github.com/axllent/mailpit/internal/tools"
)
@@ -333,8 +334,19 @@ func VerifyConfig() error {
}
if UITLSCert != "" {
UITLSCert = filepath.Clean(UITLSCert)
UITLSKey = filepath.Clean(UITLSKey)
if strings.HasPrefix(UITLSCert, "sans:") {
// generate a self-signed certificate
UITLSCert = snakeoil.Public(UITLSCert)
} else {
UITLSCert = filepath.Clean(UITLSCert)
}
if strings.HasPrefix(UITLSKey, "sans:") {
// generate a self-signed key
UITLSKey = snakeoil.Private(UITLSKey)
} else {
UITLSKey = filepath.Clean(UITLSKey)
}
if !isFile(UITLSCert) {
return fmt.Errorf("[ui] TLS certificate not found or readable: %s", UITLSCert)
@@ -393,8 +405,19 @@ func VerifyConfig() error {
}
if SMTPTLSCert != "" {
SMTPTLSCert = filepath.Clean(SMTPTLSCert)
SMTPTLSKey = filepath.Clean(SMTPTLSKey)
if strings.HasPrefix(SMTPTLSCert, "sans:") {
// generate a self-signed certificate
SMTPTLSCert = snakeoil.Public(SMTPTLSCert)
} else {
SMTPTLSCert = filepath.Clean(SMTPTLSCert)
}
if strings.HasPrefix(SMTPTLSKey, "sans:") {
// generate a self-signed key
SMTPTLSKey = snakeoil.Private(SMTPTLSKey)
} else {
SMTPTLSKey = filepath.Clean(SMTPTLSKey)
}
if !isFile(SMTPTLSCert) {
return fmt.Errorf("[smtp] TLS certificate not found or readable: %s", SMTPTLSCert)
@@ -462,8 +485,18 @@ func VerifyConfig() error {
// POP3 server
if POP3TLSCert != "" {
POP3TLSCert = filepath.Clean(POP3TLSCert)
POP3TLSKey = filepath.Clean(POP3TLSKey)
if strings.HasPrefix(POP3TLSCert, "sans:") {
// generate a self-signed certificate
POP3TLSCert = snakeoil.Public(POP3TLSCert)
} else {
POP3TLSCert = filepath.Clean(POP3TLSCert)
}
if strings.HasPrefix(POP3TLSKey, "sans:") {
// generate a self-signed key
POP3TLSKey = snakeoil.Private(POP3TLSKey)
} else {
POP3TLSKey = filepath.Clean(POP3TLSKey)
}
if !isFile(POP3TLSCert) {
return fmt.Errorf("[pop3] TLS certificate not found or readable: %s", POP3TLSCert)

View File

@@ -1,34 +1,76 @@
import eslintConfigPrettier from "eslint-config-prettier/flat";
import neostandard, { resolveIgnoresFromGitignore } from "neostandard";
import globals from "globals";
import { includeIgnoreFile } from "@eslint/compat";
import js from "@eslint/js";
import vue from "eslint-plugin-vue";
import { fileURLToPath } from "node:url";
const gitignorePath = fileURLToPath(new URL(".gitignore", import.meta.url));
export default [
/* Baseline JS rules, provided by Neostandard */
...neostandard({
/* Allows references to browser APIs like `document` */
env: ["browser"],
/* Use .gitignore to prevent linting of irrelevant files */
includeIgnoreFile(gitignorePath, ".gitignore"),
/* We rely on .gitignore to avoid running against dist / dependency files */
ignores: resolveIgnoresFromGitignore(),
/* Disables a range of style-related rules, as we use Prettier for that */
noStyle: true,
/* Ensures we only lint JS and Vue files */
/* ESLint's recommended rules */
{
files: ["**/*.js", "**/*.vue"],
}),
languageOptions: { globals: { ...globals.browser, ...globals.node } },
rules: js.configs.recommended.rules,
},
/* Vue-specific rules */
...vue.configs["flat/recommended"],
/* Prettier is responsible for formatting, so this disables any conflicting rules */
/* Prettier is responsible for formatting, so we disable conflicting rules */
eslintConfigPrettier,
/* Our custom rules */
{
rules: {
/* We prefer arrow functions for tidiness and consistency */
/* Always use arrow functions for tidiness and consistency */
"prefer-arrow-callback": "error",
/* Always use camelCase for variable names */
camelcase: [
"error",
{
ignoreDestructuring: false,
ignoreGlobals: true,
ignoreImports: false,
properties: "never",
},
],
/* The default case in switch statements must always be last */
"default-case-last": "error",
/* Always use dot notation where possible (e.g. `obj.val` over `obj['val']`) */
"dot-notation": "error",
/* Always use `===` and `!==` for comparisons unless unambiguous */
eqeqeq: ["error", "smart"],
/* Never use `eval()` as it violates our CSP and can lead to security issues */
"no-eval": "error",
"no-implied-eval": "error",
/* Prevents accidental use of template literals in plain strings, e.g. "my ${var}" */
"no-template-curly-in-string": "error",
/* Avoid unnecessary ternary operators */
"no-unneeded-ternary": "error",
/* Avoid unused expressions that have no purpose */
"no-unused-expressions": "error",
/* Always use `const` or `let` to make scope behaviour clear */
"no-var": "error",
/* Always use shorthand syntax for objects where possible, e.g. { a, b() { } } */
"object-shorthand": "error",
/* Always use `const` for variables that are never reassigned */
"prefer-const": "error",
},
},
];

20
go.mod
View File

@@ -21,14 +21,14 @@ require (
github.com/rqlite/gorqlite v0.0.0-20250609141355-ac86a4a1c9a8
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
github.com/spf13/pflag v1.0.7
github.com/tg123/go-htpasswd v1.2.4
github.com/vanng822/go-premailer v1.25.0
golang.org/x/crypto v0.39.0
golang.org/x/net v0.41.0
golang.org/x/text v0.26.0
golang.org/x/crypto v0.40.0
golang.org/x/net v0.42.0
golang.org/x/text v0.27.0
golang.org/x/time v0.12.0
modernc.org/sqlite v1.38.0
modernc.org/sqlite v1.38.1
)
require (
@@ -62,12 +62,12 @@ require (
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/vanng822/css v1.0.1 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/image v0.28.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
golang.org/x/image v0.29.0 // indirect
golang.org/x/mod v0.26.0 // indirect
golang.org/x/sys v0.34.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
modernc.org/libc v1.66.2 // indirect
modernc.org/libc v1.66.4 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)

51
go.sum
View File

@@ -110,8 +110,9 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -138,19 +139,19 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4=
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -160,8 +161,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -169,8 +170,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -184,8 +185,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -204,8 +205,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -214,8 +215,8 @@ 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.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
@@ -231,10 +232,10 @@ modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.1.2 h1:9mfG19tFBypPnlSKRAjI5nXGMLmVy+jLyKNVKsMzt/8=
modernc.org/goabi0 v0.1.2/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.2 h1:JCBxlJzZOIwZY54fzjHN3Wsn8Ty5PUTPr/xioRkmecI=
modernc.org/libc v1.66.2/go.mod h1:ceIGzvXxP+JV3pgVjP9avPZo6Chlsfof2egXBH3YT5Q=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.4 h1:EW4EaqAVngI6f5KWiFibu41IYFMv/F7KEtR+NRHrS/Q=
modernc.org/libc v1.66.4/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -243,8 +244,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
modernc.org/sqlite v1.38.1 h1:jNnIjleVta+DKSAr3TnkKK87EEhjPhBLzi6hvIX9Bas=
modernc.org/sqlite v1.38.1/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@@ -31,7 +31,14 @@ var (
</html>`
expectedHTMLLinks = []string{
"http://example.com", "https://example.com", "HTTPS://EXAMPLE.COM", "http://localhost", "https://localhost", "https://127.0.0.1", "http://link with spaces", "http://example.com/?blaah=yes&test=true",
"http://example.com",
"https://example.com",
"HTTPS://EXAMPLE.COM",
"http://localhost",
"https://localhost",
"https://127.0.0.1",
"http://link with spaces",
"http://example.com/?blaah=yes&test=true",
"http://remote-host/style.css", // css
"https://example.com/image.jpg", // images
}
@@ -41,10 +48,18 @@ var (
[http://localhost]
www.google.com < ignored
|||http://example.com/?some=query-string|||
// RFC2396 appendix E states angle brackets are recommended for text/plain emails to
// recognize potential spaces in between the URL
<https://example.com/ link with spaces>
`
expectedTextLinks = []string{
"http://example.com", "https://example.com", "HTTPS://EXAMPLE.COM", "http://localhost", "http://example.com/?some=query-string",
"http://example.com",
"https://example.com",
"HTTPS://EXAMPLE.COM",
"http://localhost",
"http://example.com/?some=query-string",
"https://example.com/ link with spaces",
}
)

View File

@@ -30,9 +30,28 @@ func RunTests(msg *storage.Message, followRedirects bool) (Response, error) {
}
func extractTextLinks(msg *storage.Message) []string {
testLinkRe := regexp.MustCompile(`(?im)([^<]\b)((http|https):\/\/([\-\w@:%_\+'!.~#?,&\/\/=;]+))`)
// RFC2396 appendix E states angle brackets are recommended for text/plain emails to
// recognize potential spaces in between the URL
// @see https://www.rfc-editor.org/rfc/rfc2396#appendix-E
bracketLinkRe := regexp.MustCompile(`(?im)<((http|https):\/\/([\-\w@:%_\+'!.~#?,&\/\/=;][^>]+))>`)
links := []string{}
links = append(links, linkRe.FindAllString(msg.Text, -1)...)
matches := testLinkRe.FindAllStringSubmatch(msg.Text, -1)
for _, match := range matches {
if len(match) > 0 {
links = append(links, match[2])
}
}
angleMatches := bracketLinkRe.FindAllStringSubmatch(msg.Text, -1)
for _, match := range angleMatches {
if len(match) > 0 {
link := strings.ReplaceAll(match[1], "\n", "")
links = append(links, link)
}
}
return links
}

View File

@@ -215,7 +215,7 @@ func handleTransactionCommand(conn net.Conn, cmd string, args []string, messages
for _, m := range messages {
totalSize += m.Size
}
sendResponse(conn, fmt.Sprintf("+OK %d %d", len(messages), int64(totalSize)))
sendResponse(conn, fmt.Sprintf("+OK %d %d", len(messages), totalSize))
case "LIST":
totalSize := uint64(0)
for _, m := range messages {
@@ -229,12 +229,12 @@ func handleTransactionCommand(conn net.Conn, cmd string, args []string, messages
sendResponse(conn, "-ERR no such message")
return
}
sendResponse(conn, fmt.Sprintf("+OK %d %d", nr, int64(messages[nr-1].Size)))
sendResponse(conn, fmt.Sprintf("+OK %d %d", nr, messages[nr-1].Size))
} else {
sendResponse(conn, fmt.Sprintf("+OK %d messages (%d octets)", len(messages), int64(totalSize)))
sendResponse(conn, fmt.Sprintf("+OK %d messages (%d octets)", len(messages), totalSize))
for row, m := range messages {
sendResponse(conn, fmt.Sprintf("%d %d", row+1, int64(m.Size))) // Convert Size to int64 when printing
sendResponse(conn, fmt.Sprintf("%d %d", row+1, m.Size))
}
sendResponse(conn, ".")
}

View File

@@ -32,7 +32,7 @@ var (
)
// InitMetrics initializes all Prometheus metrics
func InitMetrics() {
func initMetrics() {
// Create metrics
totalMessages = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "mailpit_messages",
@@ -107,8 +107,8 @@ func InitMetrics() {
}
// UpdateMetrics updates all metrics with current values
func UpdateMetrics() {
info := stats.Load()
func updateMetrics() {
info := stats.Load(false)
totalMessages.Set(float64(info.Messages))
unreadMessages.Set(float64(info.Unread))
@@ -130,7 +130,7 @@ func UpdateMetrics() {
}
}
// Returns the Prometheus handler & disables double compression in middleware
// GetHandler returns the Prometheus handler & disables double compression in middleware
func GetHandler() http.Handler {
return promhttp.HandlerFor(Registry, promhttp.HandlerOpts{
DisableCompression: true,
@@ -139,8 +139,8 @@ func GetHandler() http.Handler {
// StartUpdater starts the periodic metrics update routine
func StartUpdater() {
InitMetrics()
UpdateMetrics()
initMetrics()
updateMetrics()
// Start periodic updates
go func() {
@@ -148,7 +148,7 @@ func StartUpdater() {
defer ticker.Stop()
for range ticker.C {
UpdateMetrics()
updateMetrics()
}
}()
}
@@ -165,8 +165,9 @@ func StartSeparateServer() {
// Create a dedicated server instance
server := &http.Server{
Addr: config.PrometheusListen,
Handler: mux,
Addr: config.PrometheusListen,
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
}
// Start HTTP server

View File

@@ -0,0 +1,196 @@
// Package snakeoil provides functionality to generate a temporary self-signed certificates
// for testing purposes. It generates a public and private key pair, stores them in the
// OS's temporary directory, returning the paths to these files.
package snakeoil
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/pem"
"errors"
"math/big"
"os"
"strings"
"time"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/tools"
)
var keys = make(map[string]KeyPair)
// KeyPair holds the public and private key paths for a self-signed certificate.
type KeyPair struct {
Public string
Private string
}
// Certificates returns all configured self-signed certificates in use,
// used for file deletion on exit.
func Certificates() map[string]KeyPair {
return keys
}
// Public returns the path to a generated PEM-encoded RSA public key.
func Public(str string) string {
domains, key, err := parse(str)
if err != nil {
logger.Log().Errorf("[tls] failed to parse domains: %v", err)
return ""
}
if pair, ok := keys[key]; ok {
return pair.Public
}
private, public, err := generate(domains)
if err != nil {
logger.Log().Errorf("[tls] failed to generate public certificate: %v", err)
return ""
}
keys[key] = KeyPair{
Public: public,
Private: private,
}
return public
}
// Private returns the path to a generated PEM-encoded RSA private key.
func Private(str string) string {
domains, key, err := parse(str)
if err != nil {
logger.Log().Errorf("[tls] failed to parse domains: %v", err)
return ""
}
if pair, ok := keys[key]; ok {
return pair.Private
}
private, public, err := generate(domains)
if err != nil {
logger.Log().Errorf("[tls] failed to generate public certificate: %v", err)
return ""
}
keys[key] = KeyPair{
Public: public,
Private: private,
}
return private
}
// Parse takes the original string input, removes the "sans:" prefix,
// splits the result into individual domains, and returns a slice of unique domains,
// along with a unique key that is a comma-separated list of these domains.
func parse(str string) ([]string, string, error) {
// remove "sans:" prefix
str = str[5:]
var domains []string
// split the string by commas and trim whitespace
for domain := range strings.SplitSeq(str, ",") {
domain = strings.ToLower(strings.TrimSpace(domain))
if domain != "" && !tools.InArray(domain, domains) {
domains = append(domains, domain)
}
}
if len(domains) == 0 {
return domains, "", errors.New("no valid domains provided")
}
// generate sha256 hash of the domains to create a unique key
hasher := sha256.New()
hasher.Write([]byte(strings.Join(domains, ",")))
key := base64.URLEncoding.EncodeToString(hasher.Sum(nil))
return domains, key, nil
}
// Generate a new self-signed certificate and return a public & private key paths.
func generate(domains []string) (string, string, error) {
logger.Log().Infof("[tls] generating temp self-signed certificate for: %s", strings.Join(domains, ","))
key, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return "", "", err
}
keyBytes := x509.MarshalPKCS1PrivateKey(key)
// PEM encoding of private key
keyPEM := pem.EncodeToMemory(
&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: keyBytes,
},
)
notBefore := time.Now()
notAfter := notBefore.Add(365 * 24 * time.Hour)
// create certificate template
template := x509.Certificate{
SerialNumber: big.NewInt(0),
Subject: pkix.Name{
CommonName: domains[0],
Organization: []string{"Mailpit self-signed certificate"},
},
DNSNames: domains,
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: notBefore,
NotAfter: notAfter,
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement | x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
}
// create certificate using template
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
return "", "", err
}
// PEM encoding of certificate
certPem := pem.EncodeToMemory(
&pem.Block{
Type: "CERTIFICATE",
Bytes: derBytes,
},
)
// Store the paths to the generated keys
priv, err := os.CreateTemp("", ".mailpit-*-private.pem")
if err != nil {
return "", "", err
}
if _, err := priv.Write(keyPEM); err != nil {
return "", "", err
}
if err := priv.Close(); err != nil {
return "", "", err
}
pub, err := os.CreateTemp("", ".mailpit-*-public.pem")
if err != nil {
return "", "", err
}
if _, err := pub.Write(certPem); err != nil {
return "", "", err
}
if err := pub.Close(); err != nil {
return "", "", err
}
return priv.Name(), pub.Name(), nil
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/storage"
"github.com/axllent/mailpit/internal/tools"
)
// Stores cached version along with its expiry time and error count.
@@ -81,7 +82,7 @@ func getBackoff(errCount int) time.Duration {
}
// Load the current statistics
func Load() AppInformation {
func Load(detectLatestVersion bool) AppInformation {
info := AppInformation{}
info.Version = config.Version
@@ -98,7 +99,7 @@ func Load() AppInformation {
if config.DisableVersionCheck {
info.LatestVersion = "disabled"
} else {
} else if detectLatestVersion {
mu.RLock()
cacheValid := time.Now().Before(vCache.expiry)
cacheValue := vCache.value
@@ -146,7 +147,7 @@ func Track() {
func LogSMTPAccepted(size int) {
mu.Lock()
smtpAccepted = smtpAccepted + 1
smtpAcceptedSize = smtpAcceptedSize + uint64(size)
smtpAcceptedSize = smtpAcceptedSize + tools.SafeUint64(size)
mu.Unlock()
}

View File

@@ -60,20 +60,19 @@ func pruneMessages() {
var size float64 // use float64 for rqlite compatibility
// prune using `--max` if set
if config.MaxMessages > 0 {
total := CountTotal()
if total > uint64(config.MaxAgeInHours) {
offset := config.MaxMessages
if config.DemoMode {
offset = 500
}
q := sqlf.Select("ID, Size").
From(tenant("mailbox")).
OrderBy("Created DESC").
Limit(5000).
Offset(offset)
if config.MaxMessages > 0 && CountTotal() > uint64(config.MaxMessages) {
offset := config.MaxMessages
if config.DemoMode {
offset = 500
}
q := sqlf.Select("ID, Size").
From(tenant("mailbox")).
OrderBy("Created DESC").
Limit(5000).
Offset(offset)
if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
if err := q.QueryAndClose(
context.TODO(), db, func(row *sql.Rows) {
var id string
if err := row.Scan(&id, &size); err != nil {
@@ -83,10 +82,10 @@ func pruneMessages() {
ids = append(ids, id)
prunedSize = prunedSize + uint64(size)
}); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return
}
},
); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return
}
}

View File

@@ -108,7 +108,7 @@ func Store(body *[]byte, username *string) (string, error) {
if config.Compression > 0 {
// insert compressed raw message
compressed := dbEncoder.EncodeAll(*body, make([]byte, 0, int(size)))
compressed := dbEncoder.EncodeAll(*body, make([]byte, 0, size))
if sqlDriver == "rqlite" {
// rqlite does not support binary data in query, so we need to encode the compressed message into hexadecimal
@@ -202,7 +202,7 @@ func Store(body *[]byte, username *string) (string, error) {
BroadcastMailboxStats()
logger.Log().Debugf("[db] saved message %s (%d bytes)", id, int64(size))
logger.Log().Debugf("[db] saved message %s (%d bytes)", id, size)
return id, nil
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/axllent/mailpit/internal/html2text"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/tools"
"github.com/jhillyerd/enmime/v2"
)
@@ -88,7 +89,7 @@ func cleanString(str string) string {
// LogMessagesDeleted logs the number of messages deleted
func logMessagesDeleted(n int) {
mu.Lock()
StatsDeleted = StatsDeleted + uint64(n)
StatsDeleted = StatsDeleted + tools.SafeUint64(n)
mu.Unlock()
}

View File

@@ -36,3 +36,22 @@ func Normalize(s string) string {
return strings.TrimSpace(s)
}
// SafeUint64 converts an int or int64 to uint64, ensuring it does not exceed the maximum value for uint64.
func SafeUint64(i any) uint64 {
switch v := i.(type) {
case int:
if v < 0 {
return 0
}
return uint64(v)
case int64:
if v < 0 {
return 0
}
return uint64(v)
default:
// only accepts int or int64
return 0
}
}

3913
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@
"lint-fix": "eslint --fix && prettier --write ."
},
"dependencies": {
"axios": "^1.2.1",
"axios": "^1.11.0",
"bootstrap": "^5.2.0",
"bootstrap-icons": "^1.9.1",
"bootstrap5-tags": "^1.6.1",
@@ -30,6 +30,7 @@
"vue-router": "^4.2.4"
},
"devDependencies": {
"@eslint/compat": "^1.3.1",
"@popperjs/core": "^2.11.5",
"@types/bootstrap": "^5.2.7",
"@types/tinycon": "^0.6.3",
@@ -40,10 +41,9 @@
"eslint": "^9.29.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-vue": "^10.2.0",
"neostandard": "^0.12.1",
"prettier": "^3.5.3"
},
"prettier":{
"prettier": {
"tabWidth": 4,
"useTabs": true,
"printWidth": 120

View File

@@ -2,11 +2,15 @@
package cmd
import (
"crypto/tls"
"errors"
"fmt"
"net"
"net/mail"
"net/smtp"
"net/url"
"os"
"strings"
"github.com/axllent/mailpit/internal/logger"
)
@@ -27,7 +31,7 @@ func Send(addr string, from string, to []string, msg []byte) error {
}
if !isSocket {
return smtp.SendMail(addr, nil, fromAddress.Address, to, msg)
return sendMail(addr, nil, fromAddress.Address, to, msg)
}
conn, err := net.Dial("unix", socketPath)
@@ -69,3 +73,82 @@ func Send(addr string, from string, to []string, msg []byte) error {
return nil
}
func sendMail(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
addrParsed, err := url.Parse(addr) // ensure addr is a valid URL
if err != nil {
return fmt.Errorf("invalid address: %s", addr)
}
host := addrParsed.Host
if err := validateLine(from); err != nil {
return err
}
for _, recipient := range to {
if err := validateLine(recipient); err != nil {
return err
}
}
c, err := smtp.Dial(addr)
if err != nil {
return err
}
defer func() { _ = c.Close() }()
if err = c.Hello(addr); err != nil {
return err
}
if ok, _ := c.Extension("STARTTLS"); ok {
config := &tls.Config{ServerName: host, InsecureSkipVerify: true} // #nosec
if err = c.StartTLS(config); err != nil {
return err
}
}
if a != nil {
if ok, _ := c.Extension("AUTH"); !ok {
return errors.New("smtp: server doesn't support AUTH")
}
if err = c.Auth(a); err != nil {
return err
}
}
if err = c.Mail(from); err != nil {
return err
}
for _, addr := range to {
if err = c.Rcpt(addr); err != nil {
return err
}
}
w, err := c.Data()
if err != nil {
return err
}
_, err = w.Write(msg)
if err != nil {
return err
}
err = w.Close()
if err != nil {
return err
}
return c.Quit()
}
// validateLine checks to see if a line has CR or LF as per RFC 5321.
func validateLine(line string) error {
if strings.ContainsAny(line, "\n\r") {
return errors.New("smtp: A line must not contain CR or LF")
}
return nil
}

View File

@@ -28,7 +28,7 @@ func AppInfo(w http.ResponseWriter, _ *http.Request) {
// 400: ErrorResponse
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(stats.Load()); err != nil {
if err := json.NewEncoder(w).Encode(stats.Load(true)); err != nil {
httpError(w, err.Error())
}
}

View File

@@ -6,6 +6,7 @@ import (
"strings"
"github.com/axllent/mailpit/internal/storage"
"github.com/axllent/mailpit/internal/tools"
)
// MessagesSummary is a summary of a list of messages
@@ -241,9 +242,9 @@ func Search(w http.ResponseWriter, r *http.Request) {
res.Start = start
res.Messages = messages
res.Count = uint64(len(messages)) // legacy - now undocumented in API specs
res.Total = stats.Total // total messages in mailbox
res.MessagesCount = uint64(results)
res.Count = tools.SafeUint64(len(messages)) // legacy - now undocumented in API specs
res.Total = stats.Total // total messages in mailbox
res.MessagesCount = tools.SafeUint64(results)
res.Unread = stats.Unread
res.Tags = stats.Tags
@@ -253,7 +254,7 @@ func Search(w http.ResponseWriter, r *http.Request) {
return
}
res.MessagesUnreadCount = uint64(unread)
res.MessagesUnreadCount = tools.SafeUint64(unread)
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(res); err != nil {

View File

@@ -20,6 +20,7 @@ import (
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/pop3"
"github.com/axllent/mailpit/internal/prometheus"
"github.com/axllent/mailpit/internal/snakeoil"
"github.com/axllent/mailpit/internal/stats"
"github.com/axllent/mailpit/internal/storage"
"github.com/axllent/mailpit/internal/tools"
@@ -101,6 +102,12 @@ func Listen() {
WriteTimeout: 30 * time.Second,
}
// add temporary self-signed certificates to get deleted afterwards
for _, keyPair := range snakeoil.Certificates() {
storage.AddTempFile(keyPair.Public)
storage.AddTempFile(keyPair.Private)
}
if config.UITLSCert != "" && config.UITLSKey != "" {
logger.Log().Infof("[http] starting on %s (TLS)", config.HTTPListen)
logger.Log().Infof("[http] accessible via https://%s%s", logger.CleanHTTPIP(config.HTTPListen), config.Webroot)

View File

@@ -17,7 +17,7 @@ export default {
mixins: [CommonMixins],
watch: {
$route(to, from) {
$route() {
// hide mobile menu on URL change
this.hideNav();
},

View File

@@ -52,7 +52,7 @@ export default {
let response;
try {
response = JSON.parse(e.data);
} catch (e) {
} catch {
return;
}
@@ -128,7 +128,7 @@ export default {
}
};
ws.onclose = (e) => {
ws.onclose = () => {
if (this.socketLastConnection === 0) {
// connection failed immediately after connecting to Mailpit implies proxy websockets aren't configured
console.log("Unable to connect to websocket, disabling websocket support");

View File

@@ -47,14 +47,14 @@ export default {
},
markAllRead() {
this.put(this.resolve(`/api/v1/messages`), { read: true }, (response) => {
this.put(this.resolve(`/api/v1/messages`), { read: true }, () => {
window.scrollInPlace = true;
this.loadMessages();
});
},
deleteAllMessages() {
this.delete(this.resolve(`/api/v1/messages`), false, (response) => {
this.delete(this.resolve(`/api/v1/messages`), false, () => {
pagination.start = 0;
this.loadMessages();
});

View File

@@ -28,7 +28,7 @@ export default {
if (!mailbox.selected.length) {
return false;
}
this.put(this.resolve(`/api/v1/messages`), { Read: true, IDs: mailbox.selected }, (response) => {
this.put(this.resolve(`/api/v1/messages`), { Read: true, IDs: mailbox.selected }, () => {
window.scrollInPlace = true;
this.loadMessages();
});
@@ -43,7 +43,7 @@ export default {
if (!mailbox.selected.length) {
return false;
}
this.put(this.resolve(`/api/v1/messages`), { Read: false, IDs: mailbox.selected }, (response) => {
this.put(this.resolve(`/api/v1/messages`), { Read: false, IDs: mailbox.selected }, () => {
window.scrollInPlace = true;
this.loadMessages();
});
@@ -57,7 +57,7 @@ export default {
return false;
}
this.delete(this.resolve(`/api/v1/messages`), { IDs: ids }, (response) => {
this.delete(this.resolve(`/api/v1/messages`), { IDs: ids }, () => {
window.scrollInPlace = true;
this.loadMessages();
});

View File

@@ -257,7 +257,10 @@ export default {
if (platforms) {
try {
this.platforms = JSON.parse(platforms);
} catch (e) {}
} catch {
// if parsing fails, reset to default
this.platforms = [];
}
}
// set all options

View File

@@ -153,7 +153,7 @@ export default {
this.error = error.message;
}
})
.then((result) => {
.then(() => {
// always run
this.loading = false;
});

View File

@@ -143,12 +143,12 @@ export default {
window.addEventListener("resize", this.resizeIFrames);
const headersTab = document.getElementById("nav-headers-tab");
headersTab.addEventListener("shown.bs.tab", (event) => {
headersTab.addEventListener("shown.bs.tab", () => {
this.loadHeaders = true;
});
const rawTab = document.getElementById("nav-raw-tab");
rawTab.addEventListener("shown.bs.tab", (event) => {
rawTab.addEventListener("shown.bs.tab", () => {
this.srcURI = this.resolve("/api/v1/message/" + this.message.ID + "/raw");
this.resizeIFrames();
});
@@ -180,7 +180,7 @@ export default {
this.isHTMLTabSelected();
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach((listObj) => {
listObj.addEventListener("shown.bs.tab", (event) => {
listObj.addEventListener("shown.bs.tab", () => {
this.isHTMLTabSelected();
});
});
@@ -203,7 +203,9 @@ export default {
anchorEl.setAttribute("target", "_blank");
}
}
} catch (error) {}
} catch {
// ignore errors when accessing the iframe content
}
this.resizeIFrames();
}
}, 500);
@@ -280,7 +282,7 @@ export default {
Tags: this.messageTags,
};
this.put(this.resolve("/api/v1/tags"), data, (response) => {
this.put(this.resolve("/api/v1/tags"), data, () => {
window.scrollInPlace = true;
this.$emit("loadMessages");
});
@@ -290,15 +292,24 @@ export default {
textToHTML(s) {
let html = s;
// full links with http(s)
const re = /(\b(https?|ftp):\/\/[-\w@:%_+'!.~#?,&//=;]+)/gim;
html = html.replace(re, "˱˱˱a href=ˠˠˠ$&ˠˠˠ target=_blank rel=noopener˲˲˲$&˱˱˱/a˲˲˲");
// RFC2396 appendix E states angle brackets are recommended for text/plain emails to
// recognize potential spaces in between the URL
// @see https://www.rfc-editor.org/rfc/rfc2396#appendix-E
const angleLinks = /<((https?|ftp):\/\/[-\w@:%_+'!.~#?,&//=; ][^>]+)>/gim;
html = html.replace(angleLinks, "<˱˱˱a href=ˠˠˠ$1ˠˠˠ target=_blank rel=noopener˲˲˲$1˱˱˱/a˲˲˲>");
// find links without angle brackets, starting with http(s) or ftp
const regularLinks = /([^ˠ˲]\b)(((https?|ftp):\/\/[-\w@:%_+'!.~#?,&//=;]+))/gim;
html = html.replace(regularLinks, "$1˱˱˱a href=ˠˠˠ$2ˠˠˠ target=_blank rel=noopener˲˲˲$2˱˱˱/a˲˲˲");
// plain www links without https?:// prefix
const re2 = /(^|[^/])(www\.[\S]+(\b|$))/gim;
html = html.replace(re2, "$1˱˱˱a href=ˠˠˠhttp://$2ˠˠˠ target=ˠˠˠ_blankˠˠˠ rel=ˠˠˠnoopenerˠˠˠ˲˲˲$2˱˱˱/a˲˲˲");
const shortLinks = /(^|[^/])(www\.[\S]+(\b|$))/gim;
html = html.replace(
shortLinks,
"$1˱˱˱a href=ˠˠˠhttp://$2ˠˠˠ target=ˠˠˠ_blankˠˠˠ rel=ˠˠˠnoopenerˠˠˠ˲˲˲$2˱˱˱/a˲˲˲",
);
// escape to HTML & convert <>" back
// escape to HTML & convert <>" characters back
html = html
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")

View File

@@ -64,7 +64,7 @@ export default {
To: this.addresses,
};
this.post(this.resolve("/api/v1/message/" + this.message.ID + "/release"), data, (response) => {
this.post(this.resolve("/api/v1/message/" + this.message.ID + "/release"), data, () => {
this.modal("ReleaseModal").hide();
if (this.deleteAfterRelease) {
this.$emit("delete");

View File

@@ -14,7 +14,7 @@ export default {
},
watch: {
"mailbox.refresh": function (v) {
"mailbox.refresh"(v) {
if (v) {
// trigger a refresh
this.loadMessages();
@@ -45,9 +45,9 @@ export default {
const params = {};
mailbox.selected = [];
params["limit"] = pagination.limit;
params.limit = pagination.limit;
if (pagination.start > 0) {
params["start"] = pagination.start;
params.start = pagination.start;
}
this.get(this.apiURI, params, (response) => {

View File

@@ -33,7 +33,7 @@ export const mailbox = reactive({
watch(
() => mailbox.count,
(v) => {
() => {
mailbox.selected = [];
},
);

View File

@@ -36,7 +36,7 @@ export default {
},
watch: {
$route(to, from) {
$route() {
this.loadMailbox();
},
},

View File

@@ -94,7 +94,7 @@ export default {
},
watch: {
$route(to, from) {
$route() {
this.loadMessage();
},
},

View File

@@ -36,7 +36,7 @@ export default {
},
watch: {
$route(to, from) {
$route() {
this.doSearch();
},
},