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

Merge branch 'release/v1.13.1'

This commit is contained in:
Ralph Slooten 2024-01-27 23:14:17 +13:00
commit e812d12590
13 changed files with 410 additions and 345 deletions

View File

@ -2,6 +2,22 @@
Notable changes to Mailpit will be documented in this file.
## [v1.13.1]
### Chore
- Update node dependencies
- Update Go dependencies
### Feature
- Add TLSRequired option for smtpd ([#241](https://github.com/axllent/mailpit/issues/241))
### Fix
- Workaround for specific field searches containing unicode characters ([#239](https://github.com/axllent/mailpit/issues/239))
### UI
- Only show number of messages ignored statistics if `--ignore-duplicate-ids` is set
## [v1.13.0]
### Chore

View File

@ -101,7 +101,8 @@ func init() {
rootCmd.Flags().BoolVar(&config.SMTPAuthAcceptAny, "smtp-auth-accept-any", config.SMTPAuthAcceptAny, "Accept any SMTP username and password, including none")
rootCmd.Flags().StringVar(&config.SMTPTLSCert, "smtp-tls-cert", config.SMTPTLSCert, "TLS certificate for SMTP (STARTTLS) - requires smtp-tls-key")
rootCmd.Flags().StringVar(&config.SMTPTLSKey, "smtp-tls-key", config.SMTPTLSKey, "TLS key for SMTP (STARTTLS) - requires smtp-tls-cert")
rootCmd.Flags().BoolVar(&config.SMTPAuthAllowInsecure, "smtp-auth-allow-insecure", config.SMTPAuthAllowInsecure, "Enable insecure PLAIN & LOGIN authentication")
rootCmd.Flags().BoolVar(&config.SMTPTLSRequired, "smtp-tls-required", config.SMTPTLSRequired, "Require TLS SMTP encryption")
rootCmd.Flags().BoolVar(&config.SMTPAuthAllowInsecure, "smtp-auth-allow-insecure", config.SMTPAuthAllowInsecure, "Allow insecure PLAIN & LOGIN SMTP authentication")
rootCmd.Flags().BoolVar(&config.SMTPStrictRFCHeaders, "smtp-strict-rfc-headers", config.SMTPStrictRFCHeaders, "Return SMTP error if message headers contain <CR><CR><LF>")
rootCmd.Flags().IntVar(&config.SMTPMaxRecipients, "smtp-max-recipients", config.SMTPMaxRecipients, "Maximum SMTP recipients allowed")
rootCmd.Flags().StringVar(&config.SMTPAllowedRecipients, "smtp-allowed-recipients", config.SMTPAllowedRecipients, "Only allow SMTP recipients matching a regular expression (default allow all)")
@ -161,6 +162,9 @@ func initConfigFromEnv() {
auth.SetSMTPAuth(os.Getenv("MP_SMTP_AUTH"))
config.SMTPTLSCert = os.Getenv("MP_SMTP_TLS_CERT")
config.SMTPTLSKey = os.Getenv("MP_SMTP_TLS_KEY")
if getEnabledFromEnv("MP_SMTP_TLS_REQUIRED") {
config.SMTPTLSRequired = true
}
if getEnabledFromEnv("MP_SMTP_AUTH_ACCEPT_ANY") {
config.SMTPAuthAcceptAny = true
}

View File

@ -52,6 +52,11 @@ var (
// SMTPTLSKey file
SMTPTLSKey string
// SMTPTLSRequired to enforce TLS
// The only allowed commands are NOOP, EHLO, STARTTLS and QUIT (as specified in RFC 3207) until
// the connection is upgraded to TLS i.e. until STARTTLS is issued.
SMTPTLSRequired bool
// SMTPAuthFile for SMTP authentication
SMTPAuthFile string
@ -167,15 +172,15 @@ func VerifyConfig() error {
re := regexp.MustCompile(`.*:\d+$`)
if !re.MatchString(SMTPListen) {
return errors.New("SMTP bind should be in the format of <ip>:<port>")
return errors.New("[smtp] bind should be in the format of <ip>:<port>")
}
if !re.MatchString(HTTPListen) {
return errors.New("HTTP bind should be in the format of <ip>:<port>")
return errors.New("[ui] HTTP bind should be in the format of <ip>:<port>")
}
if UIAuthFile != "" {
if !isFile(UIAuthFile) {
return fmt.Errorf("HTTP password file not found: %s", UIAuthFile)
return fmt.Errorf("[ui] HTTP password file not found: %s", UIAuthFile)
}
b, err := os.ReadFile(UIAuthFile)
if err != nil {
@ -187,36 +192,42 @@ func VerifyConfig() error {
}
if UITLSCert != "" && UITLSKey == "" || UITLSCert == "" && UITLSKey != "" {
return errors.New("You must provide both a UI TLS certificate and a key")
return errors.New("[ui] you must provide both a UI TLS certificate and a key")
}
if UITLSCert != "" {
if !isFile(UITLSCert) {
return fmt.Errorf("TLS certificate not found: %s", UITLSCert)
return fmt.Errorf("[ui] TLS certificate not found: %s", UITLSCert)
}
if !isFile(UITLSKey) {
return fmt.Errorf("TLS key not found: %s", UITLSKey)
return fmt.Errorf("[ui] TLS key not found: %s", UITLSKey)
}
}
if SMTPTLSCert != "" && SMTPTLSKey == "" || SMTPTLSCert == "" && SMTPTLSKey != "" {
return errors.New("You must provide both an SMTP TLS certificate and a key")
return errors.New("[smtp] You must provide both an SMTP TLS certificate and a key")
}
if SMTPTLSCert != "" {
if !isFile(SMTPTLSCert) {
return fmt.Errorf("SMTP TLS certificate not found: %s", SMTPTLSCert)
return fmt.Errorf("[smtp] TLS certificate not found: %s", SMTPTLSCert)
}
if !isFile(SMTPTLSKey) {
return fmt.Errorf("SMTP TLS key not found: %s", SMTPTLSKey)
return fmt.Errorf("[smtp] TLS key not found: %s", SMTPTLSKey)
}
} else if SMTPTLSRequired {
return errors.New("[smtp] TLS cannot be required without an SMTP TLS certificate and key")
}
if SMTPTLSRequired && SMTPAuthAllowInsecure {
return errors.New("[smtp] TLS cannot be required while also allowing insecure authentication")
}
if SMTPAuthFile != "" {
if !isFile(SMTPAuthFile) {
return fmt.Errorf("SMTP password file not found: %s", SMTPAuthFile)
return fmt.Errorf("[smtp] password file not found: %s", SMTPAuthFile)
}
b, err := os.ReadFile(SMTPAuthFile)
@ -230,23 +241,23 @@ func VerifyConfig() error {
}
if auth.SMTPCredentials != nil && SMTPAuthAcceptAny {
return errors.New("SMTP authentication cannot use both credentials and --smtp-auth-accept-any")
return errors.New("[smtp] authentication cannot use both credentials and --smtp-auth-accept-any")
}
if SMTPTLSCert == "" && (auth.SMTPCredentials != nil || SMTPAuthAcceptAny) && !SMTPAuthAllowInsecure {
return errors.New("SMTP authentication requires TLS encryption, run with `--smtp-auth-allow-insecure` to allow insecure authentication")
return errors.New("[smtp] authentication requires TLS encryption, run with `--smtp-auth-allow-insecure` to allow insecure authentication")
}
validWebrootRe := regexp.MustCompile(`[^0-9a-zA-Z\/\-\_\.@]`)
if validWebrootRe.MatchString(Webroot) {
return fmt.Errorf("Invalid characters in Webroot (%s). Valid chars include: [a-z A-Z 0-9 _ . - / @]", Webroot)
return fmt.Errorf("invalid characters in Webroot (%s). Valid chars include: [a-z A-Z 0-9 _ . - / @]", Webroot)
}
s := strings.TrimRight(path.Join("/", Webroot, "/"), "/") + "/"
Webroot = s
if WebhookURL != "" && !isValidURL(WebhookURL) {
return fmt.Errorf("Webhook URL does not appear to be a valid URL (%s)", WebhookURL)
return fmt.Errorf("webhook URL does not appear to be a valid URL (%s)", WebhookURL)
}
if EnableSpamAssassin != "" {
@ -255,7 +266,6 @@ func VerifyConfig() error {
if err := spamassassin.Ping(); err != nil {
logger.Log().Warnf("[spamassassin] ping: %s", err.Error())
} else {
}
}
@ -269,15 +279,15 @@ func VerifyConfig() error {
if len(t) > 1 {
tag := tools.CleanTag(t[0])
if !ValidTagRegexp.MatchString(tag) || len(tag) == 0 {
return fmt.Errorf("Invalid tag (%s) - can only contain spaces, letters, numbers, - & _", tag)
return fmt.Errorf("[tag] invalid tag (%s) - can only contain spaces, letters, numbers, - & _", tag)
}
match := strings.TrimSpace(strings.ToLower(strings.Join(t[1:], "=")))
if len(match) == 0 {
return fmt.Errorf("Invalid tag match (%s) - no search detected", tag)
return fmt.Errorf("[tag] invalid tag match (%s) - no search detected", tag)
}
SMTPTags = append(SMTPTags, AutoTag{Tag: tag, Match: match})
} else {
return fmt.Errorf("Error parsing tags (%s)", a)
return fmt.Errorf("[tag] error parsing tags (%s)", a)
}
}
}
@ -285,7 +295,7 @@ func VerifyConfig() error {
if SMTPAllowedRecipients != "" {
restrictRegexp, err := regexp.Compile(SMTPAllowedRecipients)
if err != nil {
return fmt.Errorf("Failed to compile smtp-allowed-recipients regexp: %s", err.Error())
return fmt.Errorf("[smtp] failed to compile smtp-allowed-recipients regexp: %s", err.Error())
}
SMTPAllowedRecipientsRegexp = restrictRegexp
@ -297,7 +307,7 @@ func VerifyConfig() error {
}
if !ReleaseEnabled && SMTPRelayAllIncoming {
return errors.New("SMTP relay config must be set to relay all messages")
return errors.New("[smtp] relay config must be set to relay all messages")
}
if SMTPRelayAllIncoming {
@ -315,7 +325,7 @@ func parseRelayConfig(c string) error {
}
if !isFile(c) {
return fmt.Errorf("SMTP relay configuration not found: %s", SMTPRelayConfigFile)
return fmt.Errorf("[smtp] relay configuration not found: %s", SMTPRelayConfigFile)
}
data, err := os.ReadFile(c)
@ -328,7 +338,7 @@ func parseRelayConfig(c string) error {
}
if SMTPRelayConfig.Host == "" {
return errors.New("SMTP relay host not set")
return errors.New("[smtp] relay host not set")
}
if SMTPRelayConfig.Port == 0 {
@ -341,20 +351,20 @@ func parseRelayConfig(c string) error {
SMTPRelayConfig.Auth = "none"
} else if SMTPRelayConfig.Auth == "plain" {
if SMTPRelayConfig.Username == "" || SMTPRelayConfig.Password == "" {
return fmt.Errorf("SMTP relay host username or password not set for PLAIN authentication (%s)", c)
return fmt.Errorf("[smtp] relay host username or password not set for PLAIN authentication (%s)", c)
}
} else if SMTPRelayConfig.Auth == "login" {
SMTPRelayConfig.Auth = "login"
if SMTPRelayConfig.Username == "" || SMTPRelayConfig.Password == "" {
return fmt.Errorf("SMTP relay host username or password not set for LOGIN authentication (%s)", c)
return fmt.Errorf("[smtp] relay host username or password not set for LOGIN authentication (%s)", c)
}
} else if strings.HasPrefix(SMTPRelayConfig.Auth, "cram") {
SMTPRelayConfig.Auth = "cram-md5"
if SMTPRelayConfig.Username == "" || SMTPRelayConfig.Secret == "" {
return fmt.Errorf("SMTP relay host username or secret not set for CRAM-MD5 authentication (%s)", c)
return fmt.Errorf("[smtp] relay host username or secret not set for CRAM-MD5 authentication (%s)", c)
}
} else {
return fmt.Errorf("SMTP relay authentication method not supported: %s", SMTPRelayConfig.Auth)
return fmt.Errorf("[smtp] relay authentication method not supported: %s", SMTPRelayConfig.Auth)
}
ReleaseEnabled = true
@ -365,7 +375,7 @@ func parseRelayConfig(c string) error {
if SMTPRelayConfig.RecipientAllowlist != "" {
if err != nil {
return fmt.Errorf("Failed to compile relay recipient allowlist regexp: %s", err.Error())
return fmt.Errorf("[smtp] failed to compile relay recipient allowlist regexp: %s", err.Error())
}
SMTPRelayConfig.RecipientAllowlistRegexp = allowlistRegexp

6
go.mod
View File

@ -8,11 +8,11 @@ require (
github.com/axllent/semver v0.0.1
github.com/disintegration/imaging v1.6.2
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47
github.com/google/uuid v1.5.0
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.1
github.com/jhillyerd/enmime v1.1.0
github.com/klauspost/compress v1.17.4
github.com/klauspost/compress v1.17.5
github.com/leporo/sqlf v1.4.0
github.com/mhale/smtpd v0.8.2
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e
@ -60,7 +60,7 @@ require (
lukechampine.com/uint128 v1.3.0 // indirect
modernc.org/cc/v3 v3.41.0 // indirect
modernc.org/ccgo/v3 v3.16.15 // indirect
modernc.org/libc v1.40.6 // indirect
modernc.org/libc v1.40.7 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/opt v0.1.3 // indirect

12
go.sum
View File

@ -55,8 +55,8 @@ github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 h1:k4Tw0nt6lwr
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
@ -72,8 +72,8 @@ github.com/jhillyerd/enmime v1.1.0 h1:ubaIzg68VY7CMCe2YbHe6nkRvU9vujixTkNz3EBvZO
github.com/jhillyerd/enmime v1.1.0/go.mod h1:FRFuUPCLh8PByQv+8xRcLO9QHqaqTqreYhopv5eyk4I=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/compress v1.17.5 h1:d4vBd+7CHydUqpFBgUEKkSdtSugf9YFmSkvUYPquI5E=
github.com/klauspost/compress v1.17.5/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
@ -218,8 +218,8 @@ modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0=
modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/libc v1.40.6 h1:141JHq3SjhOOCjECBgD4K8VgTFOy19CnHwroC08DAig=
modernc.org/libc v1.40.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
modernc.org/libc v1.40.7 h1:oeLS0G067ZqUu+v143Dqad0btMfKmNS7SuOsnkq0Ysg=
modernc.org/libc v1.40.7/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=

View File

@ -203,7 +203,6 @@ func DeleteSearch(search string) error {
// SearchParser returns the SQL syntax for the database search based on the search arguments
func searchQueryBuilder(searchString string) *sqlf.Stmt {
searchString = strings.ToLower(searchString)
// group strings with quotes as a single argument and remove quotes
args := tools.ArgsParser(searchString)
@ -222,11 +221,15 @@ func searchQueryBuilder(searchString string) *sqlf.Stmt {
continue
}
// lowercase search to try match search prefixes
lw := strings.ToLower(w)
exclude := false
// search terms starting with a `-` or `!` imply an exclude
if len(w) > 1 && (strings.HasPrefix(w, "-") || strings.HasPrefix(w, "!")) {
exclude = true
w = w[1:]
lw = lw[1:]
}
re := regexp.MustCompile(`[a-zA-Z0-9]+`)
@ -234,7 +237,7 @@ func searchQueryBuilder(searchString string) *sqlf.Stmt {
continue
}
if strings.HasPrefix(w, "to:") {
if strings.HasPrefix(lw, "to:") {
w = cleanString(w[3:])
if w != "" {
if exclude {
@ -243,7 +246,7 @@ func searchQueryBuilder(searchString string) *sqlf.Stmt {
q.Where("ToJSON LIKE ?", "%"+escPercentChar(w)+"%")
}
}
} else if strings.HasPrefix(w, "from:") {
} else if strings.HasPrefix(lw, "from:") {
w = cleanString(w[5:])
if w != "" {
if exclude {
@ -252,7 +255,7 @@ func searchQueryBuilder(searchString string) *sqlf.Stmt {
q.Where("FromJSON LIKE ?", "%"+escPercentChar(w)+"%")
}
}
} else if strings.HasPrefix(w, "cc:") {
} else if strings.HasPrefix(lw, "cc:") {
w = cleanString(w[3:])
if w != "" {
if exclude {
@ -261,7 +264,7 @@ func searchQueryBuilder(searchString string) *sqlf.Stmt {
q.Where("CcJSON LIKE ?", "%"+escPercentChar(w)+"%")
}
}
} else if strings.HasPrefix(w, "bcc:") {
} else if strings.HasPrefix(lw, "bcc:") {
w = cleanString(w[4:])
if w != "" {
if exclude {
@ -270,7 +273,7 @@ func searchQueryBuilder(searchString string) *sqlf.Stmt {
q.Where("BccJSON LIKE ?", "%"+escPercentChar(w)+"%")
}
}
} else if strings.HasPrefix(w, "subject:") {
} else if strings.HasPrefix(lw, "subject:") {
w = w[8:]
if w != "" {
if exclude {
@ -279,7 +282,7 @@ func searchQueryBuilder(searchString string) *sqlf.Stmt {
q.Where("Subject LIKE ?", "%"+escPercentChar(w)+"%")
}
}
} else if strings.HasPrefix(w, "message-id:") {
} else if strings.HasPrefix(lw, "message-id:") {
w = cleanString(w[11:])
if w != "" {
if exclude {
@ -288,7 +291,7 @@ func searchQueryBuilder(searchString string) *sqlf.Stmt {
q.Where("MessageID LIKE ?", "%"+escPercentChar(w)+"%")
}
}
} else if strings.HasPrefix(w, "tag:") {
} else if strings.HasPrefix(lw, "tag:") {
w = cleanString(w[4:])
if w != "" {
if exclude {
@ -297,25 +300,25 @@ func searchQueryBuilder(searchString string) *sqlf.Stmt {
q.Where(`m.ID IN (SELECT mt.ID FROM message_tags mt JOIN tags t ON mt.TagID = t.ID WHERE t.Name = ?)`, w)
}
}
} else if w == "is:read" {
} else if lw == "is:read" {
if exclude {
q.Where("Read = 0")
} else {
q.Where("Read = 1")
}
} else if w == "is:unread" {
} else if lw == "is:unread" {
if exclude {
q.Where("Read = 1")
} else {
q.Where("Read = 0")
}
} else if w == "is:tagged" {
} else if lw == "is:tagged" {
if exclude {
q.Where(`m.ID NOT IN (SELECT DISTINCT mt.ID FROM message_tags mt JOIN tags t ON mt.TagID = t.ID)`)
} else {
q.Where(`m.ID IN (SELECT DISTINCT mt.ID FROM message_tags mt JOIN tags t ON mt.TagID = t.ID)`)
}
} else if w == "has:attachment" || w == "has:attachments" {
} else if lw == "has:attachment" || lw == "has:attachments" {
if exclude {
q.Where("Attachments = 0")
} else {
@ -324,9 +327,9 @@ func searchQueryBuilder(searchString string) *sqlf.Stmt {
} else {
// search text
if exclude {
q.Where("SearchText NOT LIKE ?", "%"+cleanString(escPercentChar(w))+"%")
q.Where("SearchText NOT LIKE ?", "%"+cleanString(escPercentChar(strings.ToLower(w)))+"%")
} else {
q.Where("SearchText LIKE ?", "%"+cleanString(escPercentChar(w))+"%")
q.Where("SearchText LIKE ?", "%"+cleanString(escPercentChar(strings.ToLower(w)))+"%")
}
}
}

View File

@ -152,3 +152,19 @@ func TestSearchDelete1100(t *testing.T) {
assertEqual(t, total, 0, "0 search results expected")
}
func TestEscPercentChar(t *testing.T) {
tests := map[string]string{}
tests["this is a test"] = "this is a test"
tests["this is% a test"] = "this is%% a test"
tests["this is%% a test"] = "this is%%%% a test"
tests["this is%%% a test"] = "this is%%%%%% a test"
tests["%this is% a test"] = "%%this is%% a test"
tests["Ä"] = "Ä"
tests["Ä%"] = "Ä%%"
for search, expected := range tests {
res := escPercentChar(search)
assertEqual(t, res, expected, "no match")
}
}

585
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -29,6 +29,9 @@ type webUIConfiguration struct {
// Whether SpamAssassin is enabled
SpamAssassin bool
// Whether messages with duplicate IDs are ignored
DuplicatesIgnored bool
}
// WebUIConfig returns configuration settings for the web UI.
@ -59,6 +62,7 @@ func WebUIConfig(w http.ResponseWriter, _ *http.Request) {
conf.DisableHTMLCheck = config.DisableHTMLCheck
conf.SpamAssassin = config.EnableSpamAssassin != ""
conf.DuplicatesIgnored = config.IgnoreDuplicateIDs
bytes, _ := json.Marshal(conf)

View File

@ -194,14 +194,16 @@ func TestAPIv1Search(t *testing.T) {
assertSearchEqual(t, ts.URL+"/api/v1/search", "from-1@example.com", 1)
assertSearchEqual(t, ts.URL+"/api/v1/search", "from:from-1@example.com", 1)
assertSearchEqual(t, ts.URL+"/api/v1/search", "-from:from-1@example.com", 99)
assertSearchEqual(t, ts.URL+"/api/v1/search", "-FROM:FROM-1@EXAMPLE.COM", 99)
assertSearchEqual(t, ts.URL+"/api/v1/search", "to:from-1@example.com", 0)
assertSearchEqual(t, ts.URL+"/api/v1/search", "from:@example.com", 100)
assertSearchEqual(t, ts.URL+"/api/v1/search", "subject:\"Subject line\"", 100)
assertSearchEqual(t, ts.URL+"/api/v1/search", "subject:\"Subject line 17 end\"", 1)
assertSearchEqual(t, ts.URL+"/api/v1/search", "subject:\"SUBJECT LINE 17 END\"", 1)
assertSearchEqual(t, ts.URL+"/api/v1/search", "!thisdoesnotexist", 100)
assertSearchEqual(t, ts.URL+"/api/v1/search", "-thisdoesnotexist", 100)
assertSearchEqual(t, ts.URL+"/api/v1/search", "-ThisDoesNotExist", 100)
assertSearchEqual(t, ts.URL+"/api/v1/search", "thisdoesnotexist", 0)
assertSearchEqual(t, ts.URL+"/api/v1/search", "tag:\"Test tag 065\"", 1)
assertSearchEqual(t, ts.URL+"/api/v1/search", "tag:\"TEST TAG 065\"", 1)
assertSearchEqual(t, ts.URL+"/api/v1/search", "!tag:\"Test tag 023\"", 99)
}

View File

@ -221,6 +221,7 @@ func listenAndServe(addr string, handler smtpd.Handler, authHandler smtpd.AuthHa
}
if config.SMTPTLSCert != "" {
srv.TLSRequired = config.SMTPTLSRequired
if err := srv.ConfigureTLS(config.SMTPTLSCert, config.SMTPTLSKey); err != nil {
return err
}

View File

@ -82,7 +82,7 @@ export default {
requestNotifications: function () {
// check if the browser supports notifications
if (!("Notification" in window)) {
alert("This browser does not support desktop notification")
alert("This browser does not support desktop notifications")
}
// we need to ask the user for permission
@ -259,7 +259,7 @@ export default {
{{ formatNumber(mailbox.appInfo.RuntimeStats.SMTPRejected) }}
</td>
</tr>
<tr>
<tr v-if="mailbox.uiConfig.DuplicatesIgnored">
<td>
SMTP messages ignored
</td>

View File

@ -1392,6 +1392,10 @@
"description": "Whether the HTML check has been globally disabled",
"type": "boolean"
},
"DuplicatesIgnored": {
"description": "Whether messages with duplicate IDs are ignored",
"type": "boolean"
},
"MessageRelay": {
"description": "Message Relay information",
"type": "object",