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:
22
CHANGELOG.md
22
CHANGELOG.md
@@ -2,6 +2,28 @@
|
|||||||
|
|
||||||
Notable changes to Mailpit will be documented in this file.
|
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]
|
## [v1.27.1]
|
||||||
|
|
||||||
### Chore
|
### Chore
|
||||||
|
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/axllent/mailpit/internal/auth"
|
"github.com/axllent/mailpit/internal/auth"
|
||||||
"github.com/axllent/mailpit/internal/logger"
|
"github.com/axllent/mailpit/internal/logger"
|
||||||
"github.com/axllent/mailpit/internal/smtpd/chaos"
|
"github.com/axllent/mailpit/internal/smtpd/chaos"
|
||||||
|
"github.com/axllent/mailpit/internal/snakeoil"
|
||||||
"github.com/axllent/mailpit/internal/spamassassin"
|
"github.com/axllent/mailpit/internal/spamassassin"
|
||||||
"github.com/axllent/mailpit/internal/tools"
|
"github.com/axllent/mailpit/internal/tools"
|
||||||
)
|
)
|
||||||
@@ -333,8 +334,19 @@ func VerifyConfig() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if UITLSCert != "" {
|
if UITLSCert != "" {
|
||||||
UITLSCert = filepath.Clean(UITLSCert)
|
if strings.HasPrefix(UITLSCert, "sans:") {
|
||||||
UITLSKey = filepath.Clean(UITLSKey)
|
// 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) {
|
if !isFile(UITLSCert) {
|
||||||
return fmt.Errorf("[ui] TLS certificate not found or readable: %s", UITLSCert)
|
return fmt.Errorf("[ui] TLS certificate not found or readable: %s", UITLSCert)
|
||||||
@@ -393,8 +405,19 @@ func VerifyConfig() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if SMTPTLSCert != "" {
|
if SMTPTLSCert != "" {
|
||||||
SMTPTLSCert = filepath.Clean(SMTPTLSCert)
|
if strings.HasPrefix(SMTPTLSCert, "sans:") {
|
||||||
SMTPTLSKey = filepath.Clean(SMTPTLSKey)
|
// 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) {
|
if !isFile(SMTPTLSCert) {
|
||||||
return fmt.Errorf("[smtp] TLS certificate not found or readable: %s", SMTPTLSCert)
|
return fmt.Errorf("[smtp] TLS certificate not found or readable: %s", SMTPTLSCert)
|
||||||
@@ -462,8 +485,18 @@ func VerifyConfig() error {
|
|||||||
|
|
||||||
// POP3 server
|
// POP3 server
|
||||||
if POP3TLSCert != "" {
|
if POP3TLSCert != "" {
|
||||||
POP3TLSCert = filepath.Clean(POP3TLSCert)
|
if strings.HasPrefix(POP3TLSCert, "sans:") {
|
||||||
POP3TLSKey = filepath.Clean(POP3TLSKey)
|
// 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) {
|
if !isFile(POP3TLSCert) {
|
||||||
return fmt.Errorf("[pop3] TLS certificate not found or readable: %s", POP3TLSCert)
|
return fmt.Errorf("[pop3] TLS certificate not found or readable: %s", POP3TLSCert)
|
||||||
|
@@ -1,34 +1,76 @@
|
|||||||
import eslintConfigPrettier from "eslint-config-prettier/flat";
|
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 vue from "eslint-plugin-vue";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const gitignorePath = fileURLToPath(new URL(".gitignore", import.meta.url));
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
/* Baseline JS rules, provided by Neostandard */
|
/* Use .gitignore to prevent linting of irrelevant files */
|
||||||
...neostandard({
|
includeIgnoreFile(gitignorePath, ".gitignore"),
|
||||||
/* Allows references to browser APIs like `document` */
|
|
||||||
env: ["browser"],
|
|
||||||
|
|
||||||
/* We rely on .gitignore to avoid running against dist / dependency files */
|
/* ESLint's recommended rules */
|
||||||
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 */
|
|
||||||
files: ["**/*.js", "**/*.vue"],
|
files: ["**/*.js", "**/*.vue"],
|
||||||
}),
|
languageOptions: { globals: { ...globals.browser, ...globals.node } },
|
||||||
|
rules: js.configs.recommended.rules,
|
||||||
|
},
|
||||||
|
|
||||||
/* Vue-specific rules */
|
/* Vue-specific rules */
|
||||||
...vue.configs["flat/recommended"],
|
...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,
|
eslintConfigPrettier,
|
||||||
|
|
||||||
/* Our custom rules */
|
/* Our custom rules */
|
||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
/* We prefer arrow functions for tidiness and consistency */
|
/* Always use arrow functions for tidiness and consistency */
|
||||||
"prefer-arrow-callback": "error",
|
"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
20
go.mod
@@ -21,14 +21,14 @@ require (
|
|||||||
github.com/rqlite/gorqlite v0.0.0-20250609141355-ac86a4a1c9a8
|
github.com/rqlite/gorqlite v0.0.0-20250609141355-ac86a4a1c9a8
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/spf13/cobra v1.9.1
|
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/tg123/go-htpasswd v1.2.4
|
||||||
github.com/vanng822/go-premailer v1.25.0
|
github.com/vanng822/go-premailer v1.25.0
|
||||||
golang.org/x/crypto v0.39.0
|
golang.org/x/crypto v0.40.0
|
||||||
golang.org/x/net v0.41.0
|
golang.org/x/net v0.42.0
|
||||||
golang.org/x/text v0.26.0
|
golang.org/x/text v0.27.0
|
||||||
golang.org/x/time v0.12.0
|
golang.org/x/time v0.12.0
|
||||||
modernc.org/sqlite v1.38.0
|
modernc.org/sqlite v1.38.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -62,12 +62,12 @@ require (
|
|||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/vanng822/css v1.0.1 // indirect
|
github.com/vanng822/css v1.0.1 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
|
||||||
golang.org/x/image v0.28.0 // indirect
|
golang.org/x/image v0.29.0 // indirect
|
||||||
golang.org/x/mod v0.25.0 // indirect
|
golang.org/x/mod v0.26.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.34.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // 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/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
)
|
)
|
||||||
|
51
go.sum
51
go.sum
@@ -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/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 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
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.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 h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
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=
|
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.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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
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.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4=
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
|
||||||
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
|
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
|
||||||
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
|
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.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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.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.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.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.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
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-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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
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.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
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.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
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-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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/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.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.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.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
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-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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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.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.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.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
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/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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.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.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
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 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
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=
|
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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
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.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
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=
|
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 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
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/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 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
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.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.1.2/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.66.2 h1:JCBxlJzZOIwZY54fzjHN3Wsn8Ty5PUTPr/xioRkmecI=
|
modernc.org/libc v1.66.4 h1:EW4EaqAVngI6f5KWiFibu41IYFMv/F7KEtR+NRHrS/Q=
|
||||||
modernc.org/libc v1.66.2/go.mod h1:ceIGzvXxP+JV3pgVjP9avPZo6Chlsfof2egXBH3YT5Q=
|
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 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
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/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
|
modernc.org/sqlite v1.38.1 h1:jNnIjleVta+DKSAr3TnkKK87EEhjPhBLzi6hvIX9Bas=
|
||||||
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
|
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 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
@@ -31,7 +31,14 @@ var (
|
|||||||
</html>`
|
</html>`
|
||||||
|
|
||||||
expectedHTMLLinks = []string{
|
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
|
"http://remote-host/style.css", // css
|
||||||
"https://example.com/image.jpg", // images
|
"https://example.com/image.jpg", // images
|
||||||
}
|
}
|
||||||
@@ -41,10 +48,18 @@ var (
|
|||||||
[http://localhost]
|
[http://localhost]
|
||||||
www.google.com < ignored
|
www.google.com < ignored
|
||||||
|||http://example.com/?some=query-string|||
|
|||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{
|
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",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -30,9 +30,28 @@ func RunTests(msg *storage.Message, followRedirects bool) (Response, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func extractTextLinks(msg *storage.Message) []string {
|
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 := []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
|
return links
|
||||||
}
|
}
|
||||||
|
@@ -215,7 +215,7 @@ func handleTransactionCommand(conn net.Conn, cmd string, args []string, messages
|
|||||||
for _, m := range messages {
|
for _, m := range messages {
|
||||||
totalSize += m.Size
|
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":
|
case "LIST":
|
||||||
totalSize := uint64(0)
|
totalSize := uint64(0)
|
||||||
for _, m := range messages {
|
for _, m := range messages {
|
||||||
@@ -229,12 +229,12 @@ func handleTransactionCommand(conn net.Conn, cmd string, args []string, messages
|
|||||||
sendResponse(conn, "-ERR no such message")
|
sendResponse(conn, "-ERR no such message")
|
||||||
return
|
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 {
|
} 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 {
|
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, ".")
|
sendResponse(conn, ".")
|
||||||
}
|
}
|
||||||
|
@@ -32,7 +32,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// InitMetrics initializes all Prometheus metrics
|
// InitMetrics initializes all Prometheus metrics
|
||||||
func InitMetrics() {
|
func initMetrics() {
|
||||||
// Create metrics
|
// Create metrics
|
||||||
totalMessages = prometheus.NewGauge(prometheus.GaugeOpts{
|
totalMessages = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
Name: "mailpit_messages",
|
Name: "mailpit_messages",
|
||||||
@@ -107,8 +107,8 @@ func InitMetrics() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateMetrics updates all metrics with current values
|
// UpdateMetrics updates all metrics with current values
|
||||||
func UpdateMetrics() {
|
func updateMetrics() {
|
||||||
info := stats.Load()
|
info := stats.Load(false)
|
||||||
|
|
||||||
totalMessages.Set(float64(info.Messages))
|
totalMessages.Set(float64(info.Messages))
|
||||||
unreadMessages.Set(float64(info.Unread))
|
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 {
|
func GetHandler() http.Handler {
|
||||||
return promhttp.HandlerFor(Registry, promhttp.HandlerOpts{
|
return promhttp.HandlerFor(Registry, promhttp.HandlerOpts{
|
||||||
DisableCompression: true,
|
DisableCompression: true,
|
||||||
@@ -139,8 +139,8 @@ func GetHandler() http.Handler {
|
|||||||
|
|
||||||
// StartUpdater starts the periodic metrics update routine
|
// StartUpdater starts the periodic metrics update routine
|
||||||
func StartUpdater() {
|
func StartUpdater() {
|
||||||
InitMetrics()
|
initMetrics()
|
||||||
UpdateMetrics()
|
updateMetrics()
|
||||||
|
|
||||||
// Start periodic updates
|
// Start periodic updates
|
||||||
go func() {
|
go func() {
|
||||||
@@ -148,7 +148,7 @@ func StartUpdater() {
|
|||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
UpdateMetrics()
|
updateMetrics()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -165,8 +165,9 @@ func StartSeparateServer() {
|
|||||||
|
|
||||||
// Create a dedicated server instance
|
// Create a dedicated server instance
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: config.PrometheusListen,
|
Addr: config.PrometheusListen,
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start HTTP server
|
// Start HTTP server
|
||||||
|
196
internal/snakeoil/snakeoil.go
Normal file
196
internal/snakeoil/snakeoil.go
Normal 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
|
||||||
|
}
|
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/axllent/mailpit/config"
|
"github.com/axllent/mailpit/config"
|
||||||
"github.com/axllent/mailpit/internal/logger"
|
"github.com/axllent/mailpit/internal/logger"
|
||||||
"github.com/axllent/mailpit/internal/storage"
|
"github.com/axllent/mailpit/internal/storage"
|
||||||
|
"github.com/axllent/mailpit/internal/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Stores cached version along with its expiry time and error count.
|
// 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
|
// Load the current statistics
|
||||||
func Load() AppInformation {
|
func Load(detectLatestVersion bool) AppInformation {
|
||||||
info := AppInformation{}
|
info := AppInformation{}
|
||||||
info.Version = config.Version
|
info.Version = config.Version
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ func Load() AppInformation {
|
|||||||
|
|
||||||
if config.DisableVersionCheck {
|
if config.DisableVersionCheck {
|
||||||
info.LatestVersion = "disabled"
|
info.LatestVersion = "disabled"
|
||||||
} else {
|
} else if detectLatestVersion {
|
||||||
mu.RLock()
|
mu.RLock()
|
||||||
cacheValid := time.Now().Before(vCache.expiry)
|
cacheValid := time.Now().Before(vCache.expiry)
|
||||||
cacheValue := vCache.value
|
cacheValue := vCache.value
|
||||||
@@ -146,7 +147,7 @@ func Track() {
|
|||||||
func LogSMTPAccepted(size int) {
|
func LogSMTPAccepted(size int) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
smtpAccepted = smtpAccepted + 1
|
smtpAccepted = smtpAccepted + 1
|
||||||
smtpAcceptedSize = smtpAcceptedSize + uint64(size)
|
smtpAcceptedSize = smtpAcceptedSize + tools.SafeUint64(size)
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -60,20 +60,19 @@ func pruneMessages() {
|
|||||||
var size float64 // use float64 for rqlite compatibility
|
var size float64 // use float64 for rqlite compatibility
|
||||||
|
|
||||||
// prune using `--max` if set
|
// prune using `--max` if set
|
||||||
if config.MaxMessages > 0 {
|
if config.MaxMessages > 0 && CountTotal() > uint64(config.MaxMessages) {
|
||||||
total := CountTotal()
|
offset := config.MaxMessages
|
||||||
if total > uint64(config.MaxAgeInHours) {
|
if config.DemoMode {
|
||||||
offset := config.MaxMessages
|
offset = 500
|
||||||
if config.DemoMode {
|
}
|
||||||
offset = 500
|
q := sqlf.Select("ID, Size").
|
||||||
}
|
From(tenant("mailbox")).
|
||||||
q := sqlf.Select("ID, Size").
|
OrderBy("Created DESC").
|
||||||
From(tenant("mailbox")).
|
Limit(5000).
|
||||||
OrderBy("Created DESC").
|
Offset(offset)
|
||||||
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
|
var id string
|
||||||
|
|
||||||
if err := row.Scan(&id, &size); err != nil {
|
if err := row.Scan(&id, &size); err != nil {
|
||||||
@@ -83,10 +82,10 @@ func pruneMessages() {
|
|||||||
ids = append(ids, id)
|
ids = append(ids, id)
|
||||||
prunedSize = prunedSize + uint64(size)
|
prunedSize = prunedSize + uint64(size)
|
||||||
|
|
||||||
}); err != nil {
|
},
|
||||||
logger.Log().Errorf("[db] %s", err.Error())
|
); err != nil {
|
||||||
return
|
logger.Log().Errorf("[db] %s", err.Error())
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -108,7 +108,7 @@ func Store(body *[]byte, username *string) (string, error) {
|
|||||||
|
|
||||||
if config.Compression > 0 {
|
if config.Compression > 0 {
|
||||||
// insert compressed raw message
|
// insert compressed raw message
|
||||||
compressed := dbEncoder.EncodeAll(*body, make([]byte, 0, int(size)))
|
compressed := dbEncoder.EncodeAll(*body, make([]byte, 0, size))
|
||||||
|
|
||||||
if sqlDriver == "rqlite" {
|
if sqlDriver == "rqlite" {
|
||||||
// rqlite does not support binary data in query, so we need to encode the compressed message into hexadecimal
|
// 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()
|
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
|
return id, nil
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/axllent/mailpit/internal/html2text"
|
"github.com/axllent/mailpit/internal/html2text"
|
||||||
"github.com/axllent/mailpit/internal/logger"
|
"github.com/axllent/mailpit/internal/logger"
|
||||||
|
"github.com/axllent/mailpit/internal/tools"
|
||||||
"github.com/jhillyerd/enmime/v2"
|
"github.com/jhillyerd/enmime/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ func cleanString(str string) string {
|
|||||||
// LogMessagesDeleted logs the number of messages deleted
|
// LogMessagesDeleted logs the number of messages deleted
|
||||||
func logMessagesDeleted(n int) {
|
func logMessagesDeleted(n int) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
StatsDeleted = StatsDeleted + uint64(n)
|
StatsDeleted = StatsDeleted + tools.SafeUint64(n)
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -36,3 +36,22 @@ func Normalize(s string) string {
|
|||||||
|
|
||||||
return strings.TrimSpace(s)
|
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
3913
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@
|
|||||||
"lint-fix": "eslint --fix && prettier --write ."
|
"lint-fix": "eslint --fix && prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.2.1",
|
"axios": "^1.11.0",
|
||||||
"bootstrap": "^5.2.0",
|
"bootstrap": "^5.2.0",
|
||||||
"bootstrap-icons": "^1.9.1",
|
"bootstrap-icons": "^1.9.1",
|
||||||
"bootstrap5-tags": "^1.6.1",
|
"bootstrap5-tags": "^1.6.1",
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
"vue-router": "^4.2.4"
|
"vue-router": "^4.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/compat": "^1.3.1",
|
||||||
"@popperjs/core": "^2.11.5",
|
"@popperjs/core": "^2.11.5",
|
||||||
"@types/bootstrap": "^5.2.7",
|
"@types/bootstrap": "^5.2.7",
|
||||||
"@types/tinycon": "^0.6.3",
|
"@types/tinycon": "^0.6.3",
|
||||||
@@ -40,10 +41,9 @@
|
|||||||
"eslint": "^9.29.0",
|
"eslint": "^9.29.0",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"eslint-plugin-vue": "^10.2.0",
|
"eslint-plugin-vue": "^10.2.0",
|
||||||
"neostandard": "^0.12.1",
|
|
||||||
"prettier": "^3.5.3"
|
"prettier": "^3.5.3"
|
||||||
},
|
},
|
||||||
"prettier":{
|
"prettier": {
|
||||||
"tabWidth": 4,
|
"tabWidth": 4,
|
||||||
"useTabs": true,
|
"useTabs": true,
|
||||||
"printWidth": 120
|
"printWidth": 120
|
||||||
|
@@ -2,11 +2,15 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/axllent/mailpit/internal/logger"
|
"github.com/axllent/mailpit/internal/logger"
|
||||||
)
|
)
|
||||||
@@ -27,7 +31,7 @@ func Send(addr string, from string, to []string, msg []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !isSocket {
|
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)
|
conn, err := net.Dial("unix", socketPath)
|
||||||
@@ -69,3 +73,82 @@ func Send(addr string, from string, to []string, msg []byte) error {
|
|||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
@@ -28,7 +28,7 @@ func AppInfo(w http.ResponseWriter, _ *http.Request) {
|
|||||||
// 400: ErrorResponse
|
// 400: ErrorResponse
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
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())
|
httpError(w, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/axllent/mailpit/internal/storage"
|
"github.com/axllent/mailpit/internal/storage"
|
||||||
|
"github.com/axllent/mailpit/internal/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MessagesSummary is a summary of a list of messages
|
// 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.Start = start
|
||||||
res.Messages = messages
|
res.Messages = messages
|
||||||
res.Count = uint64(len(messages)) // legacy - now undocumented in API specs
|
res.Count = tools.SafeUint64(len(messages)) // legacy - now undocumented in API specs
|
||||||
res.Total = stats.Total // total messages in mailbox
|
res.Total = stats.Total // total messages in mailbox
|
||||||
res.MessagesCount = uint64(results)
|
res.MessagesCount = tools.SafeUint64(results)
|
||||||
res.Unread = stats.Unread
|
res.Unread = stats.Unread
|
||||||
res.Tags = stats.Tags
|
res.Tags = stats.Tags
|
||||||
|
|
||||||
@@ -253,7 +254,7 @@ func Search(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res.MessagesUnreadCount = uint64(unread)
|
res.MessagesUnreadCount = tools.SafeUint64(unread)
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
w.Header().Add("Content-Type", "application/json")
|
||||||
if err := json.NewEncoder(w).Encode(res); err != nil {
|
if err := json.NewEncoder(w).Encode(res); err != nil {
|
||||||
|
@@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/axllent/mailpit/internal/logger"
|
"github.com/axllent/mailpit/internal/logger"
|
||||||
"github.com/axllent/mailpit/internal/pop3"
|
"github.com/axllent/mailpit/internal/pop3"
|
||||||
"github.com/axllent/mailpit/internal/prometheus"
|
"github.com/axllent/mailpit/internal/prometheus"
|
||||||
|
"github.com/axllent/mailpit/internal/snakeoil"
|
||||||
"github.com/axllent/mailpit/internal/stats"
|
"github.com/axllent/mailpit/internal/stats"
|
||||||
"github.com/axllent/mailpit/internal/storage"
|
"github.com/axllent/mailpit/internal/storage"
|
||||||
"github.com/axllent/mailpit/internal/tools"
|
"github.com/axllent/mailpit/internal/tools"
|
||||||
@@ -101,6 +102,12 @@ func Listen() {
|
|||||||
WriteTimeout: 30 * time.Second,
|
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 != "" {
|
if config.UITLSCert != "" && config.UITLSKey != "" {
|
||||||
logger.Log().Infof("[http] starting on %s (TLS)", config.HTTPListen)
|
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)
|
logger.Log().Infof("[http] accessible via https://%s%s", logger.CleanHTTPIP(config.HTTPListen), config.Webroot)
|
||||||
|
@@ -17,7 +17,7 @@ export default {
|
|||||||
mixins: [CommonMixins],
|
mixins: [CommonMixins],
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
$route(to, from) {
|
$route() {
|
||||||
// hide mobile menu on URL change
|
// hide mobile menu on URL change
|
||||||
this.hideNav();
|
this.hideNav();
|
||||||
},
|
},
|
||||||
|
@@ -52,7 +52,7 @@ export default {
|
|||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = JSON.parse(e.data);
|
response = JSON.parse(e.data);
|
||||||
} catch (e) {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ export default {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = (e) => {
|
ws.onclose = () => {
|
||||||
if (this.socketLastConnection === 0) {
|
if (this.socketLastConnection === 0) {
|
||||||
// connection failed immediately after connecting to Mailpit implies proxy websockets aren't configured
|
// connection failed immediately after connecting to Mailpit implies proxy websockets aren't configured
|
||||||
console.log("Unable to connect to websocket, disabling websocket support");
|
console.log("Unable to connect to websocket, disabling websocket support");
|
||||||
|
@@ -47,14 +47,14 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
markAllRead() {
|
markAllRead() {
|
||||||
this.put(this.resolve(`/api/v1/messages`), { read: true }, (response) => {
|
this.put(this.resolve(`/api/v1/messages`), { read: true }, () => {
|
||||||
window.scrollInPlace = true;
|
window.scrollInPlace = true;
|
||||||
this.loadMessages();
|
this.loadMessages();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteAllMessages() {
|
deleteAllMessages() {
|
||||||
this.delete(this.resolve(`/api/v1/messages`), false, (response) => {
|
this.delete(this.resolve(`/api/v1/messages`), false, () => {
|
||||||
pagination.start = 0;
|
pagination.start = 0;
|
||||||
this.loadMessages();
|
this.loadMessages();
|
||||||
});
|
});
|
||||||
|
@@ -28,7 +28,7 @@ export default {
|
|||||||
if (!mailbox.selected.length) {
|
if (!mailbox.selected.length) {
|
||||||
return false;
|
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;
|
window.scrollInPlace = true;
|
||||||
this.loadMessages();
|
this.loadMessages();
|
||||||
});
|
});
|
||||||
@@ -43,7 +43,7 @@ export default {
|
|||||||
if (!mailbox.selected.length) {
|
if (!mailbox.selected.length) {
|
||||||
return false;
|
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;
|
window.scrollInPlace = true;
|
||||||
this.loadMessages();
|
this.loadMessages();
|
||||||
});
|
});
|
||||||
@@ -57,7 +57,7 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.delete(this.resolve(`/api/v1/messages`), { IDs: ids }, (response) => {
|
this.delete(this.resolve(`/api/v1/messages`), { IDs: ids }, () => {
|
||||||
window.scrollInPlace = true;
|
window.scrollInPlace = true;
|
||||||
this.loadMessages();
|
this.loadMessages();
|
||||||
});
|
});
|
||||||
|
@@ -257,7 +257,10 @@ export default {
|
|||||||
if (platforms) {
|
if (platforms) {
|
||||||
try {
|
try {
|
||||||
this.platforms = JSON.parse(platforms);
|
this.platforms = JSON.parse(platforms);
|
||||||
} catch (e) {}
|
} catch {
|
||||||
|
// if parsing fails, reset to default
|
||||||
|
this.platforms = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// set all options
|
// set all options
|
||||||
|
@@ -153,7 +153,7 @@ export default {
|
|||||||
this.error = error.message;
|
this.error = error.message;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then(() => {
|
||||||
// always run
|
// always run
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
});
|
});
|
||||||
|
@@ -143,12 +143,12 @@ export default {
|
|||||||
window.addEventListener("resize", this.resizeIFrames);
|
window.addEventListener("resize", this.resizeIFrames);
|
||||||
|
|
||||||
const headersTab = document.getElementById("nav-headers-tab");
|
const headersTab = document.getElementById("nav-headers-tab");
|
||||||
headersTab.addEventListener("shown.bs.tab", (event) => {
|
headersTab.addEventListener("shown.bs.tab", () => {
|
||||||
this.loadHeaders = true;
|
this.loadHeaders = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const rawTab = document.getElementById("nav-raw-tab");
|
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.srcURI = this.resolve("/api/v1/message/" + this.message.ID + "/raw");
|
||||||
this.resizeIFrames();
|
this.resizeIFrames();
|
||||||
});
|
});
|
||||||
@@ -180,7 +180,7 @@ export default {
|
|||||||
this.isHTMLTabSelected();
|
this.isHTMLTabSelected();
|
||||||
|
|
||||||
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach((listObj) => {
|
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach((listObj) => {
|
||||||
listObj.addEventListener("shown.bs.tab", (event) => {
|
listObj.addEventListener("shown.bs.tab", () => {
|
||||||
this.isHTMLTabSelected();
|
this.isHTMLTabSelected();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -203,7 +203,9 @@ export default {
|
|||||||
anchorEl.setAttribute("target", "_blank");
|
anchorEl.setAttribute("target", "_blank");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch {
|
||||||
|
// ignore errors when accessing the iframe content
|
||||||
|
}
|
||||||
this.resizeIFrames();
|
this.resizeIFrames();
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
@@ -280,7 +282,7 @@ export default {
|
|||||||
Tags: this.messageTags,
|
Tags: this.messageTags,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.put(this.resolve("/api/v1/tags"), data, (response) => {
|
this.put(this.resolve("/api/v1/tags"), data, () => {
|
||||||
window.scrollInPlace = true;
|
window.scrollInPlace = true;
|
||||||
this.$emit("loadMessages");
|
this.$emit("loadMessages");
|
||||||
});
|
});
|
||||||
@@ -290,15 +292,24 @@ export default {
|
|||||||
textToHTML(s) {
|
textToHTML(s) {
|
||||||
let html = s;
|
let html = s;
|
||||||
|
|
||||||
// full links with http(s)
|
// RFC2396 appendix E states angle brackets are recommended for text/plain emails to
|
||||||
const re = /(\b(https?|ftp):\/\/[-\w@:%_+'!.~#?,&//=;]+)/gim;
|
// recognize potential spaces in between the URL
|
||||||
html = html.replace(re, "˱˱˱a href=ˠˠˠ$&ˠˠˠ target=_blank rel=noopener˲˲˲$&˱˱˱/a˲˲˲");
|
// @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
|
// plain www links without https?:// prefix
|
||||||
const re2 = /(^|[^/])(www\.[\S]+(\b|$))/gim;
|
const shortLinks = /(^|[^/])(www\.[\S]+(\b|$))/gim;
|
||||||
html = html.replace(re2, "$1˱˱˱a href=ˠˠˠhttp://$2ˠˠˠ target=ˠˠˠ_blankˠˠˠ rel=ˠˠˠnoopenerˠˠˠ˲˲˲$2˱˱˱/a˲˲˲");
|
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
|
html = html
|
||||||
.replace(/&/g, "&")
|
.replace(/&/g, "&")
|
||||||
.replace(/</g, "<")
|
.replace(/</g, "<")
|
||||||
|
@@ -64,7 +64,7 @@ export default {
|
|||||||
To: this.addresses,
|
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();
|
this.modal("ReleaseModal").hide();
|
||||||
if (this.deleteAfterRelease) {
|
if (this.deleteAfterRelease) {
|
||||||
this.$emit("delete");
|
this.$emit("delete");
|
||||||
|
@@ -14,7 +14,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
"mailbox.refresh": function (v) {
|
"mailbox.refresh"(v) {
|
||||||
if (v) {
|
if (v) {
|
||||||
// trigger a refresh
|
// trigger a refresh
|
||||||
this.loadMessages();
|
this.loadMessages();
|
||||||
@@ -45,9 +45,9 @@ export default {
|
|||||||
const params = {};
|
const params = {};
|
||||||
mailbox.selected = [];
|
mailbox.selected = [];
|
||||||
|
|
||||||
params["limit"] = pagination.limit;
|
params.limit = pagination.limit;
|
||||||
if (pagination.start > 0) {
|
if (pagination.start > 0) {
|
||||||
params["start"] = pagination.start;
|
params.start = pagination.start;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.get(this.apiURI, params, (response) => {
|
this.get(this.apiURI, params, (response) => {
|
||||||
|
@@ -33,7 +33,7 @@ export const mailbox = reactive({
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => mailbox.count,
|
() => mailbox.count,
|
||||||
(v) => {
|
() => {
|
||||||
mailbox.selected = [];
|
mailbox.selected = [];
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@@ -36,7 +36,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
$route(to, from) {
|
$route() {
|
||||||
this.loadMailbox();
|
this.loadMailbox();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@@ -94,7 +94,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
$route(to, from) {
|
$route() {
|
||||||
this.loadMessage();
|
this.loadMessage();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@@ -36,7 +36,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
$route(to, from) {
|
$route() {
|
||||||
this.doSearch();
|
this.doSearch();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user