Compare commits

...

35 Commits

Author SHA1 Message Date
Ralph Slooten
009d02816f Merge branch 'release/v1.20.7' 2024-10-19 23:43:17 +13:00
Ralph Slooten
5131b6a0cc Release v1.20.7 2024-10-19 23:43:16 +13:00
Ralph Slooten
d2070e1ee1 Chore: Update caniemail database 2024-10-18 18:03:25 +13:00
Ralph Slooten
405babda7b Testing: Add tenantIDs to tests 2024-10-18 17:55:46 +13:00
Ralph Slooten
882adeebe3 SQL error deleting a tag while using tenant-id (take 2) 2024-10-17 22:41:41 +13:00
Ralph Slooten
f8efda0149 Fix: SQL error deleting a tag while using tenant-id (#374) 2024-10-17 22:30:58 +13:00
Ralph Slooten
d61304a854 Merge tag 'v1.20.6' into develop
Release v1.20.6
2024-10-14 17:42:20 +13:00
Ralph Slooten
4ff9fdf298 Merge branch 'release/v1.20.6' 2024-10-14 17:42:17 +13:00
Ralph Slooten
51e29ba90a Release v1.20.6 2024-10-14 17:42:17 +13:00
Ralph Slooten
9ab289a6c9 Chore: Bump Go compile version to 1.23 2024-10-14 17:39:52 +13:00
Ralph Slooten
2c945be5b9 Remove blank authentication from RapiDoc 2024-10-12 15:54:08 +13:00
Ralph Slooten
f9a185da46 Chore: Update node modules 2024-10-12 15:50:26 +13:00
Ralph Slooten
73a993492e Chore: Update swagger file tests 2024-10-12 15:30:33 +13:00
Ralph Slooten
a56fd1f53d Chore: Code cleanup 2024-10-12 15:20:11 +13:00
Ralph Slooten
073ddd33d5 Update stale issue action 2024-10-02 17:43:10 +02:00
Ralph Slooten
f142893d58 Chore: Update Go dependencies 2024-10-01 11:50:57 +02:00
Ralph Slooten
bd026bef8c Chore: Update minimum Go version (1.22.0) 2024-10-01 11:48:20 +02:00
Ralph Slooten
26e8706eb4 Chore: Update node dependencies 2024-10-01 11:40:41 +02:00
Ralph Slooten
ff8cd229ca Merge tag 'v1.20.5' into develop
Release v1.20.5
2024-09-26 17:20:28 +02:00
Ralph Slooten
2c326acf08 Merge branch 'release/v1.20.5' 2024-09-26 17:20:25 +02:00
Ralph Slooten
1aed5fda5a Release v1.20.5 2024-09-26 17:20:25 +02:00
Ralph Slooten
9a4982e646 Chore: Update node modules 2024-09-26 17:13:16 +02:00
cui fliter
a64950ddea Fix: Use correct parameter order in SpamAssassin socket detection (#364)
Signed-off-by: cuishuang <imcusg@gmail.com>
2024-09-27 03:04:52 +12:00
Ralph Slooten
7f4cd90c03 Add undocumented "demonstration mode" 2024-09-08 00:23:15 +12:00
Ralph Slooten
56f1138f8e Chore: Use consistent margins for Mailpit label if set 2024-09-07 17:34:42 +12:00
Ralph Slooten
bd5c450294 Chore: Improve tag detection in UI 2024-09-06 15:57:41 +12:00
Ralph Slooten
54a72e8e1e Chore: Improve link detection in the HTML preview 2024-09-05 17:46:02 +12:00
Ralph Slooten
069967f502 Merge tag 'v1.20.4' into develop
Release v1.20.4
2024-09-05 17:33:21 +12:00
Ralph Slooten
4ee3ba4753 Merge branch 'release/v1.20.4' 2024-09-05 17:33:19 +12:00
Ralph Slooten
84e46e6604 Release v1.20.4 2024-09-05 17:33:19 +12:00
Ralph Slooten
2048f15bbf Chore: Update Go modules 2024-09-05 17:32:03 +12:00
Ralph Slooten
93761b6f53 Chore: Update node modules 2024-09-05 17:31:05 +12:00
Ralph Slooten
2a0853d21a Fix: Relax URL detection in link check tool (#357) 2024-09-05 17:15:53 +12:00
Ralph Slooten
dc1a16ed5c Chore: Upgrade vue-css-donut-chart & related charts 2024-09-01 22:08:18 +12:00
Ralph Slooten
f95147fd83 Merge tag 'v1.20.3' into develop
Release v1.20.3
2024-09-01 19:56:00 +12:00
47 changed files with 1247 additions and 1086 deletions

View File

@@ -14,7 +14,7 @@ jobs:
with:
days-before-issue-stale: 7
days-before-issue-close: 3
exempt-issue-labels: "enhancement,bug,javascript,docker"
exempt-issue-labels: "enhancement,bug,awaiting feedback"
stale-issue-label: "stale"
stale-issue-message: "This issue has been marked as stale because it has been open for 7 days with no activity."
close-issue-message: "This issue was closed because there has been no activity since being marked as stale."

View File

@@ -26,7 +26,7 @@ jobs:
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- run: go test -p 1 ./internal/storage ./server ./server/pop3 ./internal/tools ./internal/html2text -v
- run: go test -p 1 ./internal/storage ./server ./server/pop3 ./internal/tools ./internal/html2text ./internal/linkcheck -v
- run: go test -p 1 ./internal/storage ./internal/html2text -bench=.
# build the assets
@@ -44,6 +44,6 @@ jobs:
# validate the swagger file
- name: Validate OpenAPI definition
if: startsWith(matrix.os, 'ubuntu') == true
uses: char0n/swagger-editor-validate@v1
uses: swaggerexpert/swagger-editor-validate@v1
with:
definition-file: server/ui/api/v1/swagger.json

View File

@@ -2,6 +2,53 @@
Notable changes to Mailpit will be documented in this file.
## [v1.20.7]
### Chore
- Update caniemail database
### Fix
- SQL error deleting a tag while using tenant-id ([#374](https://github.com/axllent/mailpit/issues/374))
### Testing
- Add tenantIDs to tests
## [v1.20.6]
### Chore
- Bump Go compile version to 1.23
- Update node modules
- Update swagger file tests
- Code cleanup
- Update Go dependencies
- Update minimum Go version (1.22.0)
- Update node dependencies
## [v1.20.5]
### Chore
- Update node modules
- Use consistent margins for Mailpit label if set
- Improve tag detection in UI
- Improve link detection in the HTML preview
### Fix
- Use correct parameter order in SpamAssassin socket detection ([#364](https://github.com/axllent/mailpit/issues/364))
## [v1.20.4]
### Chore
- Update Go modules
- Update node modules
- Upgrade vue-css-donut-chart & related charts
### Fix
- Relax URL detection in link check tool ([#357](https://github.com/axllent/mailpit/issues/357))
## [v1.20.3]
### Chore

View File

@@ -305,6 +305,9 @@ func initConfigFromEnv() {
if len(os.Getenv("MP_WEBHOOK_LIMIT")) > 0 {
webhook.RateLimit, _ = strconv.Atoi(os.Getenv("MP_WEBHOOK_LIMIT"))
}
// Demo mode
config.DemoMode = getEnabledFromEnv("MP_DEMO_MODE")
}
// load deprecated settings from environment and warn

View File

@@ -178,6 +178,9 @@ var (
// DisableHTMLCheck DEPRECATED 2024/04/13 - kept here to display console warning only
DisableHTMLCheck = false
// DemoMode disables SMTP relay, link checking & HTTP send functionality
DemoMode = false
)
// AutoTag struct for auto-tagging
@@ -230,14 +233,9 @@ func VerifyConfig() error {
return err
}
TenantID = tools.Normalize(TenantID)
TenantID = DBTenantID(TenantID)
if TenantID != "" {
logger.Log().Infof("[db] using tenant \"%s\"", TenantID)
re := regexp.MustCompile(`[^a-zA-Z0-9\_]`)
TenantID = re.ReplaceAllString(TenantID, "_")
if !strings.HasSuffix(TenantID, "_") {
TenantID = TenantID + "_"
}
}
re := regexp.MustCompile(`.*:\d+$`)
@@ -467,6 +465,12 @@ func VerifyConfig() error {
logger.Log().Warnf("[relay] auto-relaying all new messages via %s:%d", SMTPRelayConfig.Host, SMTPRelayConfig.Port)
}
if DemoMode {
MaxMessages = 1000
// this deserves a warning
logger.Log().Info("demo mode enabled")
}
return nil
}
@@ -623,3 +627,17 @@ func isValidURL(s string) bool {
return strings.HasPrefix(u.Scheme, "http")
}
// DBTenantID converts a tenant ID to a DB-friendly value if set
func DBTenantID(s string) string {
s = tools.Normalize(s)
if s != "" {
re := regexp.MustCompile(`[^a-zA-Z0-9\_]`)
s = re.ReplaceAllString(s, "_")
if !strings.HasSuffix(s, "_") {
s = s + "_"
}
}
return s
}

33
go.mod
View File

@@ -1,34 +1,34 @@
module github.com/axllent/mailpit
go 1.21.0
go 1.23
toolchain go1.22.1
toolchain go1.23.2
require (
github.com/PuerkitoBio/goquery v1.9.2
github.com/PuerkitoBio/goquery v1.10.0
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/axllent/semver v0.0.1
github.com/gomarkdown/markdown v0.0.0-20240730141124-034f12af3bf6
github.com/gomarkdown/markdown v0.0.0-20240930133441-72d49d9543d8
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.3
github.com/jhillyerd/enmime v1.3.0
github.com/klauspost/compress v1.17.9
github.com/klauspost/compress v1.17.11
github.com/kovidgoyal/imaging v1.6.3
github.com/leporo/sqlf v1.4.0
github.com/lithammer/shortuuid/v4 v4.0.0
github.com/mhale/smtpd v0.8.3
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e
github.com/rqlite/gorqlite v0.0.0-20240808172217-12ae7d03ef19
github.com/rqlite/gorqlite v0.0.0-20241013203532-4385768ae85d
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
github.com/tg123/go-htpasswd v1.2.2
github.com/vanng822/go-premailer v1.21.0
golang.org/x/net v0.28.0
golang.org/x/text v0.17.0
golang.org/x/time v0.6.0
github.com/vanng822/go-premailer v1.22.0
golang.org/x/net v0.30.0
golang.org/x/text v0.19.0
golang.org/x/time v0.7.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.32.0
modernc.org/sqlite v1.33.1
)
require (
@@ -54,12 +54,13 @@ 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/crypto v0.26.0 // indirect
golang.org/x/image v0.19.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
golang.org/x/image v0.21.0 // indirect
golang.org/x/sys v0.26.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect
modernc.org/libc v1.60.1 // indirect
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852 // indirect
modernc.org/libc v1.61.0 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/strutil v1.2.0 // indirect

110
go.sum
View File

@@ -1,8 +1,8 @@
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI=
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec=
github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
@@ -18,13 +18,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/gomarkdown/markdown v0.0.0-20240730141124-034f12af3bf6 h1:ZPy+2XJ8u0bB3sNFi+I72gMEMS7MTg7aZCCXPOjV8iw=
github.com/gomarkdown/markdown v0.0.0-20240730141124-034f12af3bf6/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/gomarkdown/markdown v0.0.0-20240930133441-72d49d9543d8 h1:4txT5G2kqVAKMjzidIabL/8KqjIK71yj30YOeuxLn10=
github.com/gomarkdown/markdown v0.0.0-20240930133441-72d49d9543d8/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -44,8 +45,8 @@ github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQykt
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jhillyerd/enmime v1.3.0 h1:LV5kzfLidiOr8qRGIpYYmUZCnhrPbcFAnAFUnWn99rw=
github.com/jhillyerd/enmime v1.3.0/go.mod h1:6c6jg5HdRRV2FtvVL69LjiX1M8oE0xDX9VEhV3oy4gs=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kovidgoyal/imaging v1.6.3 h1:iNPpv7ygiaB/NOztc6APMT7yr9UwBS+rOZwIbAdtyY8=
github.com/kovidgoyal/imaging v1.6.3/go.mod h1:sHvcLOOVhJuto2IoNdPLEqnAUoL5ZfHEF0PpNH+882g=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -89,8 +90,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rqlite/gorqlite v0.0.0-20240808172217-12ae7d03ef19 h1:uuWunw893WVwpSg4kNBuS6swgABwc+rwInVtwR5E3eM=
github.com/rqlite/gorqlite v0.0.0-20240808172217-12ae7d03ef19/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg=
github.com/rqlite/gorqlite v0.0.0-20241013203532-4385768ae85d h1:c88ius/WcN19inn14R+X2EQCFjjAu92txgdxNNnGxDI=
github.com/rqlite/gorqlite v0.0.0-20241013203532-4385768ae85d/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
@@ -104,49 +105,64 @@ github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02n
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tg123/go-htpasswd v1.2.2 h1:tmNccDsQ+wYsoRfiONzIhDm5OkVHQzN3w4FOBAlN6BY=
github.com/tg123/go-htpasswd v1.2.2/go.mod h1:FcIrK0J+6zptgVwK1JDlqyajW/1B4PtuJ/FLWl7nx8A=
github.com/unrolled/render v1.0.3/go.mod h1:gN9T0NhL4Bfbwu8ann7Ry/TGHYfosul+J0obPf6NBdM=
github.com/unrolled/render v1.7.0/go.mod h1:LwQSeDhjml8NLjIO9GJO1/1qpFJxtfVIpzxXKjfVkoI=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/vanng822/css v1.0.1 h1:10yiXc4e8NI8ldU6mSrWmSWMuyWgPr9DZ63RSlsgDw8=
github.com/vanng822/css v1.0.1/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w=
github.com/vanng822/go-premailer v1.21.0 h1:qIwX4urphNPO3xa60MGqowmyjzzMtFacJPKNrt1UWFU=
github.com/vanng822/go-premailer v1.21.0/go.mod h1:6Y3H2NzNmK3sFBNgR1ENdfV9hzG8hMzrA1nL/XBbbP4=
github.com/vanng822/go-premailer v1.22.0 h1:5gG92q3nG3BwcfUUDzrSDbYDbpwYC/lri4nba+vhdJQ=
github.com/vanng822/go-premailer v1.22.0/go.mod h1:K7DxRBW6AxdZUTqmW9jU6041CtfAWiP9uSXm2WmMB1k=
github.com/vanng822/r2router v0.0.0-20150523112421-1023140a4f30/go.mod h1:1BVq8p2jVr55Ost2PkZWDrG86PiJ/0lxqcXoAcGxvWU=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
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.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
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.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
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=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
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.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
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=
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.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -155,37 +171,51 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.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.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
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.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
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.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
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.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -203,10 +233,10 @@ modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a h1:CfbpOLEo2IwNzJdMvE8aiRbPMxoTpgAJeyePh0SmO8M=
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.60.1 h1:at373l8IFRTkJIkAU85BIuUoBM4T1b51ds0E1ovPG2s=
modernc.org/libc v1.60.1/go.mod h1:xJuobKuNxKH3RUatS7GjR+suWj+5c2K7bi4m/S5arOY=
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852 h1:IYXPPTTjjoSHvUClZIYexDiO7g+4x+XveKT4gCIAwiY=
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE=
modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0=
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.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
@@ -215,8 +245,8 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s=
modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=
modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@@ -78,5 +78,6 @@ func clean(text string) string {
}, text)
text = re.ReplaceAllString(text, " ")
return strings.TrimSpace(text)
}

View File

@@ -1,6 +1,6 @@
{
"api_version":"1.0.4",
"last_update_date":"2024-08-31 16:00:28 +0000",
"last_update_date":"2024-10-09 08:12:03 +0000",
"nicenames":{"family":{"gmail":"Gmail","outlook":"Outlook","yahoo":"Yahoo! Mail","apple-mail":"Apple Mail","aol":"AOL","thunderbird":"Mozilla Thunderbird","microsoft":"Microsoft","samsung-email":"Samsung Email","sfr":"SFR","orange":"Orange","protonmail":"ProtonMail","hey":"HEY","mail-ru":"Mail.ru","fastmail":"Fastmail","laposte":"LaPoste.net","t-online-de":"T-online.de","free-fr":"Free.fr","gmx":"GMX","web-de":"WEB.DE","ionos-1and1":"1&1","rainloop":"RainLoop","wp-pl":"WP.pl"},"platform":{"desktop-app":"Desktop","desktop-webmail":"Desktop Webmail","mobile-webmail":"Mobile Webmail","webmail":"Webmail","ios":"iOS","android":"Android","windows":"Windows","macos":"macOS","windows-mail":"Windows Mail","outlook-com":"Outlook.com"},"support":{"supported":"Supported","mitigated":"Partially supported","unsupported":"Not supported","unknown":"Support unknown","mixed":"Mixed support"},"category":{"html":"HTML","css":"CSS","image":"Image formats","others":"Others"}},
"data":[
{
@@ -1406,7 +1406,7 @@
"last_test_date":"2019-10-01",
"test_url":"https://www.caniemail.com/tests/css-margin.html",
"test_results_url":"https://app.emailonacid.com/app/acidtest/UmR6V6XenYY9bQiABuLGZRRrdP3fj2ZraiJjEyi4WKBho/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.4":"y"}},"gmail":{"desktop-webmail":{"2019-10":"a #1"},"ios":{"2019-10":"a #1"},"android":{"2019-10":"a #1"},"mobile-webmail":{"2020-02":"a #1"}},"orange":{"desktop-webmail":{"2019-10":"y","2021-03":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"outlook":{"windows":{"2003":"y","2007":"a #1 #2 #3 #4","2010":"a #1 #2 #3 #4","2013":"a #1 #2 #3 #4","2016":"a #1 #2 #3 #4","2019":"a #1 #2 #3 #4"},"windows-mail":{"2019-10":"a #1 #2 #3"},"macos":{"2011":"y","2016":"y","16.80":"a #1"},"outlook-com":{"2019-10":"a #1","2023-12":"a #1"},"ios":{"2.51.1":"y","4.3.1":"a #1"},"android":{"2019-10":"a #1"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"thunderbird":{"macos":{"60.3":"y"}},"aol":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"yahoo":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"a #1"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"y"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"y"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.4":"y"}},"gmail":{"desktop-webmail":{"2019-10":"a #1"},"ios":{"2019-10":"a #1"},"android":{"2019-10":"a #1"},"mobile-webmail":{"2020-02":"a #1"}},"orange":{"desktop-webmail":{"2019-10":"y","2021-03":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"outlook":{"windows":{"2003":"y","2007":"a #1 #2 #3 #4","2010":"a #1 #2 #3 #4","2013":"a #1 #2 #3 #4","2016":"a #1 #2 #3 #4","2019":"a #1 #2 #3 #4"},"windows-mail":{"2019-10":"a #1 #2 #3"},"macos":{"2011":"y","2016":"y","16.80":"a #1"},"outlook-com":{"2019-10":"a #1","2023-12":"a #1"},"ios":{"2.51.1":"y","4.3.1":"a #1"},"android":{"2019-10":"a #1"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"thunderbird":{"macos":{"60.3":"y"}},"aol":{"desktop-webmail":{"2019-10":"y","2024-10":"a #1"},"ios":{"2019-10":"y","2024-10":"a #1"},"android":{"2019-10":"y","2024-10":"a #1"}},"yahoo":{"desktop-webmail":{"2019-10":"y","2024-10":"a #1"},"ios":{"2019-10":"y","2024-10":"a #1"},"android":{"2019-10":"y","2024-10":"a #1"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"a #1"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"y"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"y"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Negative values are not supported.","2":"Partial. Not supported on `<span>` and `<body>` elements.","3":"Buggy. `background-color` is included inside the `margin`.","4":"Partial. `auto` value is not supported."}
},
@@ -3219,6 +3219,22 @@
"notes_by_num":{"1":"Buggy. `visibility:collapse` applied to a `<tr>` only hides content and does not \"remove\" it from layout.","2":"Partially supported. `visibility:collapse` is not supported."}
},
{
"slug":"css-white-space-collapse",
"title":"white-space-collapse",
"description":"Controls how white space inside an element is collapsed.",
"url":"https://www.caniemail.com/features/css-white-space-collapse/",
"category":"css",
"tags":[],
"keywords":"break, space, collapse, hide",
"last_test_date":"2024-09-04",
"test_url":"https://www.caniemail.com/tests/css-white-space-collapse.html",
"test_results_url":"https://testi.at/proj/e6y4s3zytp5kty7kcg",
"stats":{"apple-mail":{"macos":{"10":"n","11":"n","12":"n","13":"n","14":"y #1"},"ios":{"15":"y #1"}},"gmail":{"desktop-webmail":{"2024-09":"n"},"ios":{"2024-09":"n"},"android":{"2024-09":"n"},"mobile-webmail":{"2024-09":"n"}},"orange":{"desktop-webmail":{"2024-09":"u"},"ios":{"2024-09":"u"},"android":{"2024-09":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2024-09":"n"},"macos":{"2024-09":"n"},"outlook-com":{"2024-09":"n"},"ios":{"2024-09":"n"},"android":{"2024-09":"n"}},"yahoo":{"desktop-webmail":{"2024-09":"n"},"ios":{"2024-09":"n"},"android":{"2024-09":"n"}},"aol":{"desktop-webmail":{"2024-09":"n"},"ios":{"2024-09":"n"},"android":{"2024-09":"n"}},"samsung-email":{"android":{"2024-09":"y #1"}},"sfr":{"desktop-webmail":{"2024-03":"u"},"ios":{"2024-03":"u"},"android":{"2024-03":"u"}},"protonmail":{"desktop-webmail":{"2024-09":"u"},"ios":{"2024-09":"u"},"android":{"2024-09":"u"}},"hey":{"desktop-webmail":{"2024-09":"u"}},"mail-ru":{"desktop-webmail":{"2024-09":"y #1"}},"fastmail":{"desktop-webmail":{"2024-09":"u"}}},
"notes":null,
"notes_by_num":{"1":"Partial. `preserve-spaces` value works only on Firefox."}
},
{
"slug":"css-white-space",
"title":"white-space",
@@ -4734,7 +4750,7 @@
"last_test_date":"2023-01-15",
"test_url":"https://www.caniemail.com/tests/images.html",
"test_results_url":"https://app.emailonacid.com/app/acidtest/xm1T5nQ1MKtHpVSJidhagmt3Z53CjqbkMhorlvuM0Gz57/list",
"stats":{"apple-mail":{"macos":{"13":"n","14":"y"},"ios":{"13":"n","14":"n","15":"y"}},"gmail":{"desktop-webmail":{"2020-02":"n","2023-01":"n","2024-07":"n"},"ios":{"2020-02":"a #1","2023-01":"a #1"},"android":{"2020-02":"a #1","2023-01":"a #1"},"mobile-webmail":{"2020-02":"n","2023-01":"n"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-02":"y"},"macos":{"2016":"n","13.1":"y","16.80":"y"},"outlook-com":{"2020-02":"y","2024-01":"y"},"ios":{"2020-02":"n","2023-01":"y"},"android":{"2020-02":"y"}},"samsung-email":{"android":{"9.0":"y"}},"thunderbird":{"windows":{"2020-02":"y"},"macos":{"68.4":"y"}},"aol":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"yahoo":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2020-02":"y","2021-03":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"sfr":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"n","2023-01":"n"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-09":"y"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"web-de":{"desktop-webmail":{"2022-09":"y"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-09":"y"},"android":{"2022-09":"y"}}},
"stats":{"apple-mail":{"macos":{"13":"n","14":"y"},"ios":{"13":"n","14":"n","15":"y"}},"gmail":{"desktop-webmail":{"2020-02":"n","2023-01":"n","2024-07":"n"},"ios":{"2020-02":"a #1","2023-01":"a #1"},"android":{"2020-02":"a #1","2023-01":"a #1"},"mobile-webmail":{"2020-02":"n","2023-01":"n"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"y"},"windows-mail":{"2020-02":"y"},"macos":{"2016":"n","13.1":"y","16.80":"y"},"outlook-com":{"2020-02":"y","2024-01":"y"},"ios":{"2020-02":"n","2023-01":"y"},"android":{"2020-02":"y"}},"samsung-email":{"android":{"9.0":"y"}},"thunderbird":{"windows":{"2020-02":"y"},"macos":{"68.4":"y"}},"aol":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"yahoo":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2020-02":"y","2021-03":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"sfr":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"n","2023-01":"n"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-09":"y"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"web-de":{"desktop-webmail":{"2022-09":"y"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-09":"y"},"android":{"2022-09":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partially supported. Only works with non Google accounts."}
},

View File

@@ -0,0 +1,71 @@
package linkcheck
import (
"reflect"
"testing"
"github.com/axllent/mailpit/internal/storage"
)
var (
testHTML = `
<html>
<head>
<link rel=stylesheet href="http://remote-host/style.css"></link>
<script async src="https://www.googletagmanager.com/gtag/js?id=ignored"></script>
</head>
<body>
<div>
<p><a href="http://example.com">HTTP link</a></p>
<p><a href="https://example.com">HTTPS link</a></p>
<p><a href="HTTPS://EXAMPLE.COM">HTTPS link</a></p>
<p><a href="http://localhost">Localhost link</a> (ignored)</p>
<p><a href="https://localhost">Localhost link</a> (ignored)</p>
<p><a href='https://127.0.0.1'>Single quotes link</a> (ignored)</p>
<p><img src=https://example.com/image.jpg></p>
<p href="http://invalid-link.com">This should be ignored</p>
<p><a href="http://link with spaces">Link with spaces</a></p>
<p><a href="http://example.com/?blaah=yes&amp;test=true">URL-encoded characters</a></p>
</div>
</body>
</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://remote-host/style.css", // css
"https://example.com/image.jpg", // images
}
testTextLinks = `This is a line with http://example.com https://example.com
HTTPS://EXAMPLE.COM
[http://localhost]
www.google.com < ignored
|||http://example.com/?some=query-string|||
`
expectedTextLinks = []string{
"http://example.com", "https://example.com", "HTTPS://EXAMPLE.COM", "http://localhost", "http://example.com/?some=query-string",
}
)
func TestLinkDetection(t *testing.T) {
t.Log("Testing HTML link detection")
m := storage.Message{}
m.Text = testTextLinks
m.HTML = testHTML
textLinks := extractTextLinks(&m)
if !reflect.DeepEqual(textLinks, expectedTextLinks) {
t.Fatalf("Failed to detect text links correctly")
}
htmlLinks := extractHTMLLinks(&m)
if !reflect.DeepEqual(htmlLinks, expectedHTMLLinks) {
t.Fatalf("Failed to detect HTML links correctly")
}
}

View File

@@ -10,7 +10,7 @@ import (
"github.com/axllent/mailpit/internal/tools"
)
var linkRe = regexp.MustCompile(`(?m)\b(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:'!\/~+#-]*[\w@?^=%&\/~+#-])`)
var linkRe = regexp.MustCompile(`(?im)\b(http|https):\/\/([\-\w@:%_\+'!.~#?,&\/\/=;]+)`)
// RunTests will run all tests on an HTML string
func RunTests(msg *storage.Message, followRedirects bool) (Response, error) {

View File

@@ -30,14 +30,17 @@ type Conn struct {
// Opt represents the client configuration.
type Opt struct {
// Host name
Host string `json:"host"`
Port int `json:"port"`
// Default is 3 seconds.
// Port number
Port int `json:"port"`
// DialTimeout default is 3 seconds.
DialTimeout time.Duration `json:"dial_timeout"`
Dialer Dialer `json:"-"`
TLSEnabled bool `json:"tls_enabled"`
// Dialer
Dialer Dialer `json:"-"`
// TLSEnabled sets whether SLS is enabled
TLSEnabled bool `json:"tls_enabled"`
// TLSSkipVerify skips TLS verification (ie: self-signed)
TLSSkipVerify bool `json:"tls_skip_verify"`
}
@@ -49,16 +52,15 @@ type Dialer interface {
// MessageID contains the ID and size of an individual message.
type MessageID struct {
// ID is the numerical index (non-unique) of the message.
ID int
ID int
// Size in bytes
Size int
// UID is only present if the response is to the UIDL command.
UID string
}
var (
lineBreak = []byte("\r\n")
lineBreak = []byte("\r\n")
respOK = []byte("+OK") // `+OK` without additional info
respOKInfo = []byte("+OK ") // `+OK <info>`
respErr = []byte("-ERR") // `-ERR` without additional info
@@ -126,6 +128,7 @@ func (c *Conn) Send(b string) error {
if _, err := c.w.WriteString(b + "\r\n"); err != nil {
return err
}
return c.w.Flush()
}
@@ -223,12 +226,14 @@ func (c *Conn) Auth(user, password string) error {
// User sends the username to the server.
func (c *Conn) User(s string) error {
_, err := c.Cmd("USER", false, s)
return err
}
// Pass sends the password to the server.
func (c *Conn) Pass(s string) error {
_, err := c.Cmd("PASS", false, s)
return err
}

View File

@@ -71,7 +71,7 @@ func Ping() error {
}
var client *spamc.Client
if strings.HasPrefix("unix:", service) {
if strings.HasPrefix(service, "unix:") {
client = spamc.NewUnix(strings.TrimLeft(service, "unix:"))
} else {
client = spamc.NewTCP(service, timeout)
@@ -112,7 +112,7 @@ func Check(msg []byte) (Result, error) {
}
} else {
var client *spamc.Client
if strings.HasPrefix("unix:", service) {
if strings.HasPrefix(service, "unix:") {
client = spamc.NewUnix(strings.TrimLeft(service, "unix:"))
} else {
client = spamc.NewTCP(service, timeout)

View File

@@ -13,6 +13,8 @@ import (
"strconv"
"strings"
"time"
"github.com/axllent/mailpit/internal/tools"
)
// ProtoVersion is the protocol version
@@ -81,6 +83,7 @@ func (c *Client) dial() (connection, error) {
}
return net.DialUnix("unix", nil, unixAddr)
}
panic("Client.net must be either \"tcp\" or \"unix\"")
}
@@ -107,26 +110,25 @@ func (c *Client) report(email []byte) ([]string, error) {
}
bw := bufio.NewWriter(conn)
_, err = bw.WriteString("REPORT SPAMC/" + ProtoVersion + "\r\n")
if err != nil {
if _, err := bw.WriteString("REPORT SPAMC/" + ProtoVersion + "\r\n"); err != nil {
return nil, err
}
_, err = bw.WriteString("Content-length: " + strconv.Itoa(len(email)) + "\r\n\r\n")
if err != nil {
if _, err := bw.WriteString("Content-length: " + strconv.Itoa(len(email)) + "\r\n\r\n"); err != nil {
return nil, err
}
_, err = bw.Write(email)
if err != nil {
if _, err := bw.Write(email); err != nil {
return nil, err
}
err = bw.Flush()
if err != nil {
if err := bw.Flush(); err != nil {
return nil, err
}
// Client is supposed to close its writing side of the connection
// after sending its request.
err = conn.CloseWrite()
if err != nil {
if err := conn.CloseWrite(); err != nil {
return nil, err
}
@@ -134,6 +136,7 @@ func (c *Client) report(email []byte) ([]string, error) {
lines []string
br = bufio.NewReader(conn)
)
for {
line, err := br.ReadString('\n')
if err == io.EOF {
@@ -171,11 +174,12 @@ func (c *Client) parseOutput(output []string) Result {
continue
}
}
// summary
if spamMainRe.MatchString(row) {
res := spamMainRe.FindStringSubmatch(row)
if len(res) == 4 {
if strings.ToLower(res[1]) == "true" || strings.ToLower(res[1]) == "yes" {
if tools.InArray(res[1], []string{"true", "yes"}) {
result.Spam = true
} else {
result.Spam = false
@@ -197,8 +201,8 @@ func (c *Client) parseOutput(output []string) Result {
reachedRules = true
continue
}
// details
// row = strings.Trim(row, " \t\r\n")
if reachedRules && spamDetailsRe.MatchString(row) {
res := spamDetailsRe.FindStringSubmatch(row)
if len(res) == 5 {
@@ -207,6 +211,7 @@ func (c *Client) parseOutput(output []string) Result {
}
}
}
return result
}
@@ -222,12 +227,11 @@ func (c *Client) Ping() error {
return err
}
_, err = io.WriteString(conn, fmt.Sprintf("PING SPAMC/%s\r\n\r\n", ProtoVersion))
if err != nil {
if _, err := io.WriteString(conn, fmt.Sprintf("PING SPAMC/%s\r\n\r\n", ProtoVersion)); err != nil {
return err
}
err = conn.CloseWrite()
if err != nil {
if err := conn.CloseWrite(); err != nil {
return err
}
@@ -241,5 +245,6 @@ func (c *Client) Ping() error {
return err
}
}
return nil
}

View File

@@ -61,25 +61,32 @@ func pruneMessages() {
// prune using `--max` if set
if config.MaxMessages > 0 {
q := sqlf.Select("ID, Size").
From(tenant("mailbox")).
OrderBy("Created DESC").
Limit(5000).
Offset(config.MaxMessages)
total := CountTotal()
if total > float64(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 err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
var id string
if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
var id string
if err := row.Scan(&id, &size); err != nil {
if err := row.Scan(&id, &size); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return
}
ids = append(ids, id)
prunedSize = prunedSize + int64(size)
}); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return
}
ids = append(ids, id)
prunedSize = prunedSize + int64(size)
}); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return
}
}
@@ -166,6 +173,10 @@ func pruneMessages() {
logMessagesDeleted(len(ids))
if config.DemoMode {
vacuumDb()
}
websockets.Broadcast("prune", nil)
}

View File

@@ -39,8 +39,12 @@ var (
// InitDB will initialise the database
func InitDB() error {
var (
dsn string
err error
)
p := config.Database
var dsn string
if p == "" {
// when no path is provided then we create a temporary file
@@ -74,8 +78,6 @@ func InitDB() error {
}
}
var err error
db, err = sql.Open(sqlDriver, dsn)
if err != nil {
return err

View File

@@ -430,6 +430,21 @@ func GetAttachmentPart(id, partID string) (*enmime.Part, error) {
return nil, errors.New("attachment not found")
}
// AttachmentSummary returns a summary of the attachment without any binary data
func AttachmentSummary(a *enmime.Part) Attachment {
o := Attachment{}
o.PartID = a.PartID
o.FileName = a.FileName
if o.FileName == "" {
o.FileName = a.ContentID
}
o.ContentType = a.ContentType
o.ContentID = a.ContentID
o.Size = float64(len(a.Content))
return o
}
// LatestID returns the latest message ID
//
// If a query argument is set in the request the function will return the

View File

@@ -3,10 +3,12 @@ package storage
import (
"testing"
"time"
"github.com/axllent/mailpit/config"
)
func TestTextEmailInserts(t *testing.T) {
setup()
setup("")
defer Close()
t.Log("Testing text email storage")
@@ -38,113 +40,140 @@ func TestTextEmailInserts(t *testing.T) {
}
func TestMimeEmailInserts(t *testing.T) {
setup()
defer Close()
for _, tenantID := range []string{"", "MyServer 3", "host.example.com"} {
tenantID = config.DBTenantID(tenantID)
t.Log("Testing mime email storage")
setup(tenantID)
start := time.Now()
if tenantID == "" {
t.Log("Testing mime email storage")
} else {
t.Logf("Testing mime email storage (tenant %s)", tenantID)
}
start := time.Now()
for i := 0; i < testRuns; i++ {
if _, err := Store(&testMimeEmail); err != nil {
t.Log("error ", err)
t.Fail()
}
}
assertEqual(t, CountTotal(), float64(testRuns), "Incorrect number of mime emails stored")
t.Logf("Inserted %d text emails in %s", testRuns, time.Since(start))
delStart := time.Now()
if err := DeleteAllMessages(); err != nil {
t.Log("error ", err)
t.Fail()
}
assertEqual(t, CountTotal(), float64(0), "incorrect number of mime emails deleted")
t.Logf("Deleted %d mime emails in %s", testRuns, time.Since(delStart))
Close()
}
}
func TestRetrieveMimeEmail(t *testing.T) {
for _, tenantID := range []string{"", "MyServer 3", "host.example.com"} {
tenantID = config.DBTenantID(tenantID)
setup(tenantID)
if tenantID == "" {
t.Log("Testing mime email retrieval")
} else {
t.Logf("Testing mime email retrieval (tenant %s)", tenantID)
}
id, err := Store(&testMimeEmail)
if err != nil {
t.Log("error ", err)
t.Fail()
}
msg, err := GetMessage(id)
if err != nil {
t.Log("error ", err)
t.Fail()
}
assertEqual(t, msg.From.Name, "Sender Smith", "\"From\" name does not match")
assertEqual(t, msg.From.Address, "sender2@example.com", "\"From\" address does not match")
assertEqual(t, msg.Subject, "inline + attachment", "subject does not match")
assertEqual(t, len(msg.To), 1, "incorrect number of recipients")
assertEqual(t, msg.To[0].Name, "Recipient Ross", "\"To\" name does not match")
assertEqual(t, msg.To[0].Address, "recipient2@example.com", "\"To\" address does not match")
assertEqual(t, len(msg.Attachments), 1, "incorrect number of attachments")
assertEqual(t, msg.Attachments[0].FileName, "Sample PDF.pdf", "attachment filename does not match")
assertEqual(t, len(msg.Inline), 1, "incorrect number of inline attachments")
assertEqual(t, msg.Inline[0].FileName, "inline-image.jpg", "inline attachment filename does not match")
attachmentData, err := GetAttachmentPart(id, msg.Attachments[0].PartID)
if err != nil {
t.Log("error ", err)
t.Fail()
}
assertEqual(t, float64(len(attachmentData.Content)), msg.Attachments[0].Size, "attachment size does not match")
inlineData, err := GetAttachmentPart(id, msg.Inline[0].PartID)
if err != nil {
t.Log("error ", err)
t.Fail()
}
assertEqual(t, float64(len(inlineData.Content)), msg.Inline[0].Size, "inline attachment size does not match")
Close()
}
}
func TestMessageSummary(t *testing.T) {
for _, tenantID := range []string{"", "MyServer 3", "host.example.com"} {
tenantID = config.DBTenantID(tenantID)
setup(tenantID)
if tenantID == "" {
t.Log("Testing message summary")
} else {
t.Logf("Testing message summary (tenant %s)", tenantID)
}
for i := 0; i < testRuns; i++ {
if _, err := Store(&testMimeEmail); err != nil {
t.Log("error ", err)
t.Fail()
}
summaries, err := List(0, 0, 1)
if err != nil {
t.Log("error ", err)
t.Fail()
}
assertEqual(t, len(summaries), 1, "Expected 1 result")
msg := summaries[0]
assertEqual(t, msg.From.Name, "Sender Smith", "\"From\" name does not match")
assertEqual(t, msg.From.Address, "sender2@example.com", "\"From\" address does not match")
assertEqual(t, msg.Subject, "inline + attachment", "subject does not match")
assertEqual(t, len(msg.To), 1, "incorrect number of recipients")
assertEqual(t, msg.To[0].Name, "Recipient Ross", "\"To\" name does not match")
assertEqual(t, msg.To[0].Address, "recipient2@example.com", "\"To\" address does not match")
assertEqual(t, msg.Snippet, "Message with inline image and attachment:", "\"Snippet\" does does not match")
assertEqual(t, msg.Attachments, 1, "Expected 1 attachment")
assertEqual(t, msg.MessageID, "33af2ac1-c33d-9738-35e3-a6daf90bbd89@gmail.com", "\"MessageID\" does not match")
Close()
}
assertEqual(t, CountTotal(), float64(testRuns), "Incorrect number of mime emails stored")
t.Logf("Inserted %d text emails in %s", testRuns, time.Since(start))
delStart := time.Now()
if err := DeleteAllMessages(); err != nil {
t.Log("error ", err)
t.Fail()
}
assertEqual(t, CountTotal(), float64(0), "incorrect number of mime emails deleted")
t.Logf("Deleted %d mime emails in %s", testRuns, time.Since(delStart))
}
func TestRetrieveMimeEmail(t *testing.T) {
setup()
defer Close()
t.Log("Testing mime email retrieval")
id, err := Store(&testMimeEmail)
if err != nil {
t.Log("error ", err)
t.Fail()
}
msg, err := GetMessage(id)
if err != nil {
t.Log("error ", err)
t.Fail()
}
assertEqual(t, msg.From.Name, "Sender Smith", "\"From\" name does not match")
assertEqual(t, msg.From.Address, "sender2@example.com", "\"From\" address does not match")
assertEqual(t, msg.Subject, "inline + attachment", "subject does not match")
assertEqual(t, len(msg.To), 1, "incorrect number of recipients")
assertEqual(t, msg.To[0].Name, "Recipient Ross", "\"To\" name does not match")
assertEqual(t, msg.To[0].Address, "recipient2@example.com", "\"To\" address does not match")
assertEqual(t, len(msg.Attachments), 1, "incorrect number of attachments")
assertEqual(t, msg.Attachments[0].FileName, "Sample PDF.pdf", "attachment filename does not match")
assertEqual(t, len(msg.Inline), 1, "incorrect number of inline attachments")
assertEqual(t, msg.Inline[0].FileName, "inline-image.jpg", "inline attachment filename does not match")
attachmentData, err := GetAttachmentPart(id, msg.Attachments[0].PartID)
if err != nil {
t.Log("error ", err)
t.Fail()
}
assertEqual(t, float64(len(attachmentData.Content)), msg.Attachments[0].Size, "attachment size does not match")
inlineData, err := GetAttachmentPart(id, msg.Inline[0].PartID)
if err != nil {
t.Log("error ", err)
t.Fail()
}
assertEqual(t, float64(len(inlineData.Content)), msg.Inline[0].Size, "inline attachment size does not match")
}
func TestMessageSummary(t *testing.T) {
setup()
defer Close()
t.Log("Testing message summary")
if _, err := Store(&testMimeEmail); err != nil {
t.Log("error ", err)
t.Fail()
}
summaries, err := List(0, 0, 1)
if err != nil {
t.Log("error ", err)
t.Fail()
}
assertEqual(t, len(summaries), 1, "Expected 1 result")
msg := summaries[0]
assertEqual(t, msg.From.Name, "Sender Smith", "\"From\" name does not match")
assertEqual(t, msg.From.Address, "sender2@example.com", "\"From\" address does not match")
assertEqual(t, msg.Subject, "inline + attachment", "subject does not match")
assertEqual(t, len(msg.To), 1, "incorrect number of recipients")
assertEqual(t, msg.To[0].Name, "Recipient Ross", "\"To\" name does not match")
assertEqual(t, msg.To[0].Address, "recipient2@example.com", "\"To\" address does not match")
assertEqual(t, msg.Snippet, "Message with inline image and attachment:", "\"Snippet\" does does not match")
assertEqual(t, msg.Attachments, 1, "Expected 1 attachment")
assertEqual(t, msg.MessageID, "33af2ac1-c33d-9738-35e3-a6daf90bbd89@gmail.com", "\"MessageID\" does not match")
}
func BenchmarkImportText(b *testing.B) {
setup()
setup("")
defer Close()
for i := 0; i < b.N; i++ {
@@ -156,7 +185,7 @@ func BenchmarkImportText(b *testing.B) {
}
func BenchmarkImportMime(b *testing.B) {
setup()
setup("")
defer Close()
for i := 0; i < b.N; i++ {

View File

@@ -43,10 +43,14 @@ func ReindexAll() {
logger.Log().Infof("reindexing %d messages", total)
type updateStruct struct {
ID string
// ID in database
ID string
// SearchText for searching
SearchText string
Snippet string
Metadata string
// Snippet for UI
Snippet string
// Metadata info
Metadata string
}
parser := enmime.NewParser(enmime.DisableCharacterDetection(true))
@@ -137,5 +141,6 @@ func chunkBy[T any](items []T, chunkSize int) (chunks [][]T) {
for chunkSize < len(items) {
items, chunks = items[chunkSize:], append(chunks, items[0:chunkSize:chunkSize])
}
return append(chunks, items)
}

View File

@@ -198,6 +198,7 @@ func DeleteSearch(search, timezone string) error {
}
dbLastAction = time.Now()
addDeletedSize(int64(deleteSize))
logMessagesDeleted(total)

View File

@@ -6,133 +6,154 @@ import (
"math/rand"
"testing"
"github.com/axllent/mailpit/config"
"github.com/jhillyerd/enmime"
)
func TestSearch(t *testing.T) {
setup()
defer Close()
for _, tenantID := range []string{"", "MyServer 3", "host.example.com"} {
tenantID = config.DBTenantID(tenantID)
t.Log("Testing search")
for i := 0; i < testRuns; i++ {
msg := enmime.Builder().
From(fmt.Sprintf("From %d", i), fmt.Sprintf("from-%d@example.com", i)).
CC(fmt.Sprintf("CC %d", i), fmt.Sprintf("cc-%d@example.com", i)).
CC(fmt.Sprintf("CC2 %d", i), fmt.Sprintf("cc2-%d@example.com", i)).
Subject(fmt.Sprintf("Subject line %d end", i)).
Text([]byte(fmt.Sprintf("This is the email body %d <jdsauk;dwqmdqw;>.", i))).
To(fmt.Sprintf("To %d", i), fmt.Sprintf("to-%d@example.com", i)).
To(fmt.Sprintf("To2 %d", i), fmt.Sprintf("to2-%d@example.com", i)).
ReplyTo(fmt.Sprintf("Reply To %d", i), fmt.Sprintf("reply-to-%d@example.com", i))
setup(tenantID)
env, err := msg.Build()
if tenantID == "" {
t.Log("Testing search")
} else {
t.Logf("Testing search (tenant %s)", tenantID)
}
for i := 0; i < testRuns; i++ {
msg := enmime.Builder().
From(fmt.Sprintf("From %d", i), fmt.Sprintf("from-%d@example.com", i)).
CC(fmt.Sprintf("CC %d", i), fmt.Sprintf("cc-%d@example.com", i)).
CC(fmt.Sprintf("CC2 %d", i), fmt.Sprintf("cc2-%d@example.com", i)).
Subject(fmt.Sprintf("Subject line %d end", i)).
Text([]byte(fmt.Sprintf("This is the email body %d <jdsauk;dwqmdqw;>.", i))).
To(fmt.Sprintf("To %d", i), fmt.Sprintf("to-%d@example.com", i)).
To(fmt.Sprintf("To2 %d", i), fmt.Sprintf("to2-%d@example.com", i)).
ReplyTo(fmt.Sprintf("Reply To %d", i), fmt.Sprintf("reply-to-%d@example.com", i))
env, err := msg.Build()
if err != nil {
t.Log("error ", err)
t.Fail()
}
buf := new(bytes.Buffer)
if err := env.Encode(buf); err != nil {
t.Log("error ", err)
t.Fail()
}
bufBytes := buf.Bytes()
if _, err := Store(&bufBytes); err != nil {
t.Log("error ", err)
t.Fail()
}
}
for i := 1; i < 51; i++ {
// search a random something that will return a single result
uniqueSearches := []string{
fmt.Sprintf("from-%d@example.com", i),
fmt.Sprintf("from:from-%d@example.com", i),
fmt.Sprintf("to-%d@example.com", i),
fmt.Sprintf("to:to-%d@example.com", i),
fmt.Sprintf("to2-%d@example.com", i),
fmt.Sprintf("to:to2-%d@example.com", i),
fmt.Sprintf("cc-%d@example.com", i),
fmt.Sprintf("cc:cc-%d@example.com", i),
fmt.Sprintf("cc2-%d@example.com", i),
fmt.Sprintf("cc:cc2-%d@example.com", i),
fmt.Sprintf("reply-to-%d@example.com", i),
fmt.Sprintf("reply-to:\"reply-to-%d@example.com\"", i),
fmt.Sprintf("\"Subject line %d end\"", i),
fmt.Sprintf("subject:\"Subject line %d end\"", i),
fmt.Sprintf("\"the email body %d jdsauk dwqmdqw\"", i),
}
searchIdx := rand.Intn(len(uniqueSearches))
search := uniqueSearches[searchIdx]
summaries, _, err := Search(search, "", 0, 0, 100)
if err != nil {
t.Log("error ", err)
t.Fail()
}
assertEqual(t, len(summaries), 1, "search result expected")
assertEqual(t, summaries[0].From.Name, fmt.Sprintf("From %d", i), "\"From\" name does not match")
assertEqual(t, summaries[0].From.Address, fmt.Sprintf("from-%d@example.com", i), "\"From\" address does not match")
assertEqual(t, summaries[0].To[0].Name, fmt.Sprintf("To %d", i), "\"To\" name does not match")
assertEqual(t, summaries[0].To[0].Address, fmt.Sprintf("to-%d@example.com", i), "\"To\" address does not match")
assertEqual(t, summaries[0].Subject, fmt.Sprintf("Subject line %d end", i), "\"Subject\" does not match")
}
// search something that will return 200 results
summaries, _, err := Search("This is the email body", "", 0, 0, testRuns)
if err != nil {
t.Log("error ", err)
t.Fail()
}
assertEqual(t, len(summaries), testRuns, "search results expected")
buf := new(bytes.Buffer)
if err := env.Encode(buf); err != nil {
t.Log("error ", err)
t.Fail()
}
bufBytes := buf.Bytes()
if _, err := Store(&bufBytes); err != nil {
t.Log("error ", err)
t.Fail()
}
Close()
}
for i := 1; i < 51; i++ {
// search a random something that will return a single result
uniqueSearches := []string{
fmt.Sprintf("from-%d@example.com", i),
fmt.Sprintf("from:from-%d@example.com", i),
fmt.Sprintf("to-%d@example.com", i),
fmt.Sprintf("to:to-%d@example.com", i),
fmt.Sprintf("to2-%d@example.com", i),
fmt.Sprintf("to:to2-%d@example.com", i),
fmt.Sprintf("cc-%d@example.com", i),
fmt.Sprintf("cc:cc-%d@example.com", i),
fmt.Sprintf("cc2-%d@example.com", i),
fmt.Sprintf("cc:cc2-%d@example.com", i),
fmt.Sprintf("reply-to-%d@example.com", i),
fmt.Sprintf("reply-to:\"reply-to-%d@example.com\"", i),
fmt.Sprintf("\"Subject line %d end\"", i),
fmt.Sprintf("subject:\"Subject line %d end\"", i),
fmt.Sprintf("\"the email body %d jdsauk dwqmdqw\"", i),
}
searchIdx := rand.Intn(len(uniqueSearches))
search := uniqueSearches[searchIdx]
summaries, _, err := Search(search, "", 0, 0, 100)
if err != nil {
t.Log("error ", err)
t.Fail()
}
assertEqual(t, len(summaries), 1, "search result expected")
assertEqual(t, summaries[0].From.Name, fmt.Sprintf("From %d", i), "\"From\" name does not match")
assertEqual(t, summaries[0].From.Address, fmt.Sprintf("from-%d@example.com", i), "\"From\" address does not match")
assertEqual(t, summaries[0].To[0].Name, fmt.Sprintf("To %d", i), "\"To\" name does not match")
assertEqual(t, summaries[0].To[0].Address, fmt.Sprintf("to-%d@example.com", i), "\"To\" address does not match")
assertEqual(t, summaries[0].Subject, fmt.Sprintf("Subject line %d end", i), "\"Subject\" does not match")
}
// search something that will return 200 results
summaries, _, err := Search("This is the email body", "", 0, 0, testRuns)
if err != nil {
t.Log("error ", err)
t.Fail()
}
assertEqual(t, len(summaries), testRuns, "search results expected")
}
func TestSearchDelete100(t *testing.T) {
setup()
defer Close()
for _, tenantID := range []string{"", "MyServer 3", "host.example.com"} {
tenantID = config.DBTenantID(tenantID)
t.Log("Testing search delete of 100 messages")
for i := 0; i < 100; i++ {
if _, err := Store(&testTextEmail); err != nil {
setup(tenantID)
if tenantID == "" {
t.Log("Testing search delete of 100 messages")
} else {
t.Logf("Testing search delete of 100 messages (tenant %s)", tenantID)
}
for i := 0; i < 100; i++ {
if _, err := Store(&testTextEmail); err != nil {
t.Log("error ", err)
t.Fail()
}
if _, err := Store(&testMimeEmail); err != nil {
t.Log("error ", err)
t.Fail()
}
}
_, total, err := Search("from:sender@example.com", "", 0, 0, 100)
if err != nil {
t.Log("error ", err)
t.Fail()
}
if _, err := Store(&testMimeEmail); err != nil {
assertEqual(t, total, 100, "100 search results expected")
if err := DeleteSearch("from:sender@example.com", ""); err != nil {
t.Log("error ", err)
t.Fail()
}
_, total, err = Search("from:sender@example.com", "", 0, 0, 100)
if err != nil {
t.Log("error ", err)
t.Fail()
}
assertEqual(t, total, 0, "0 search results expected")
Close()
}
_, total, err := Search("from:sender@example.com", "", 0, 0, 100)
if err != nil {
t.Log("error ", err)
t.Fail()
}
assertEqual(t, total, 100, "100 search results expected")
if err := DeleteSearch("from:sender@example.com", ""); err != nil {
t.Log("error ", err)
t.Fail()
}
_, total, err = Search("from:sender@example.com", "", 0, 0, 100)
if err != nil {
t.Log("error ", err)
t.Fail()
}
assertEqual(t, total, 0, "0 search results expected")
}
func TestSearchDelete1100(t *testing.T) {
setup()
setup("")
defer Close()
t.Log("Testing search delete of 1100 messages")

View File

@@ -3,8 +3,6 @@ package storage
import (
"net/mail"
"time"
"github.com/jhillyerd/enmime"
)
// Message data excluding physical attachments
@@ -114,21 +112,6 @@ type DBMailSummary struct {
ReplyTo []*mail.Address
}
// AttachmentSummary returns a summary of the attachment without any binary data
func AttachmentSummary(a *enmime.Part) Attachment {
o := Attachment{}
o.PartID = a.PartID
o.FileName = a.FileName
if o.FileName == "" {
o.FileName = a.ContentID
}
o.ContentType = a.ContentType
o.ContentID = a.ContentID
o.Size = float64(len(a.Content))
return o
}
// ListUnsubscribe contains a summary of List-Unsubscribe & List-Unsubscribe-Post headers
// including validation of the link structure
type ListUnsubscribe struct {

View File

@@ -13,9 +13,12 @@ import (
// TagFilter struct
type TagFilter struct {
// Match is the user-defined match
Match string
SQL *sqlf.Stmt
Tags []string
// SQL represents the SQL equivalent of Match
SQL *sqlf.Stmt
// Tags to add on match
Tags []string
}
var tagFilters = []TagFilter{}

View File

@@ -108,6 +108,7 @@ func addMessageTag(id, name string) (string, error) {
Set("ID", id).
Set("TagID", tagID).
ExecAndClose(context.TODO(), db)
return foundName.String, err
}
@@ -129,7 +130,7 @@ func addMessageTag(id, name string) (string, error) {
func deleteMessageTag(id, name string) error {
if _, err := sqlf.DeleteFrom(tenant("message_tags")).
Where(tenant("message_tags.ID")+" = ?", id).
Where(tenant("message_tags.Key")+` IN (SELECT Key FROM `+tenant("message_tags")+` LEFT JOIN tags ON `+tenant("TagID")+"="+tenant("tags.ID")+` WHERE Name = ?)`, name).
Where(tenant("message_tags.Key")+` IN (SELECT Key FROM `+tenant("message_tags")+` LEFT JOIN `+tenant("tags")+` ON TagID=`+tenant("tags.ID")+` WHERE Name = ?)`, name).
ExecAndClose(context.TODO(), db); err != nil {
return err
}

View File

@@ -4,127 +4,140 @@ import (
"fmt"
"strings"
"testing"
"github.com/axllent/mailpit/config"
)
func TestTags(t *testing.T) {
setup()
defer Close()
t.Log("Testing tags")
for _, tenantID := range []string{"", "MyServer 3", "host.example.com"} {
tenantID = config.DBTenantID(tenantID)
ids := []string{}
setup(tenantID)
for i := 0; i < 10; i++ {
if tenantID == "" {
t.Log("Testing tags")
} else {
t.Logf("Testing tags (tenant %s)", tenantID)
}
ids := []string{}
for i := 0; i < 10; i++ {
id, err := Store(&testMimeEmail)
if err != nil {
t.Log("error ", err)
t.Fail()
}
ids = append(ids, id)
}
for i := 0; i < 10; i++ {
if _, err := SetMessageTags(ids[i], []string{fmt.Sprintf("Tag-%d", i)}); err != nil {
t.Log("error ", err)
t.Fail()
}
}
for i := 0; i < 10; i++ {
message, err := GetMessage(ids[i])
if err != nil {
t.Log("error ", err)
t.Fail()
}
if len(message.Tags) != 1 || message.Tags[0] != fmt.Sprintf("Tag-%d", i) {
t.Fatal("Message tags do not match")
}
}
if err := DeleteAllMessages(); err != nil {
t.Log("error ", err)
t.Fail()
}
// test 20 tags
id, err := Store(&testMimeEmail)
if err != nil {
t.Log("error ", err)
t.Fail()
}
ids = append(ids, id)
}
for i := 0; i < 10; i++ {
if _, err := SetMessageTags(ids[i], []string{fmt.Sprintf("Tag-%d", i)}); err != nil {
newTags := []string{}
for i := 0; i < 20; i++ {
// pad number with 0 to ensure they are returned alphabetically
newTags = append(newTags, fmt.Sprintf("AnotherTag %02d", i))
}
if _, err := SetMessageTags(id, newTags); err != nil {
t.Log("error ", err)
t.Fail()
}
}
returnedTags := getMessageTags(id)
assertEqual(t, strings.Join(newTags, "|"), strings.Join(returnedTags, "|"), "Message tags do not match")
for i := 0; i < 10; i++ {
message, err := GetMessage(ids[i])
// remove first tag
if err := deleteMessageTag(id, newTags[0]); err != nil {
t.Log("error ", err)
t.Fail()
}
returnedTags = getMessageTags(id)
assertEqual(t, strings.Join(newTags[1:], "|"), strings.Join(returnedTags, "|"), "Message tags do not match after deleting 1")
// remove all tags
if err := DeleteAllMessageTags(id); err != nil {
t.Log("error ", err)
t.Fail()
}
returnedTags = getMessageTags(id)
assertEqual(t, "", strings.Join(returnedTags, "|"), "Message tags should be empty")
// apply the same tag twice
if _, err := SetMessageTags(id, []string{"Duplicate Tag", "Duplicate Tag"}); err != nil {
t.Log("error ", err)
t.Fail()
}
returnedTags = getMessageTags(id)
assertEqual(t, "Duplicate Tag", strings.Join(returnedTags, "|"), "Message tags should be duplicated")
if err := DeleteAllMessageTags(id); err != nil {
t.Log("error ", err)
t.Fail()
}
// apply tag with invalid characters
if _, err := SetMessageTags(id, []string{"Dirty! \"Tag\""}); err != nil {
t.Log("error ", err)
t.Fail()
}
returnedTags = getMessageTags(id)
assertEqual(t, "Dirty Tag", strings.Join(returnedTags, "|"), "Dirty message tag did not clean as expected")
if err := DeleteAllMessageTags(id); err != nil {
t.Log("error ", err)
t.Fail()
}
// Check deleted message tags also prune the tags database
allTags := GetAllTags()
assertEqual(t, "", strings.Join(allTags, "|"), "Tags did not delete as expected")
if err := DeleteAllMessages(); err != nil {
t.Log("error ", err)
t.Fail()
}
// test 20 tags
id, err = Store(&testTagEmail)
if err != nil {
t.Log("error ", err)
t.Fail()
}
if len(message.Tags) != 1 || message.Tags[0] != fmt.Sprintf("Tag-%d", i) {
t.Fatal("Message tags do not match")
returnedTags = getMessageTags(id)
assertEqual(t, "BccTag|CcTag|FromFag|ToTag|X-tag1|X-tag2", strings.Join(returnedTags, "|"), "Tags not detected correctly")
if err := DeleteAllMessageTags(id); err != nil {
t.Log("error ", err)
t.Fail()
}
Close()
}
if err := DeleteAllMessages(); err != nil {
t.Log("error ", err)
t.Fail()
}
// test 20 tags
id, err := Store(&testMimeEmail)
if err != nil {
t.Log("error ", err)
t.Fail()
}
newTags := []string{}
for i := 0; i < 20; i++ {
// pad number with 0 to ensure they are returned alphabetically
newTags = append(newTags, fmt.Sprintf("AnotherTag %02d", i))
}
if _, err := SetMessageTags(id, newTags); err != nil {
t.Log("error ", err)
t.Fail()
}
returnedTags := getMessageTags(id)
assertEqual(t, strings.Join(newTags, "|"), strings.Join(returnedTags, "|"), "Message tags do not match")
// remove first tag
if err := deleteMessageTag(id, newTags[0]); err != nil {
t.Log("error ", err)
t.Fail()
}
returnedTags = getMessageTags(id)
assertEqual(t, strings.Join(newTags[1:], "|"), strings.Join(returnedTags, "|"), "Message tags do not match after deleting 1")
// remove all tags
if err := DeleteAllMessageTags(id); err != nil {
t.Log("error ", err)
t.Fail()
}
returnedTags = getMessageTags(id)
assertEqual(t, "", strings.Join(returnedTags, "|"), "Message tags should be empty")
// apply the same tag twice
if _, err := SetMessageTags(id, []string{"Duplicate Tag", "Duplicate Tag"}); err != nil {
t.Log("error ", err)
t.Fail()
}
returnedTags = getMessageTags(id)
assertEqual(t, "Duplicate Tag", strings.Join(returnedTags, "|"), "Message tags should be duplicated")
if err := DeleteAllMessageTags(id); err != nil {
t.Log("error ", err)
t.Fail()
}
// apply tag with invalid characters
if _, err := SetMessageTags(id, []string{"Dirty! \"Tag\""}); err != nil {
t.Log("error ", err)
t.Fail()
}
returnedTags = getMessageTags(id)
assertEqual(t, "Dirty Tag", strings.Join(returnedTags, "|"), "Dirty message tag did not clean as expected")
if err := DeleteAllMessageTags(id); err != nil {
t.Log("error ", err)
t.Fail()
}
// Check deleted message tags also prune the tags database
allTags := GetAllTags()
assertEqual(t, "", strings.Join(allTags, "|"), "Tags did not delete as expected")
if err := DeleteAllMessages(); err != nil {
t.Log("error ", err)
t.Fail()
}
// test 20 tags
id, err = Store(&testTagEmail)
if err != nil {
t.Log("error ", err)
t.Fail()
}
returnedTags = getMessageTags(id)
assertEqual(t, "BccTag|CcTag|FromFag|ToTag|X-tag1|X-tag2", strings.Join(returnedTags, "|"), "Tags not detected correctly")
if err := DeleteAllMessageTags(id); err != nil {
t.Log("error ", err)
t.Fail()
}
}

View File

@@ -16,10 +16,11 @@ var (
testRuns = 100
)
func setup() {
func setup(tenantID string) {
logger.NoLogging = true
config.MaxMessages = 0
config.Database = os.Getenv("MP_DATABASE")
config.TenantID = config.DBTenantID(tenantID)
if err := InitDB(); err != nil {
panic(err)

View File

@@ -11,14 +11,14 @@ func Plural(total int, singular, plural string) string {
if total == 1 {
return fmt.Sprintf("%d %s", total, singular)
}
return fmt.Sprintf("%d %s", total, plural)
}
// InArray tests if a string is within an array. It is not case sensitive.
func InArray(k string, arr []string) bool {
k = strings.ToLower(k)
for _, v := range arr {
if strings.ToLower(v) == k {
if strings.EqualFold(v, k) {
return true
}
}

View File

@@ -71,5 +71,6 @@ func Unzip(src string, dest string) ([]string, error) {
return filenames, err
}
}
return filenames, nil
}

View File

@@ -23,6 +23,7 @@ var (
// AllowPrereleases defines whether pre-releases may be included
AllowPrereleases = false
// temporary directory
tempDir string
)

1103
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -31,7 +31,7 @@
"@types/bootstrap": "^5.2.7",
"@types/tinycon": "^0.6.3",
"@vue/compiler-sfc": "^3.2.37",
"esbuild": "^0.23.0",
"esbuild": "^0.24.0",
"esbuild-plugin-vue-next": "^0.1.4",
"esbuild-sass-plugin": "^3.0.0"
}

View File

@@ -35,7 +35,6 @@ var (
SMTPAddr = "localhost:1025"
// FromAddr email address
FromAddr string
// UseB - used to set from `-bs`
UseB bool
// UseS - used to set from `-bs`

View File

@@ -601,6 +601,11 @@ func LinkCheck(w http.ResponseWriter, r *http.Request) {
// 200: LinkCheckResponse
// default: ErrorResponse
if config.DemoMode {
httpError(w, "this functionality has been disabled for demonstration purposes")
return
}
vars := mux.Vars(r)
id := vars["id"]

View File

@@ -37,6 +37,11 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
// 200: OKResponse
// default: ErrorResponse
if config.DemoMode {
httpError(w, "this functionality has been disabled for demonstration purposes")
return
}
vars := mux.Vars(r)
id := vars["id"]

View File

@@ -11,6 +11,7 @@ import (
"net/mail"
"strings"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/tools"
"github.com/axllent/mailpit/server/smtpd"
"github.com/jhillyerd/enmime"
@@ -141,6 +142,11 @@ func SendMessageHandler(w http.ResponseWriter, r *http.Request) {
// 200: sendMessageResponse
// default: jsonErrorResponse
if config.DemoMode {
httpJSONError(w, "this functionality has been disabled for demonstration purposes")
return
}
decoder := json.NewDecoder(r.Body)
data := SendRequest{}

View File

@@ -79,5 +79,6 @@ func getSafeArg(args []string, nr int) (string, error) {
if nr < len(args) {
return args[nr], nil
}
return "", errors.New("-ERR out of range")
}

View File

@@ -220,6 +220,7 @@ func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc {
fn(w, r)
return
}
w.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(w)
defer gz.Close()

View File

@@ -6,6 +6,7 @@ import mitt from 'mitt';
import './assets/styles.scss'
import 'bootstrap-icons/font/bootstrap-icons.scss'
import 'bootstrap'
import 'vue-css-donut-chart/src/styles/main.css'
const app = createApp(App)

View File

@@ -65,9 +65,10 @@ export default {
<template>
<template v-if="!modals">
<div class="text-center badge text-bg-primary py-2 mt-2 w-100 text-truncate fw-normal"
v-if="mailbox.uiConfig.Label">
{{ mailbox.uiConfig.Label }}
<div class="text-center badge text-bg-primary py-2 my-2 w-100" v-if="mailbox.uiConfig.Label">
<div class="text-truncate fw-normal">
{{ mailbox.uiConfig.Label }}
</div>
</div>
<div class="list-group my-2" :class="mailbox.uiConfig.Label ? 'mt-0' : ''">

View File

@@ -52,9 +52,10 @@ export default {
<template>
<template v-if="!modals">
<div class="text-center badge text-bg-primary py-2 mt-2 w-100 text-truncate fw-normal"
v-if="mailbox.uiConfig.Label">
{{ mailbox.uiConfig.Label }}
<div class="text-center badge text-bg-primary py-2 my-2 w-100" v-if="mailbox.uiConfig.Label">
<div class="text-truncate fw-normal">
{{ mailbox.uiConfig.Label }}
</div>
</div>
<div class="list-group my-2" :class="mailbox.uiConfig.Label ? 'mt-0' : ''">

View File

@@ -22,7 +22,7 @@ export default {
return false
}
let re = new RegExp(`\\btag:("${tag}"|${tag}\\b)`, 'i')
let re = new RegExp(`(^|\\s)tag:("${tag}"|${tag}\\b)`, 'i')
return query.match(re)
},

View File

@@ -1,5 +1,5 @@
<script>
import Donut from 'vue-css-donut-chart/src/components/Donut.vue'
import { VcDonut } from 'vue-css-donut-chart'
import axios from 'axios'
import commonMixins from '../../mixins/CommonMixins'
import { Tooltip } from 'bootstrap'
@@ -10,7 +10,7 @@ export default {
},
components: {
Donut,
VcDonut,
},
emits: ["setHtmlScore", "setBadgeStyle"],
@@ -299,7 +299,7 @@ export default {
<div class="mt-5 mb-3">
<div class="row w-100">
<div class="col-md-8">
<Donut :sections="graphSections" background="var(--bs-body-bg)" :size="180" unit="px"
<vc-donut :sections="graphSections" background="var(--bs-body-bg)" :size="180" unit="px"
:thickness="20" has-legend legend-placement="bottom" :total="100" :start-angle="0"
:auto-adjust-text-size="true" @section-click="scrollToWarnings">
<h2 class="m-0" :class="scoreColor" @click="scrollToWarnings">
@@ -327,7 +327,7 @@ export default {
calculated from {{ formatNumber(check.Total.Tests) }} tests
</p>
</template>
</Donut>
</vc-donut>
<div class="input-group justify-content-center mb-3">
<button class="btn btn-outline-secondary" data-bs-toggle="modal"

View File

@@ -182,7 +182,7 @@ export default {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
[...tooltipTriggerList].map(tooltipTriggerEl => new Tooltip(tooltipTriggerEl))
// delay 0.2s until vue has rendered the iframe content
// delay 0.5s until vue has rendered the iframe content
window.setTimeout(() => {
let p = document.getElementById('preview-html')
if (p && typeof p.contentWindow.document.body == 'object') {
@@ -193,14 +193,14 @@ export default {
let anchorEl = anchorEls[i]
let href = anchorEl.getAttribute('href')
if (href && href.match(/^http/)) {
if (href && href.match(/^https?:\/\//i)) {
anchorEl.setAttribute('target', '_blank')
}
}
} catch (error) { }
this.resizeIFrames()
}
}, 200)
}, 500)
// html highlighting
window.Prism = window.Prism || {}

View File

@@ -1,5 +1,5 @@
<script>
import Donut from 'vue-css-donut-chart/src/components/Donut.vue'
import { VcDonut } from 'vue-css-donut-chart'
import axios from 'axios'
import commonMixins from '../../mixins/CommonMixins'
@@ -9,7 +9,7 @@ export default {
},
components: {
Donut,
VcDonut,
},
emits: ["setSpamScore", "setBadgeStyle"],
@@ -156,7 +156,7 @@ export default {
<template v-else-if="check">
<div class="row w-100 mt-5">
<div class="col-xl-5 mb-2">
<Donut :sections="graphSections" background="var(--bs-body-bg)" :size="230" unit="px" :thickness="20"
<vc-donut :sections="graphSections" background="var(--bs-body-bg)" :size="230" unit="px" :thickness="20"
:total="100" :start-angle="270" :auto-adjust-text-size="true" foreground="#198754">
<h2 class="m-0" :class="scoreColor" @click="scrollToWarnings">
{{ check.Score }} / 5
@@ -165,7 +165,7 @@ export default {
<span v-if="check.IsSpam" class="text-white badge rounded-pill bg-danger p-2">Spam</span>
<span v-else class="badge rounded-pill p-2" :class="badgeStyle()">Not spam</span>
</div>
</Donut>
</vc-donut>
</div>
<div class="col-xl-7">
<div class="row w-100 py-2 border-bottom">

View File

@@ -544,9 +544,10 @@ export default {
<div class="row flex-fill" style="min-height:0">
<div class="d-none d-xl-flex col-xl-3 h-100 flex-column">
<div class="text-center badge text-bg-primary py-2 mt-2 w-100 text-truncate fw-normal"
v-if="mailbox.uiConfig.Label">
{{ mailbox.uiConfig.Label }}
<div class="text-center badge text-bg-primary py-2 my-2 w-100" v-if="mailbox.uiConfig.Label">
<div class="text-truncate fw-normal">
{{ mailbox.uiConfig.Label }}
</div>
</div>
<div class="list-group my-2" :class="mailbox.uiConfig.Label ? 'mt-0' : ''">

View File

@@ -13,6 +13,7 @@
<body>
<rapi-doc id="thedoc" spec-url="swagger.json" theme="light" layout="column" render-style="read" load-fonts="false"
allow-authentication="false"
regular-font="system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'"
mono-font="Courier New, Courier, System, fixed-width" font-size="large" allow-spec-url-load="false"
allow-spec-file-load="false" allow-server-selection="false" allow-search="false" allow-advanced-search="false"