mirror of
https://github.com/axllent/mailpit.git
synced 2025-03-19 21:28:07 +02:00
Merge branch 'release/v1.16.0'
This commit is contained in:
commit
affe19beb5
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@ -26,8 +26,8 @@ jobs:
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- run: go test ./internal/storage ./server ./internal/tools ./internal/html2text -v
|
||||
- run: go test ./internal/storage ./internal/html2text -bench=.
|
||||
- run: go test -p 1 ./internal/storage ./server ./internal/tools ./internal/html2text -v
|
||||
- run: go test -p 1 ./internal/storage ./internal/html2text -bench=.
|
||||
|
||||
# build the assets
|
||||
- name: Build web UI
|
||||
|
19
CHANGELOG.md
19
CHANGELOG.md
@ -2,6 +2,25 @@
|
||||
|
||||
Notable changes to Mailpit will be documented in this file.
|
||||
|
||||
## [v1.16.0]
|
||||
|
||||
### Chore
|
||||
- Update caniemail test database
|
||||
- Update node dependencies
|
||||
- Update Go dependencies
|
||||
- Switch database flag/env to `--database` / `MP_DATABASE`
|
||||
|
||||
### Feature
|
||||
- Search support for before: and after: dates ([#252](https://github.com/axllent/mailpit/issues/252))
|
||||
- Add optional tenant ID to isolate data in shared databases ([#254](https://github.com/axllent/mailpit/issues/254))
|
||||
- Option to use rqlite database storage ([#254](https://github.com/axllent/mailpit/issues/254))
|
||||
|
||||
### Fix
|
||||
- Remove duplicated authentication check ([#276](https://github.com/axllent/mailpit/issues/276))
|
||||
- Prevent conditional JS error when global mailbox tag list is modified via auto/plus-address tagging while viewing a message
|
||||
- Extract plus addresses from email addresses only, not names
|
||||
|
||||
|
||||
## [v1.15.1]
|
||||
|
||||
### Chore
|
||||
|
@ -41,7 +41,8 @@ settings to determine the HTTP bind interface & port.
|
||||
IdleConnTimeout: time.Second * 5,
|
||||
ExpectContinueTimeout: time.Second * 5,
|
||||
TLSHandshakeTimeout: time.Second * 5,
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
// do not verify TLS in case this instance is using HTTPS
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // #nosec
|
||||
}
|
||||
client := &http.Client{Transport: conf}
|
||||
|
||||
|
@ -19,7 +19,7 @@ If you have several thousand messages in your mailbox, then it is advised to shu
|
||||
Mailpit while you reindex as this process will likely result in database locking issues.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
config.DataFile = args[0]
|
||||
config.Database = args[0]
|
||||
config.MaxMessages = 0
|
||||
|
||||
if err := storage.InitDB(); err != nil {
|
||||
|
20
cmd/root.go
20
cmd/root.go
@ -80,7 +80,8 @@ func init() {
|
||||
// load environment variables
|
||||
initConfigFromEnv()
|
||||
|
||||
rootCmd.Flags().StringVarP(&config.DataFile, "db-file", "d", config.DataFile, "Database file to store persistent data")
|
||||
rootCmd.Flags().StringVarP(&config.Database, "database", "d", config.Database, "Database to store persistent data")
|
||||
rootCmd.Flags().StringVar(&config.TenantID, "tenant-id", config.TenantID, "Database tenant ID to isolate data")
|
||||
rootCmd.Flags().IntVarP(&config.MaxMessages, "max", "m", config.MaxMessages, "Max number of messages to store")
|
||||
rootCmd.Flags().BoolVar(&config.UseMessageDates, "use-message-dates", config.UseMessageDates, "Use message dates as the received dates")
|
||||
rootCmd.Flags().BoolVar(&config.IgnoreDuplicateIDs, "ignore-duplicate-ids", config.IgnoreDuplicateIDs, "Ignore duplicate messages (by Message-Id)")
|
||||
@ -132,6 +133,10 @@ func init() {
|
||||
rootCmd.Flags().StringVar(&config.WebhookURL, "webhook-url", config.WebhookURL, "Send a webhook request for new messages")
|
||||
rootCmd.Flags().IntVar(&webhook.RateLimit, "webhook-limit", webhook.RateLimit, "Limit webhook requests per second")
|
||||
|
||||
// DEPRECATED FLAG 2024/04/12 - but will not be removed to maintain backwards compatibility
|
||||
rootCmd.Flags().StringVar(&config.Database, "db-file", config.Database, "Database file to store persistent data")
|
||||
rootCmd.Flags().Lookup("db-file").Hidden = true
|
||||
|
||||
// DEPRECATED FLAGS 2023/03/12
|
||||
rootCmd.Flags().StringVar(&config.UITLSCert, "ui-ssl-cert", config.UITLSCert, "SSL certificate for web UI - requires ui-ssl-key")
|
||||
rootCmd.Flags().StringVar(&config.UITLSKey, "ui-ssl-key", config.UITLSKey, "SSL key for web UI - requires ui-ssl-cert")
|
||||
@ -155,7 +160,12 @@ func init() {
|
||||
// Load settings from environment
|
||||
func initConfigFromEnv() {
|
||||
// General
|
||||
config.DataFile = os.Getenv("MP_DATA_FILE")
|
||||
if len(os.Getenv("MP_DATABASE")) > 0 {
|
||||
config.Database = os.Getenv("MP_DATABASE")
|
||||
}
|
||||
|
||||
config.TenantID = os.Getenv("MP_TENANT_ID")
|
||||
|
||||
if len(os.Getenv("MP_MAX_MESSAGES")) > 0 {
|
||||
config.MaxMessages, _ = strconv.Atoi(os.Getenv("MP_MAX_MESSAGES"))
|
||||
}
|
||||
@ -223,7 +233,6 @@ func initConfigFromEnv() {
|
||||
if getEnabledFromEnv("MP_SMTP_REQUIRE_TLS") {
|
||||
config.SMTPRequireTLS = true
|
||||
}
|
||||
|
||||
if getEnabledFromEnv("MP_SMTP_AUTH_ALLOW_INSECURE") {
|
||||
config.SMTPAuthAllowInsecure = true
|
||||
}
|
||||
@ -289,6 +298,11 @@ func initConfigFromEnv() {
|
||||
|
||||
// load deprecated settings from environment and warn
|
||||
func initDeprecatedConfigFromEnv() {
|
||||
// deprecated 2024/04/12 - but will not be removed to maintain backwards compatibility
|
||||
if len(os.Getenv("MP_DATA_FILE")) > 0 {
|
||||
config.Database = os.Getenv("MP_DATA_FILE")
|
||||
}
|
||||
|
||||
// deprecated 2023/03/12
|
||||
if len(os.Getenv("MP_UI_SSL_CERT")) > 0 {
|
||||
logger.Log().Warn("ENV MP_UI_SSL_CERT has been deprecated, use MP_UI_TLS_CERT")
|
||||
|
@ -26,8 +26,12 @@ var (
|
||||
// HTTPListen to listen on <interface>:<port>
|
||||
HTTPListen = "[::]:8025"
|
||||
|
||||
// DataFile for mail (optional)
|
||||
DataFile string
|
||||
// Database for mail (optional)
|
||||
Database string
|
||||
|
||||
// TenantID is an optional prefix to be applied to all database tables,
|
||||
// allowing multiple isolated instances of Mailpit to share a database.
|
||||
TenantID = ""
|
||||
|
||||
// MaxMessages is the maximum number of messages a mailbox can have (auto-pruned every minute)
|
||||
MaxMessages = 500
|
||||
@ -185,8 +189,18 @@ func VerifyConfig() error {
|
||||
cssFontRestriction, cssFontRestriction,
|
||||
)
|
||||
|
||||
if DataFile != "" && isDir(DataFile) {
|
||||
DataFile = filepath.Join(DataFile, "mailpit.db")
|
||||
if Database != "" && isDir(Database) {
|
||||
Database = filepath.Join(Database, "mailpit.db")
|
||||
}
|
||||
|
||||
TenantID = strings.TrimSpace(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+$`)
|
||||
|
21
go.mod
21
go.mod
@ -3,37 +3,36 @@ module github.com/axllent/mailpit
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/GuiaBolso/darwin v0.0.0-20191218124601-fd6d2aa3d244
|
||||
github.com/PuerkitoBio/goquery v1.9.1
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
||||
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/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/jhillyerd/enmime v1.2.0
|
||||
github.com/klauspost/compress v1.17.7
|
||||
github.com/klauspost/compress v1.17.8
|
||||
github.com/leporo/sqlf v1.4.0
|
||||
github.com/lithammer/shortuuid/v4 v4.0.0
|
||||
github.com/mhale/smtpd v0.8.2
|
||||
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e
|
||||
github.com/rqlite/gorqlite v0.0.0-20240227123050-397b03f02418
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/tg123/go-htpasswd v1.2.2
|
||||
github.com/vanng822/go-premailer v1.20.2
|
||||
golang.org/x/net v0.22.0
|
||||
golang.org/x/net v0.24.0
|
||||
golang.org/x/text v0.14.0
|
||||
golang.org/x/time v0.5.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.29.5
|
||||
modernc.org/sqlite v1.29.6
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 // indirect
|
||||
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
|
||||
github.com/cznic/ql v1.2.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
@ -53,14 +52,14 @@ 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.21.0 // indirect
|
||||
golang.org/x/crypto v0.22.0 // indirect
|
||||
golang.org/x/image v0.15.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b // indirect
|
||||
modernc.org/libc v1.45.0 // indirect
|
||||
modernc.org/libc v1.49.3 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
)
|
||||
|
70
go.sum
70
go.sum
@ -1,41 +1,19 @@
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
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/GuiaBolso/darwin v0.0.0-20191218124601-fd6d2aa3d244 h1:dqzm54OhCqY8RinR/cx+Ppb0y56Ds5I3wwWhx4XybDg=
|
||||
github.com/GuiaBolso/darwin v0.0.0-20191218124601-fd6d2aa3d244/go.mod h1:3sqgkckuISJ5rs1EpOp6vCvwOUKe/z9vPmyuIlq8Q/A=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI=
|
||||
github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
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=
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
|
||||
github.com/axllent/semver v0.0.1 h1:QqF+KSGxgj8QZzSXAvKFqjGWE5792ksOnQhludToK8E=
|
||||
github.com/axllent/semver v0.0.1/go.mod h1:2xSPzvG8n9mRfdtxSvWvfTfQGWfHsMsHO1iZnKATMSc=
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07 h1:UHFGPvSxX4C4YBApSPvmUfL8tTvWLj2ryqvT9K4Jcuk=
|
||||
github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
|
||||
github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f h1:7uSNgsgcarNk4oiN/nNkO0J7KAjlsF5Yv5Gf/tFdHas=
|
||||
github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg=
|
||||
github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4 h1:CVAqftqbj+exlab+8KJQrE+kNIVlQfJt58j4GxCMF1s=
|
||||
github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc=
|
||||
github.com/cznic/internal v0.0.0-20180608152220-f44710a21d00 h1:FHpbUtp2K8X53/b4aFNj4my5n+i3x+CQCZWNuHWH/+E=
|
||||
github.com/cznic/internal v0.0.0-20180608152220-f44710a21d00/go.mod h1:olo7eAdKwJdXxb55TKGLiJ6xt1H0/tiiRCWKVLmtjY4=
|
||||
github.com/cznic/lldb v1.1.0 h1:AIA+ham6TSJ+XkMe8imQ/g8KPzMUVWAwqUQQdtuMsHs=
|
||||
github.com/cznic/lldb v1.1.0/go.mod h1:FIZVUmYUVhPwRiPzL8nD/mpFcJ/G7SSXjjXYG4uRI3A=
|
||||
github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 h1:XNT/Zf5l++1Pyg08/HV04ppB0gKxAqtZQBRYiYrUuYk=
|
||||
github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
|
||||
github.com/cznic/ql v1.2.0 h1:lcKp95ZtdF0XkWhGnVIXGF8dVD2X+ClS08tglKtf+ak=
|
||||
github.com/cznic/ql v1.2.0/go.mod h1:FbpzhyZrqr0PVlK6ury+PoW3T0ODUV22OeWIxcaOrSE=
|
||||
github.com/cznic/sortutil v0.0.0-20150617083342-4c7342852e65 h1:hxuZop6tSoOi0sxFzoGGYdRqNrPubyaIf9KoBG9tPiE=
|
||||
github.com/cznic/sortutil v0.0.0-20150617083342-4c7342852e65/go.mod h1:q2w6Bg5jeox1B+QkJ6Wp/+Vn0G/bo3f1uY7Fn3vivIQ=
|
||||
github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186 h1:0rkFMAbn5KBKNpJyHQ6Prb95vIKanmAe62KxsrN+sqA=
|
||||
github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
|
||||
github.com/cznic/zappy v0.0.0-20160723133515-2533cb5b45cc h1:YKKpTb2BrXN2GYyGaygIdis1vXbE7SSAG9axGWIMClg=
|
||||
github.com/cznic/zappy v0.0.0-20160723133515-2533cb5b45cc/go.mod h1:Y1SNZ4dRUOKXshKUbwUapqNncRrho4mkjQebgEHZLj8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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=
|
||||
@ -43,15 +21,12 @@ github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
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/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 h1:aaQcKT9WumO6JEJcRyTqFVq4XUZiUcKR2/GI31TOcz8=
|
||||
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
||||
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
|
||||
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/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 h1:k4Tw0nt6lwro3Uin8eqoET7MDA4JnT8YgbCjc/g5E3k=
|
||||
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 h1:4gjrh/PN2MuWCCElk8/I4OCKRKWCCo2zEct3VKCbibU=
|
||||
github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@ -71,8 +46,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.2.0 h1:dIu1IPEymQgoT2dzuB//ttA/xcV40NMPpQtmd4wslHk=
|
||||
github.com/jhillyerd/enmime v1.2.0/go.mod h1:FRFuUPCLh8PByQv+8xRcLO9QHqaqTqreYhopv5eyk4I=
|
||||
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
|
||||
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
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=
|
||||
@ -87,6 +62,7 @@ github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQ
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
@ -107,12 +83,16 @@ github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e h1:quuzZLi72kkJjl
|
||||
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e/go.mod h1:+5vNVvEWwEIx86DB9Ke/+a5wBI464eDRo3eF0LcfpWg=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
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-20240227123050-397b03f02418 h1:gYUQqzapdN4PQF5j0zDFI9ANQVAVFoJivNp5bTZEZMo=
|
||||
github.com/rqlite/gorqlite v0.0.0-20240227123050-397b03f02418/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=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
@ -144,8 +124,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
@ -161,8 +141,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
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.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
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=
|
||||
@ -177,8 +157,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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=
|
||||
@ -206,22 +186,22 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.19.3 h1:vE9kmJqUcyvNOf8F2Hn8od14SOMq34BiqcZ2tMzLk5c=
|
||||
modernc.org/ccgo/v4 v4.11.0 h1:2uc2kRvZLC/oHylsrirRW6f1I4wljQST2BBbm+aKiXM=
|
||||
modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk=
|
||||
modernc.org/ccgo/v4 v4.16.0 h1:ofwORa6vx2FMm0916/CkZjpFPSR70VwTjUCe2Eg5BnA=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
||||
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b h1:BnN1t+pb1cy61zbvSUV7SeI0PwosMhlAEi/vBY4qxp8=
|
||||
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||
modernc.org/libc v1.45.0 h1:qmAJZf9tYFqK/SFSFqpBc9uHWGsvoYWtRcMQdG+JEfM=
|
||||
modernc.org/libc v1.45.0/go.mod h1:YkRHLoN4L70OdO1cVmM83KZhRbRvsc3XogfVzbTXBwE=
|
||||
modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg=
|
||||
modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo=
|
||||
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=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||
modernc.org/sqlite v1.29.5 h1:8l/SQKAjDtZFo9lkJLdk8g9JEOeYRG4/ghStDCCTiTE=
|
||||
modernc.org/sqlite v1.29.5/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U=
|
||||
modernc.org/sqlite v1.29.6 h1:0lOXGrycJPptfHDuohfYgNqoe4hu+gYuN/pKgY5XjS4=
|
||||
modernc.org/sqlite v1.29.6/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U=
|
||||
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=
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"api_version":"1.0.4",
|
||||
"last_update_date":"2024-03-09 07:21:32 +0000",
|
||||
"last_update_date":"2024-04-03 12:33:14 +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":[
|
||||
{
|
||||
@ -110,7 +110,7 @@
|
||||
"last_test_date":"2023-12-19",
|
||||
"test_url":"https://www.caniemail.com/tests/css-font-face.html",
|
||||
"test_results_url":"https://app.emailonacid.com/app/acidtest/veY9MhuhgFeF1ly5crrhTXawfLJSwxgpYi27OElI7iSoc/list",
|
||||
"stats":{"apple-mail":{"macos":{"12.2":"y"},"ios":{"10.3":"y","12.3.1":"y"}},"gmail":{"desktop-webmail":{"2019-07":"n #6"},"ios":{"2019-07":"n"},"android":{"2019-07":"n"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-05":"a #2","2021-03":"n #7"},"ios":{"2019-07":"y"},"android":{"2019-07":"a #1"}},"outlook":{"windows":{"2003":"a #3","2007":"a #4 #5","2010":"a #4 #5","2013":"a #4 #5","2016":"a #4 #5","2019":"a #4"},"windows-mail":{"2020-01":"n"},"macos":{"2011":"y","2016":"y","16.80":"n"},"outlook-com":{"2019-07":"n","2023-12":"n"},"ios":{"2.51.1":"y","3.29.0":"n"},"android":{"2019-07":"n"}},"samsung-email":{"android":{"6.0":"y #8","2021-11":"y #8"}},"sfr":{"desktop-webmail":{"2019-07":"a #2"},"ios":{"2019-07":"n"},"android":{"2019-07":"n"}},"thunderbird":{"macos":{"60.7":"y","78.5":"y"}},"aol":{"desktop-webmail":{"2020-01":"n"},"ios":{"2020-01":"n"},"android":{"2020-01":"n"}},"yahoo":{"desktop-webmail":{"2019-07":"n"},"ios":{"2019-07":"n"},"android":{"2019-07":"n"}},"protonmail":{"desktop-webmail":{"2020-03":"n"},"ios":{"2020-03":"n"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"n"}},"fastmail":{"desktop-webmail":{"2021-07":"n"}},"laposte":{"desktop-webmail":{"2021-08":"a #2"}},"gmx":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"n"}},"web-de":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"n"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"n"}}},
|
||||
"stats":{"apple-mail":{"macos":{"12.2":"y"},"ios":{"10.3":"y","12.3.1":"y"}},"gmail":{"desktop-webmail":{"2019-07":"n #6"},"ios":{"2019-07":"n"},"android":{"2019-07":"n"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-05":"a #2","2021-03":"n #7"},"ios":{"2019-07":"y","2024-03":"n"},"android":{"2019-07":"a #1"}},"outlook":{"windows":{"2003":"a #3","2007":"a #4 #5","2010":"a #4 #5","2013":"a #4 #5","2016":"a #4 #5","2019":"a #4"},"windows-mail":{"2020-01":"n"},"macos":{"2011":"y","2016":"y","16.80":"n"},"outlook-com":{"2019-07":"n","2023-12":"n"},"ios":{"2.51.1":"y","3.29.0":"n"},"android":{"2019-07":"n"}},"samsung-email":{"android":{"6.0":"y #8","2021-11":"y #8"}},"sfr":{"desktop-webmail":{"2019-07":"a #2"},"ios":{"2019-07":"n"},"android":{"2019-07":"n"}},"thunderbird":{"macos":{"60.7":"y","78.5":"y"}},"aol":{"desktop-webmail":{"2020-01":"n"},"ios":{"2020-01":"n"},"android":{"2020-01":"n"}},"yahoo":{"desktop-webmail":{"2019-07":"n"},"ios":{"2019-07":"n"},"android":{"2019-07":"n"}},"protonmail":{"desktop-webmail":{"2020-03":"n"},"ios":{"2020-03":"n"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"n"}},"fastmail":{"desktop-webmail":{"2021-07":"n"}},"laposte":{"desktop-webmail":{"2021-08":"a #2"}},"gmx":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"n"}},"web-de":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"n"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"n"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":{"1":"Partial. Only supported through a `<link>` tag.","2":"Partial. Only supported directly through a `<style>` tag.","3":"Buggy. Support depends on the version of IE installed.","4":"Partial. The declaration is supported but distant fonts are ignored.","5":"Buggy. Elements using a font declared with `@font-face` ignore the font stack and fall back to Times New Roman. Use `mso-generic-font-family` and `mso-font-alt` to control the fallback.","6":"Not supported. Roboto and Google Sans can be used, but only because they're embedded with the webmail's own styles.","7":"Not supported. The `@font-face` declaration is kept but the `src` property is removed.","8":"Not supported when using a Microsoft email address; outlook, live, hotmail, etc.."}
|
||||
},
|
||||
@ -174,7 +174,7 @@
|
||||
"last_test_date":"2022-08-29",
|
||||
"test_url":"https://www.caniemail.com/tests/css-media-hover.html",
|
||||
"test_results_url":"https://testi.at/proj/onECpNVH8Dhv7BSLPXUbQ4s0O",
|
||||
"stats":{"apple-mail":{"macos":{"2022-08":"y"},"ios":{"2022-08":"y"}},"gmail":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"},"mobile-webmail":{"2022-08":"n"}},"orange":{"desktop-webmail":{"2022-08":"u"},"ios":{"2022-08":"u"},"android":{"2022-08":"u"}},"outlook":{"windows":{"2022-08":"n"},"windows-mail":{"2022-08":"n"},"macos":{"2011":"y","2016":"y","16.80":"y"},"outlook-com":{"2022-08":"y"},"ios":{"2022-08":"y"},"android":{"2022-08":"y"}},"samsung-email":{"android":{"10":"y","11":"y"}},"sfr":{"desktop-webmail":{"2022-08":"u"},"ios":{"2022-08":"u"},"android":{"2022-08":"u"}},"thunderbird":{"macos":{"2022-08":"u"}},"aol":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"}},"yahoo":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"}},"protonmail":{"desktop-webmail":{"2022-08":"u"},"ios":{"2022-08":"u"},"android":{"2022-08":"u"}},"hey":{"desktop-webmail":{"2022-08":"u"}},"mail-ru":{"desktop-webmail":{"2022-08":"n"}},"fastmail":{"desktop-webmail":{"2022-08":"u"}},"laposte":{"desktop-webmail":{"2022-08":"u"}}},
|
||||
"stats":{"apple-mail":{"macos":{"2022-08":"y"},"ios":{"2022-08":"y"}},"gmail":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"},"mobile-webmail":{"2022-08":"n"}},"orange":{"desktop-webmail":{"2022-08":"u"},"ios":{"2022-08":"u"},"android":{"2022-08":"u"}},"outlook":{"windows":{"2022-08":"n"},"windows-mail":{"2022-08":"n"},"macos":{"2011":"y","2016":"y","16.80":"y"},"outlook-com":{"2022-08":"y"},"ios":{"2022-08":"y"},"android":{"2022-08":"y"}},"samsung-email":{"android":{"10":"y","11":"y"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"n"},"android":{"2024-03":"n"}},"thunderbird":{"macos":{"2022-08":"u"}},"aol":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"}},"yahoo":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"}},"protonmail":{"desktop-webmail":{"2022-08":"u"},"ios":{"2022-08":"u"},"android":{"2022-08":"u"}},"hey":{"desktop-webmail":{"2022-08":"u"}},"mail-ru":{"desktop-webmail":{"2022-08":"n"}},"fastmail":{"desktop-webmail":{"2022-08":"u"}},"laposte":{"desktop-webmail":{"2022-08":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":null
|
||||
},
|
||||
@ -259,6 +259,22 @@
|
||||
"notes_by_num":{"1":"Properties not supported by the client are replaced by `_filtered_a` inside the parenthesis.","2":"Not supported. `@supports` is transformed into `@media not all`."}
|
||||
},
|
||||
|
||||
{
|
||||
"slug":"css-backdrop-filter",
|
||||
"title":"backdrop-filter",
|
||||
"description":"Lets you apply graphical effects such as blurring or color shifting to the area behind an element.",
|
||||
"url":"https://www.caniemail.com/features/css-backdrop-filter/",
|
||||
"category":"css",
|
||||
"tags":[],
|
||||
"keywords":"filter",
|
||||
"last_test_date":"2024-01-17",
|
||||
"test_url":"https://www.caniemail.com/tests/css-backdrop-filter.html",
|
||||
"test_results_url":"https://testi.at/proj/p4r7t9n30o7nh7vvfpn",
|
||||
"stats":{"apple-mail":{"macos":{"10":"y #1","11":"u","12":"u","13":"y #1"},"ios":{"11":"n","12":"n","13":"y #1","14":"y #1","15":"y #1"}},"gmail":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"n"},"android":{"2024-01":"n"},"mobile-webmail":{"2024-01":"n"}},"orange":{"desktop-webmail":{"2024-01":"u"},"ios":{"2024-01":"n"},"android":{"2024-01":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2024-01":"n"},"macos":{"2024-01":"y"},"outlook-com":{"2024-01":"n"},"ios":{"2024-01":"n"},"android":{"2024-01":"n"}},"yahoo":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"n"},"android":{"2024-01":"n"}},"aol":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"n"},"android":{"2024-01":"n"}},"samsung-email":{"android":{"2024-01":"y"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"2024-01":"u"}},"protonmail":{"desktop-webmail":{"2024-01":"u"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"hey":{"desktop-webmail":{"2024-01":"u"}},"mail-ru":{"desktop-webmail":{"2024-01":"y"}},"fastmail":{"desktop-webmail":{"2024-01":"u"}},"laposte":{"desktop-webmail":{"2024-01":"u"}},"gmx":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"web-de":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"ionos-1and1":{"desktop-webmail":{"2024-01":"u"},"android":{"2024-01":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":{"1":"Works with prefix `-webkit`"}
|
||||
},
|
||||
|
||||
{
|
||||
"slug":"css-background-blend-mode",
|
||||
"title":"background-blend-mode",
|
||||
@ -430,7 +446,7 @@
|
||||
"last_test_date":"2023-12-20",
|
||||
"test_url":"https://www.caniemail.com/tests/css-border-collapse.html",
|
||||
"test_results_url":"https://testi.at/proj/4zk4fe7tv86fn4bc6",
|
||||
"stats":{"apple-mail":{"macos":{"2023-12":"y"},"ios":{"2023-12":"y"}},"gmail":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"},"mobile-webmail":{"2023-12":"y"}},"orange":{"desktop-webmail":{"2023-12":"u"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"outlook":{"windows":{"2023-12":"y"},"windows-mail":{"2023-12":"y"},"macos":{"2023-12":"y"},"outlook-com":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"yahoo":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"aol":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"samsung-email":{"android":{"2023-12":"y"}},"sfr":{"desktop-webmail":{"2023-12":"u"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"thunderbird":{"macos":{"2023-12":"y"}},"protonmail":{"desktop-webmail":{"2023-12":"u"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"hey":{"desktop-webmail":{"2023-12":"u"}},"mail-ru":{"desktop-webmail":{"2023-12":"y"}},"fastmail":{"desktop-webmail":{"2023-12":"u"}},"laposte":{"desktop-webmail":{"2023-12":"u"}},"gmx":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"web-de":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"ionos-1and1":{"desktop-webmail":{"2023-12":"u"},"android":{"2023-12":"u"}}},
|
||||
"stats":{"apple-mail":{"macos":{"2023-12":"y"},"ios":{"2023-12":"y"}},"gmail":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"},"mobile-webmail":{"2023-12":"y"}},"orange":{"desktop-webmail":{"2023-12":"u"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"outlook":{"windows":{"2023-12":"y"},"windows-mail":{"2023-12":"y"},"macos":{"2023-12":"y"},"outlook-com":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"yahoo":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"aol":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"samsung-email":{"android":{"2023-12":"y"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"2023-12":"y"}},"protonmail":{"desktop-webmail":{"2023-12":"u"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"hey":{"desktop-webmail":{"2023-12":"u"}},"mail-ru":{"desktop-webmail":{"2023-12":"y"}},"fastmail":{"desktop-webmail":{"2023-12":"u"}},"laposte":{"desktop-webmail":{"2023-12":"u"}},"gmx":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"web-de":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"ionos-1and1":{"desktop-webmail":{"2023-12":"u"},"android":{"2023-12":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":null
|
||||
},
|
||||
@ -462,7 +478,7 @@
|
||||
"last_test_date":"2022-07-14",
|
||||
"test_url":"https://www.caniemail.com/tests/css-border-logical-properties.html",
|
||||
"test_results_url":"https://testi.at/proj/1yxFDAGtwrUmbf4tzMTY",
|
||||
"stats":{"apple-mail":{"macos":{"10.12":"n","10.13":"n","10.15":"a #1","11":"y","12":"y"},"ios":{"11":"n","12":"a #1","13":"a #1","14":"a #1","15":"y"}},"gmail":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"},"mobile-webmail":{"2022-07":"n"}},"orange":{"desktop-webmail":{"2022-07":"u"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2022-07":"n"},"macos":{"2011":"a #1","2016":"a #1","16.80":"n"},"outlook-com":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"}},"samsung-email":{"android":{"10":"a #1","11":"a #1"}},"sfr":{"desktop-webmail":{"2022-07":"u"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"thunderbird":{"macos":{"60.3":"u"}},"aol":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"}},"yahoo":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"}},"protonmail":{"desktop-webmail":{"2022-07":"u"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"hey":{"desktop-webmail":{"2022-07":"u"}},"mail-ru":{"desktop-webmail":{"2022-07":"y"}},"fastmail":{"desktop-webmail":{"2022-07":"u"}},"laposte":{"desktop-webmail":{"2022-07":"u"}},"gmx":{"desktop-webmail":{"2022-09":"n"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"web-de":{"desktop-webmail":{"2022-09":"n"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-09":"y"},"android":{"2022-09":"y"}}},
|
||||
"stats":{"apple-mail":{"macos":{"10.12":"n","10.13":"n","10.15":"a #1","11":"y","12":"y"},"ios":{"11":"n","12":"a #1","13":"a #1","14":"a #1","15":"y"}},"gmail":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"},"mobile-webmail":{"2022-07":"n"}},"orange":{"desktop-webmail":{"2022-07":"u"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2022-07":"n"},"macos":{"2011":"a #1","2016":"a #1","16.80":"n"},"outlook-com":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"}},"samsung-email":{"android":{"10":"a #1","11":"a #1"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"60.3":"u"}},"aol":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"}},"yahoo":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"}},"protonmail":{"desktop-webmail":{"2022-07":"u"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"hey":{"desktop-webmail":{"2022-07":"u"}},"mail-ru":{"desktop-webmail":{"2022-07":"y"}},"fastmail":{"desktop-webmail":{"2022-07":"u"}},"laposte":{"desktop-webmail":{"2022-07":"u"}},"gmx":{"desktop-webmail":{"2022-09":"n"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"web-de":{"desktop-webmail":{"2022-09":"n"},"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":"Partial. `border-<inline/block>-color` and `border-<inline/block>-width` does not work."}
|
||||
},
|
||||
@ -478,7 +494,7 @@
|
||||
"last_test_date":"2022-07-14",
|
||||
"test_url":"https://www.caniemail.com/tests/css-border-logical-properties.html",
|
||||
"test_results_url":"https://testi.at/proj/1yxFDAGtwrUmbf4tzMTY",
|
||||
"stats":{"apple-mail":{"macos":{"10.12":"n","10.13":"n","10.15":"y","11":"y","12":"y"},"ios":{"11":"n","12":"y","13":"y","14":"y","15":"y"}},"gmail":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"},"mobile-webmail":{"2022-07":"n"}},"orange":{"desktop-webmail":{"2022-07":"u"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2022-07":"n"},"macos":{"2011":"y","2016":"y","16.80":"n"},"outlook-com":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"}},"samsung-email":{"android":{"10":"y","11":"y"}},"sfr":{"desktop-webmail":{"2022-07":"u"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"thunderbird":{"macos":{"60.3":"u"}},"aol":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"}},"yahoo":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"}},"protonmail":{"desktop-webmail":{"2022-07":"u"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"hey":{"desktop-webmail":{"2022-07":"u"}},"mail-ru":{"desktop-webmail":{"2022-07":"y"}},"fastmail":{"desktop-webmail":{"2022-07":"u"}},"laposte":{"desktop-webmail":{"2022-07":"u"}},"gmx":{"desktop-webmail":{"2022-09":"n"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"web-de":{"desktop-webmail":{"2022-09":"n"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-09":"y"},"android":{"2022-09":"y"}}},
|
||||
"stats":{"apple-mail":{"macos":{"10.12":"n","10.13":"n","10.15":"y","11":"y","12":"y"},"ios":{"11":"n","12":"y","13":"y","14":"y","15":"y"}},"gmail":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"},"mobile-webmail":{"2022-07":"n"}},"orange":{"desktop-webmail":{"2022-07":"u"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2022-07":"n"},"macos":{"2011":"y","2016":"y","16.80":"n"},"outlook-com":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"}},"samsung-email":{"android":{"10":"y","11":"y"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"60.3":"u"}},"aol":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"}},"yahoo":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"}},"protonmail":{"desktop-webmail":{"2022-07":"u"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"hey":{"desktop-webmail":{"2022-07":"u"}},"mail-ru":{"desktop-webmail":{"2022-07":"y"}},"fastmail":{"desktop-webmail":{"2022-07":"u"}},"laposte":{"desktop-webmail":{"2022-07":"u"}},"gmx":{"desktop-webmail":{"2022-09":"n"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"web-de":{"desktop-webmail":{"2022-09":"n"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-09":"y"},"android":{"2022-09":"y"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":null
|
||||
},
|
||||
@ -494,7 +510,7 @@
|
||||
"last_test_date":"2022-07-13",
|
||||
"test_url":"https://www.caniemail.com/tests/css-border-logical-properties.html",
|
||||
"test_results_url":"https://testi.at/proj/1yxFDAGtwrUmbf4tzMTY",
|
||||
"stats":{"apple-mail":{"macos":{"10.15":"n","11":"y","12":"y"},"ios":{"13":"n","14":"n","15":"y"}},"gmail":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"},"mobile-webmail":{"2022-07":"n"}},"orange":{"desktop-webmail":{"2022-07":"u"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2022-07":"n"},"macos":{"2011":"n","2016":"n","16.80":"n"},"outlook-com":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"}},"samsung-email":{"android":{"10":"n","11":"n"}},"sfr":{"desktop-webmail":{"2022-07":"u"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"thunderbird":{"macos":{"60.3":"u"}},"aol":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"}},"yahoo":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"}},"protonmail":{"desktop-webmail":{"2022-07":"u"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"hey":{"desktop-webmail":{"2022-07":"u"}},"mail-ru":{"desktop-webmail":{"2022-07":"y"}},"fastmail":{"desktop-webmail":{"2022-07":"u"}},"laposte":{"desktop-webmail":{"2022-07":"u"}},"gmx":{"desktop-webmail":{"2022-09":"n"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"web-de":{"desktop-webmail":{"2022-09":"n"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-09":"y"},"android":{"2022-09":"y"}}},
|
||||
"stats":{"apple-mail":{"macos":{"10.15":"n","11":"y","12":"y"},"ios":{"13":"n","14":"n","15":"y"}},"gmail":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"},"mobile-webmail":{"2022-07":"n"}},"orange":{"desktop-webmail":{"2022-07":"u"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2022-07":"n"},"macos":{"2011":"n","2016":"n","16.80":"n"},"outlook-com":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"}},"samsung-email":{"android":{"10":"n","11":"n"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"60.3":"u"}},"aol":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"}},"yahoo":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"n"},"android":{"2022-07":"n"}},"protonmail":{"desktop-webmail":{"2022-07":"u"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"hey":{"desktop-webmail":{"2022-07":"u"}},"mail-ru":{"desktop-webmail":{"2022-07":"y"}},"fastmail":{"desktop-webmail":{"2022-07":"u"}},"laposte":{"desktop-webmail":{"2022-07":"u"}},"gmx":{"desktop-webmail":{"2022-09":"n"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"web-de":{"desktop-webmail":{"2022-09":"n"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-09":"y"},"android":{"2022-09":"y"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":null
|
||||
},
|
||||
@ -510,7 +526,7 @@
|
||||
"last_test_date":"2022-08-16",
|
||||
"test_url":"https://www.caniemail.com/tests/css-border-logical-properties.html",
|
||||
"test_results_url":"https://testi.at/proj/1yxFDAGtwrUmbf4tzMTY",
|
||||
"stats":{"apple-mail":{"macos":{"10.15":"n","11":"n","12":"y"},"ios":{"13":"n","14":"n","15":"y"}},"gmail":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"},"mobile-webmail":{"2022-08":"n"}},"orange":{"desktop-webmail":{"2022-08":"u"},"ios":{"2022-08":"u"},"android":{"2022-08":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2022-08":"n"},"macos":{"2011":"n","2016":"n","16.80":"n"},"outlook-com":{"2022-08":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"}},"samsung-email":{"android":{"10":"n","11":"n"}},"sfr":{"desktop-webmail":{"2022-08":"u"},"ios":{"2022-08":"u"},"android":{"2022-08":"u"}},"thunderbird":{"macos":{"60.3":"u"}},"aol":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"}},"yahoo":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"}},"protonmail":{"desktop-webmail":{"2022-08":"u"},"ios":{"2022-08":"u"},"android":{"2022-08":"u"}},"hey":{"desktop-webmail":{"2022-08":"u"}},"mail-ru":{"desktop-webmail":{"2022-08":"y"}},"fastmail":{"desktop-webmail":{"2022-08":"u"}},"laposte":{"desktop-webmail":{"2022-08":"u"}}},
|
||||
"stats":{"apple-mail":{"macos":{"10.15":"n","11":"n","12":"y"},"ios":{"13":"n","14":"n","15":"y"}},"gmail":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"},"mobile-webmail":{"2022-08":"n"}},"orange":{"desktop-webmail":{"2022-08":"u"},"ios":{"2022-08":"u"},"android":{"2022-08":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2022-08":"n"},"macos":{"2011":"n","2016":"n","16.80":"n"},"outlook-com":{"2022-08":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"}},"samsung-email":{"android":{"10":"n","11":"n"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"60.3":"u"}},"aol":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"}},"yahoo":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"}},"protonmail":{"desktop-webmail":{"2022-08":"u"},"ios":{"2022-08":"u"},"android":{"2022-08":"u"}},"hey":{"desktop-webmail":{"2022-08":"u"}},"mail-ru":{"desktop-webmail":{"2022-08":"y"}},"fastmail":{"desktop-webmail":{"2022-08":"u"}},"laposte":{"desktop-webmail":{"2022-08":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":null
|
||||
},
|
||||
@ -542,7 +558,7 @@
|
||||
"last_test_date":"2023-12-20",
|
||||
"test_url":"https://www.caniemail.com/tests/css-border-spacing.html",
|
||||
"test_results_url":"https://testi.at/proj/dyodfk8c5dhjanflz",
|
||||
"stats":{"apple-mail":{"macos":{"2023-12":"y"},"ios":{"2023-12":"y"}},"gmail":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"},"mobile-webmail":{"2023-12":"y"}},"orange":{"desktop-webmail":{"2023-12":"u"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2023-12":"n"},"macos":{"2023-12":"n"},"outlook-com":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"yahoo":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"aol":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"samsung-email":{"android":{"2023-12":"y"}},"sfr":{"desktop-webmail":{"2023-12":"u"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"thunderbird":{"macos":{"2023-12":"y"}},"protonmail":{"desktop-webmail":{"2023-12":"u"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"hey":{"desktop-webmail":{"2023-12":"u"}},"mail-ru":{"desktop-webmail":{"2023-12":"y"}},"fastmail":{"desktop-webmail":{"2023-12":"u"}},"laposte":{"desktop-webmail":{"2023-12":"u"}},"gmx":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"web-de":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"ionos-1and1":{"desktop-webmail":{"2023-12":"u"},"android":{"2023-12":"u"}}},
|
||||
"stats":{"apple-mail":{"macos":{"2023-12":"y"},"ios":{"2023-12":"y"}},"gmail":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"},"mobile-webmail":{"2023-12":"y"}},"orange":{"desktop-webmail":{"2023-12":"u"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2023-12":"n"},"macos":{"2023-12":"n"},"outlook-com":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"yahoo":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"aol":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"samsung-email":{"android":{"2023-12":"y"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"2023-12":"y"}},"protonmail":{"desktop-webmail":{"2023-12":"u"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"hey":{"desktop-webmail":{"2023-12":"u"}},"mail-ru":{"desktop-webmail":{"2023-12":"y"}},"fastmail":{"desktop-webmail":{"2023-12":"u"}},"laposte":{"desktop-webmail":{"2023-12":"u"}},"gmx":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"web-de":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"ionos-1and1":{"desktop-webmail":{"2023-12":"u"},"android":{"2023-12":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":null
|
||||
},
|
||||
@ -638,9 +654,9 @@
|
||||
"last_test_date":"2023-09-18",
|
||||
"test_url":"https://www.caniemail.com/tests/css-color-scheme.html",
|
||||
"test_results_url":"",
|
||||
"stats":{"apple-mail":{"macos":{"16.0":"y #1"},"ios":{"16.1":"y #1"}},"gmail":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"n"},"mobile-webmail":{"2023-09":"n"}},"orange":{"desktop-webmail":{"2022-12":"u"},"ios":{"2022-12":"u"},"android":{"2022-12":"u"}},"outlook":{"windows":{"2003":"u","2007":"u","2010":"u","2013":"u","2016":"n","2019":"n"},"windows-mail":{"2020-01":"u"},"macos":{"16.73":"n","16.80":"n"},"outlook-com":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"n"}},"samsung-email":{"android":{"6.1":"y"}},"sfr":{"desktop-webmail":{"2022-12":"u"},"ios":{"2022-12":"u"},"android":{"2022-12":"u"}},"thunderbird":{"macos":{"102.11":"y"}},"aol":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"n"}},"yahoo":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"n"}},"protonmail":{"desktop-webmail":{"2023-09":"y"},"ios":{"2023-09":"n"},"android":{"2023-09":"n"}},"hey":{"desktop-webmail":{"2023-09":"n"}},"mail-ru":{"desktop-webmail":{"2023-09":"n"}},"fastmail":{"desktop-webmail":{"2023-09":"n"}},"laposte":{"desktop-webmail":{"2022-12":"u"}},"free-fr":{"desktop-webmail":{"2022-12":"u"}},"t-online-de":{"desktop-webmail":{"2023-09":"n"}},"gmx":{"desktop-webmail":{"2023-09":"n"},"ios":{"2022-06":"u"},"android":{"2022-06":"u"}},"web-de":{"desktop-webmail":{"2023-09":"n"},"ios":{"2022-06":"u"},"android":{"2022-06":"u"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"u"},"android":{"2022-06":"u"}}},
|
||||
"stats":{"apple-mail":{"macos":{"16.0":"y #1"},"ios":{"16.1":"y #1"}},"gmail":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"n"},"mobile-webmail":{"2023-09":"n"}},"orange":{"desktop-webmail":{"2022-12":"u"},"ios":{"2022-12":"u"},"android":{"2022-12":"u"}},"outlook":{"windows":{"2003":"u","2007":"u","2010":"u","2013":"u","2016":"n","2019":"n"},"windows-mail":{"2020-01":"u"},"macos":{"16.73":"n","16.80":"n"},"outlook-com":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"n"}},"samsung-email":{"android":{"6.1":"y"}},"sfr":{"desktop-webmail":{"2024-03":"a #2"},"ios":{"2024-03":"n"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"102.11":"y"}},"aol":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"n"}},"yahoo":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"n"}},"protonmail":{"desktop-webmail":{"2023-09":"y"},"ios":{"2023-09":"n"},"android":{"2023-09":"n"}},"hey":{"desktop-webmail":{"2023-09":"n"}},"mail-ru":{"desktop-webmail":{"2023-09":"n"}},"fastmail":{"desktop-webmail":{"2023-09":"n"}},"laposte":{"desktop-webmail":{"2022-12":"u"}},"free-fr":{"desktop-webmail":{"2022-12":"u"}},"t-online-de":{"desktop-webmail":{"2023-09":"n"}},"gmx":{"desktop-webmail":{"2023-09":"n"},"ios":{"2022-06":"u"},"android":{"2022-06":"u"}},"web-de":{"desktop-webmail":{"2023-09":"n"},"ios":{"2022-06":"u"},"android":{"2022-06":"u"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"u"},"android":{"2022-06":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":{"1":"Works only the html/root element"}
|
||||
"notes_by_num":{"1":"Works only the html/root element","2":"Buggy. The property is supported but does not work due to a prefix added by the webmail on the rule selector."}
|
||||
},
|
||||
|
||||
{
|
||||
@ -670,7 +686,7 @@
|
||||
"last_test_date":"2022-08-02",
|
||||
"test_url":"https://www.caniemail.com/tests/css-column-layout.html",
|
||||
"test_results_url":"https://testi.at/proj/E87UgpgtlXxt6Rsx4Ec1pcxm",
|
||||
"stats":{"apple-mail":{"macos":{"15":"y"},"ios":{"15":"y"}},"gmail":{"desktop-webmail":{"2022-08":"y"},"ios":{"2022-08":"y"},"android":{"2022-08":"y"},"mobile-webmail":{"2022-08":"y"}},"orange":{"desktop-webmail":{"2022-08":"u"},"ios":{"2022-08":"u"},"android":{"2022-08":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2022-08":"n"},"macos":{"2022-08":"y","16.80":"y"},"outlook-com":{"2022-08":"y"},"ios":{"2022-08":"y"},"android":{"2022-08":"y"}},"samsung-email":{"android":{"2022-08":"y"}},"sfr":{"desktop-webmail":{"2022-08":"u"},"ios":{"2022-08":"u"},"android":{"2022-08":"u"}},"thunderbird":{"macos":{"102.7":"y"}},"aol":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"}},"yahoo":{"desktop-webmail":{"2022-08":"n","2023-01":"n"},"ios":{"2022-08":"n","2023-01":"n"},"android":{"2022-08":"n"}},"protonmail":{"desktop-webmail":{"2023-01":"y"},"ios":{"2023-01":"y"},"android":{"2023-01":"y"}},"hey":{"desktop-webmail":{"2023-01":"y"}},"mail-ru":{"desktop-webmail":{"2022-08":"y"}},"fastmail":{"desktop-webmail":{"2023-01":"y"}},"laposte":{"desktop-webmail":{"2022-08":"u"}}},
|
||||
"stats":{"apple-mail":{"macos":{"15":"y"},"ios":{"15":"y"}},"gmail":{"desktop-webmail":{"2022-08":"y"},"ios":{"2022-08":"y"},"android":{"2022-08":"y"},"mobile-webmail":{"2022-08":"y"}},"orange":{"desktop-webmail":{"2022-08":"u"},"ios":{"2022-08":"u"},"android":{"2022-08":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2022-08":"n"},"macos":{"2022-08":"y","16.80":"y"},"outlook-com":{"2022-08":"y"},"ios":{"2022-08":"y"},"android":{"2022-08":"y"}},"samsung-email":{"android":{"2022-08":"y"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"102.7":"y"}},"aol":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"}},"yahoo":{"desktop-webmail":{"2022-08":"n","2023-01":"n"},"ios":{"2022-08":"n","2023-01":"n"},"android":{"2022-08":"n"}},"protonmail":{"desktop-webmail":{"2023-01":"y"},"ios":{"2023-01":"y"},"android":{"2023-01":"y"}},"hey":{"desktop-webmail":{"2023-01":"y"}},"mail-ru":{"desktop-webmail":{"2022-08":"y"}},"fastmail":{"desktop-webmail":{"2023-01":"y"}},"laposte":{"desktop-webmail":{"2022-08":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":null
|
||||
},
|
||||
@ -846,11 +862,27 @@
|
||||
"last_test_date":"2022-08-01",
|
||||
"test_url":"https://www.caniemail.com/tests/css-font-kerning.html",
|
||||
"test_results_url":"https://app.emailonacid.com/app/acidtest/RlRYNGDjVNBhofxCNxloUcRbUVWGDhJ2kZ4fy6HXpEatH/list",
|
||||
"stats":{"apple-mail":{"macos":{"11":"y","12":"y","13":"y","14":"y","15":"y"},"ios":{"11":"y","12":"y","13":"y","14":"y","15":"y"}},"gmail":{"desktop-webmail":{"2022-08":"y"},"ios":{"2022-08":"y"},"android":{"2022-08":"y"},"mobile-webmail":{"2022-08":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"macos":{"2022-08":"y","16.80":"y"},"outlook-com":{"2022-08":"y"},"ios":{"2022-08":"y"},"android":{"4.2101.1":"y"}},"yahoo":{"desktop-webmail":{"2022-08":"n"}},"aol":{"desktop-webmail":{"2022-08":"n"}},"samsung-email":{"android":{"6.1.31.2":"y"}},"mail-ru":{"desktop-webmail":{"2022-08":"n"}},"free-fr":{"desktop-webmail":{"2022-08":"y"}},"gmx":{"desktop-webmail":{"2022-08":"n"}},"t-online-de":{"desktop-webmail":{"2022-08":"n"}}},
|
||||
"stats":{"apple-mail":{"macos":{"11":"y","12":"y","13":"y","14":"y","15":"y"},"ios":{"11":"y","12":"y","13":"y","14":"y","15":"y"}},"gmail":{"desktop-webmail":{"2022-08":"y"},"ios":{"2022-08":"y"},"android":{"2022-08":"y"},"mobile-webmail":{"2022-08":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"macos":{"2022-08":"y","16.80":"y"},"outlook-com":{"2022-08":"y"},"ios":{"2022-08":"y"},"android":{"4.2101.1":"y"}},"yahoo":{"desktop-webmail":{"2022-08":"n","2024-03":"n"},"ios":{"2024-03":"n"},"android":{"2024-03":"n"}},"aol":{"desktop-webmail":{"2022-08":"n"}},"samsung-email":{"android":{"6.1.31.2":"y"}},"mail-ru":{"desktop-webmail":{"2022-08":"n"}},"free-fr":{"desktop-webmail":{"2022-08":"y"}},"gmx":{"desktop-webmail":{"2022-08":"n"}},"t-online-de":{"desktop-webmail":{"2022-08":"n"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"u"},"android":{"2024-03":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":null
|
||||
},
|
||||
|
||||
{
|
||||
"slug":"css-font-size",
|
||||
"title":"font-size",
|
||||
"description":"Sets the size of the font.",
|
||||
"url":"https://www.caniemail.com/features/css-font-size/",
|
||||
"category":"css",
|
||||
"tags":[],
|
||||
"keywords":"font,size",
|
||||
"last_test_date":"2024-02-28",
|
||||
"test_url":"https://www.caniemail.com/tests/css-font-size.html",
|
||||
"test_results_url":"https://testi.at/proj/vr3ai85bunngsxjjfd2",
|
||||
"stats":{"apple-mail":{"macos":{"11":"y","12":"y","13":"y","14":"y","15":"y"},"ios":{"11":"y","12":"y","13":"y","14":"y","15":"y"}},"gmail":{"desktop-webmail":{"2024-02":"y"},"ios":{"2024-02":"y"},"android":{"2024-02":"y"},"mobile-webmail":{"2024-02":"y"}},"orange":{"desktop-webmail":{"2024-02":"u"},"ios":{"2024-02":"u"},"android":{"2024-02":"u"}},"outlook":{"windows":{"2013":"a #1","2016":"a #1","2019":"a #1","2021":"a #1"},"windows-mail":{"2024-02":"a #2"},"macos":{"2024-02":"y"},"outlook-com":{"2024-02":"y"},"ios":{"2024-02":"y"},"android":{"2024-02":"y"}},"yahoo":{"desktop-webmail":{"2024-02":"a #1"},"ios":{"2024-02":"a #1"},"android":{"2024-02":"a #1"}},"aol":{"desktop-webmail":{"2024-02":"a #1"},"ios":{"2024-02":"a #1"},"android":{"2024-02":"a #1"}},"samsung-email":{"android":{"2024-02":"a #2"}},"sfr":{"desktop-webmail":{"2024-02":"u"},"ios":{"2024-02":"u"},"android":{"2024-02":"u"}},"thunderbird":{"macos":{"2024-02":"y"}},"protonmail":{"desktop-webmail":{"2024-02":"u"},"ios":{"2024-02":"u"},"android":{"2024-02":"u"}},"hey":{"desktop-webmail":{"2024-02":"u"}},"mail-ru":{"desktop-webmail":{"2024-02":"y"}},"fastmail":{"desktop-webmail":{"2024-02":"u"}},"laposte":{"desktop-webmail":{"2024-02":"u"}},"gmx":{"desktop-webmail":{"2024-02":"a #1"},"ios":{"2024-02":"u"},"android":{"2024-02":"u"}},"web-de":{"desktop-webmail":{"2024-02":"a #1"},"ios":{"2024-02":"u"},"android":{"2024-02":"u"}},"ionos-1and1":{"desktop-webmail":{"2024-02":"u"},"android":{"2024-02":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":{"1":"Partial support. `rem` values are not supported.","2":"Partial support. `relative` and `percentage` size values not supported."}
|
||||
},
|
||||
|
||||
{
|
||||
"slug":"css-font-stretch",
|
||||
"title":"font-stretch",
|
||||
@ -862,7 +894,7 @@
|
||||
"last_test_date":"2023-09-23",
|
||||
"test_url":"https://www.caniemail.com/tests/css-font-stretch.html",
|
||||
"test_results_url":"",
|
||||
"stats":{"apple-mail":{"macos":{"16":"y"},"ios":{"16":"y"}},"gmail":{"desktop-webmail":{"2023-09":"y #1"},"ios":{"2023-09":"y"},"android":{"2023-09":"u"},"mobile-webmail":{"2023-09":"y #1"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"macos":{"2023-09":"y","16.80":"y"},"outlook-com":{"2023-09":"y #1"},"ios":{"2023-09":"y"},"android":{"4.2101.1":"u"}},"yahoo":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2019-09":"u"}},"aol":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"}},"protonmail":{"desktop-webmail":{"2023-09":"y #1"},"ios":{"2023-09":"y"},"android":{"2022-12":"u"}},"thunderbird":{"macos":{"2023-09":"y"}},"samsung-email":{"android":{"6.1.31.2":"u","6.1.90.16":"n"}},"mail-ru":{"desktop-webmail":{"2024-01":"y"}},"free-fr":{"desktop-webmail":{"2022-08":"u"}},"gmx":{"desktop-webmail":{"2022-08":"u"}},"t-online-de":{"desktop-webmail":{"2022-08":"u"}}},
|
||||
"stats":{"apple-mail":{"macos":{"16":"y"},"ios":{"16":"y"}},"gmail":{"desktop-webmail":{"2023-09":"y #1"},"ios":{"2023-09":"y"},"android":{"2024-03":"n"},"mobile-webmail":{"2023-09":"y #1"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"macos":{"2023-09":"y","16.80":"y"},"outlook-com":{"2023-09":"y #1"},"ios":{"2023-09":"y"},"android":{"2024-03":"n"}},"yahoo":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2024-03":"n"}},"aol":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"}},"protonmail":{"desktop-webmail":{"2023-09":"y #1"},"ios":{"2023-09":"y"},"android":{"2022-12":"u"}},"thunderbird":{"macos":{"2023-09":"y"}},"samsung-email":{"android":{"6.1.31.2":"u","6.1.90.16":"n"}},"mail-ru":{"desktop-webmail":{"2024-01":"y"}},"free-fr":{"desktop-webmail":{"2022-08":"u"}},"gmx":{"desktop-webmail":{"2022-08":"u"}},"t-online-de":{"desktop-webmail":{"2022-08":"u"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"n"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":{"1":"Partial. Depends on the browser. There is currently a bug in Chrome/Edge where font-stretch does not work for variable fonts."}
|
||||
},
|
||||
@ -1006,7 +1038,7 @@
|
||||
"last_test_date":"2022-08-03",
|
||||
"test_url":"https://www.caniemail.com/tests/css-hyphen.html",
|
||||
"test_results_url":"https://testi.at/proj/rvEUZkBsPVNSbRohvoRigyRiM",
|
||||
"stats":{"apple-mail":{"macos":{"16":"y","17":"y","18":"y","19":"y","20":"y","21":"y"},"ios":{"11":"y","12":"y","13":"y","14":"y","15":"y"}},"gmail":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"},"mobile-webmail":{"2022-08":"u"}},"orange":{"desktop-webmail":{"2022-08":"u"},"ios":{"2022-08":"u"},"android":{"2022-08":"u"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2022-08":"n"},"macos":{"2022-08":"y","16.80":"n"},"outlook-com":{"2022-08":"n","2023-12":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"}},"yahoo":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"}},"aol":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"u"},"android":{"2022-08":"u"}},"samsung-email":{"android":{"2022-08":"y"}},"sfr":{"desktop-webmail":{"2022-08":"u"},"ios":{"2022-08":"u"},"android":{"2022-08":"u"}},"protonmail":{"desktop-webmail":{"2022-08":"u"},"ios":{"2022-08":"u"},"android":{"2022-08":"u"}},"hey":{"desktop-webmail":{"2022-08":"u"}},"mail-ru":{"desktop-webmail":{"2022-08":"y"}},"fastmail":{"desktop-webmail":{"2022-08":"u"}}},
|
||||
"stats":{"apple-mail":{"macos":{"16":"y","17":"y","18":"y","19":"y","20":"y","21":"y"},"ios":{"11":"y","12":"y","13":"y","14":"y","15":"y"}},"gmail":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"},"mobile-webmail":{"2022-08":"u"}},"orange":{"desktop-webmail":{"2022-08":"u"},"ios":{"2022-08":"u"},"android":{"2022-08":"u"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2022-08":"n"},"macos":{"2022-08":"y","16.80":"n"},"outlook-com":{"2022-08":"n","2023-12":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"}},"yahoo":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"}},"aol":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"u"},"android":{"2022-08":"u"}},"samsung-email":{"android":{"2022-08":"y"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"protonmail":{"desktop-webmail":{"2022-08":"u"},"ios":{"2022-08":"u"},"android":{"2022-08":"u"}},"hey":{"desktop-webmail":{"2022-08":"u"}},"mail-ru":{"desktop-webmail":{"2022-08":"y"}},"fastmail":{"desktop-webmail":{"2022-08":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":null
|
||||
},
|
||||
@ -1422,7 +1454,7 @@
|
||||
"last_test_date":"2023-08-31",
|
||||
"test_url":"https://www.caniemail.com/tests/css-nesting.html",
|
||||
"test_results_url":"https://app.emailonacid.com/app/acidtest/8z9ecWkyaSHebmYl0r6dlWFfcia0VNfeKu6s01l5Fw3M0/list",
|
||||
"stats":{"apple-mail":{"macos":{"16.0":"a #1"},"ios":{"16.6":"a #1","17.2":"y"}},"gmail":{"desktop-webmail":{"2023-08":"n"},"ios":{"2023-08":"n"},"android":{"2023-08":"n"},"mobile-webmail":{"2023-08":"n"}},"orange":{"desktop-webmail":{"2023-08":"a #2"},"ios":{"2023-08":"a #2"},"android":{"2023-08":"u"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2023-08":"n"},"macos":{"16.78":"a #1","16.80":"n"},"outlook-com":{"2023-08":"n","2024-01":"n"},"ios":{"2023-08":"n"},"android":{"2023-08":"u"}},"samsung-email":{"android":{"6.0":"u","6.1.90.16":"a #4"}},"sfr":{"desktop-webmail":{"2023-08":"a #1 #2"},"ios":{"2023-08":"n"},"android":{"2023-08":"u"}},"thunderbird":{"macos":{"102.15":"n"}},"aol":{"desktop-webmail":{"2024-01":"n #3"},"ios":{"2023-08":"u"},"android":{"2023-08":"u"}},"yahoo":{"desktop-webmail":{"2023-08":"n #3"},"ios":{"2023-08":"n #3"},"android":{"2023-08":"u"}},"protonmail":{"desktop-webmail":{"2023-08":"u"},"ios":{"2023-08":"u"},"android":{"2023-08":"u"}},"hey":{"desktop-webmail":{"2023-08":"u"}},"mail-ru":{"desktop-webmail":{"2024-01":"n"}},"fastmail":{"desktop-webmail":{"2023-08":"u"}},"laposte":{"desktop-webmail":{"2023-08":"u"}},"free-fr":{"desktop-webmail":{"2023-08":"u"}},"t-online-de":{"desktop-webmail":{"2023-08":"u"}},"gmx":{"desktop-webmail":{"2021-12":"u"},"ios":{"2021-12":"u"},"android":{"2021-12":"u"}},"web-de":{"desktop-webmail":{"2021-12":"u"},"ios":{"2021-12":"u"},"android":{"2021-12":"u"}},"ionos-1and1":{"desktop-webmail":{"2021-12":"u"},"android":{"2021-12":"u"}}},
|
||||
"stats":{"apple-mail":{"macos":{"16.0":"a #1"},"ios":{"16.6":"a #1","17.2":"y"}},"gmail":{"desktop-webmail":{"2023-08":"n"},"ios":{"2023-08":"n"},"android":{"2023-08":"n"},"mobile-webmail":{"2023-08":"n"}},"orange":{"desktop-webmail":{"2023-08":"a #2"},"ios":{"2023-08":"a #2"},"android":{"2023-08":"u"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2023-08":"n"},"macos":{"16.78":"a #1","16.80":"n"},"outlook-com":{"2023-08":"n","2024-01":"n"},"ios":{"2023-08":"n"},"android":{"2024-03":"n"}},"samsung-email":{"android":{"6.0":"u","6.1.90.16":"a #4"}},"sfr":{"desktop-webmail":{"2023-08":"a #1 #2"},"ios":{"2023-08":"n"},"android":{"2023-08":"u"}},"thunderbird":{"macos":{"102.15":"n"}},"aol":{"desktop-webmail":{"2024-01":"n #3"},"ios":{"2023-08":"u"},"android":{"2023-08":"u"}},"yahoo":{"desktop-webmail":{"2023-08":"n #3"},"ios":{"2023-08":"n #3"},"android":{"2024-03":"n #3"}},"protonmail":{"desktop-webmail":{"2023-08":"u"},"ios":{"2023-08":"u"},"android":{"2023-08":"u"}},"hey":{"desktop-webmail":{"2023-08":"u"}},"mail-ru":{"desktop-webmail":{"2024-01":"n"}},"fastmail":{"desktop-webmail":{"2023-08":"u"}},"laposte":{"desktop-webmail":{"2023-08":"u"}},"free-fr":{"desktop-webmail":{"2023-08":"u"}},"t-online-de":{"desktop-webmail":{"2023-08":"u"}},"gmx":{"desktop-webmail":{"2021-12":"u"},"ios":{"2021-12":"u"},"android":{"2021-12":"u"}},"web-de":{"desktop-webmail":{"2021-12":"u"},"ios":{"2021-12":"u"},"android":{"2021-12":"u"}},"ionos-1and1":{"desktop-webmail":{"2021-12":"u"},"android":{"2021-12":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":{"1":"Partial. `E { F {}}` doesn’t work, but `E { & F {}}` does. Full support was added in macOS 14.2.","2":"Buggy. The syntax is supported, but nested selectors are prefixed by the webmail, which might invalidate the selector.","3":"Not supported. The nested selectors are removed, making the nested properties apply to the parent selector.","4":"Partial. Not supported with Hotmail/Outlook accounts."}
|
||||
},
|
||||
@ -2051,6 +2083,22 @@
|
||||
"notes_by_num":{"1":"Gradients can be created in VML using `type=\"gradientRadial\"` on a `<v:fill>`. See [VML documentation](https://docs.microsoft.com/en-us/windows/win32/vml/web-workshop---how-to-use-vml-on-web-pages-----fill--element#gradient-fill)."}
|
||||
},
|
||||
|
||||
{
|
||||
"slug":"css-resize",
|
||||
"title":"resize",
|
||||
"description":"Sets whether an element is resizable, and in which directions.",
|
||||
"url":"https://www.caniemail.com/features/css-resize/",
|
||||
"category":"css",
|
||||
"tags":[],
|
||||
"keywords":"textarea",
|
||||
"last_test_date":"2024-01-17",
|
||||
"test_url":"https://www.caniemail.com/tests/css-resize.html",
|
||||
"test_results_url":"https://testi.at/proj/6vd212zjibljibxkt9",
|
||||
"stats":{"apple-mail":{"macos":{"2024-01":"a #2"},"ios":{"2024-01":"n"}},"gmail":{"desktop-webmail":{"2024-01":"n #1"},"ios":{"2024-01":"n #1"},"android":{"2024-01":"n"},"mobile-webmail":{"2024-01":"u"}},"orange":{"desktop-webmail":{"2024-01":"u"},"ios":{"2024-01":"n"},"android":{"2024-01":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2024-01":"n"},"macos":{"2024-01":"n #1"},"outlook-com":{"2024-01":"n #1"},"ios":{"2024-01":"n #1"},"android":{"2024-01":"n"}},"yahoo":{"desktop-webmail":{"2024-01":"n #1"},"ios":{"2024-01":"n"},"android":{"2024-01":"n"}},"aol":{"desktop-webmail":{"2024-01":"n #1"},"ios":{"2024-01":"n"},"android":{"2024-01":"n"}},"samsung-email":{"android":{"2024-01":"y"}},"sfr":{"desktop-webmail":{"2024-01":"u"},"ios":{"2024-01":"n"},"android":{"2024-01":"u"}},"thunderbird":{"macos":{"2024-01":"u"}},"protonmail":{"desktop-webmail":{"2024-01":"u"},"ios":{"2024-01":"n"},"android":{"2024-01":"u"}},"hey":{"desktop-webmail":{"2024-01":"u"}},"mail-ru":{"desktop-webmail":{"2024-01":"y"}},"fastmail":{"desktop-webmail":{"2024-01":"u"}},"laposte":{"desktop-webmail":{"2024-01":"u"}},"gmx":{"desktop-webmail":{"2024-01":"n #1"},"ios":{"2024-01":"n"},"android":{"2024-01":"u"}},"web-de":{"desktop-webmail":{"2024-01":"n #1"},"ios":{"2024-01":"n"},"android":{"2024-01":"u"}},"ionos-1and1":{"desktop-webmail":{"2024-01":"u"},"android":{"2024-01":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":{"1":"`resize` property is stripped from style tag","2":"Does not support `inline` and `block` values"}
|
||||
},
|
||||
|
||||
{
|
||||
"slug":"css-rgb",
|
||||
"title":"rgb()",
|
||||
@ -2275,6 +2323,38 @@
|
||||
"notes_by_num":{"1":"Partial. Not supported with non Google accounts.","2":"Buggy. The first `<head>` in the HTML is removed, so `<style>` elements need to be in a second `<head>` element.","3":"Not supported. The selector is removed and left prefixed with the outer most parent element."}
|
||||
},
|
||||
|
||||
{
|
||||
"slug":"css-shape-margin",
|
||||
"title":"shape-margin",
|
||||
"description":"Sets a margin for a CSS shape created using `shape-outside`",
|
||||
"url":"https://www.caniemail.com/features/css-shape-margin/",
|
||||
"category":"css",
|
||||
"tags":[],
|
||||
"keywords":"shape, margin",
|
||||
"last_test_date":"2024-03-13",
|
||||
"test_url":"https://www.caniemail.com/tests/css-shape-margin.html",
|
||||
"test_results_url":"https://testi.at/proj/l2xyho58cl2yfnknhb",
|
||||
"stats":{"apple-mail":{"macos":{"2024-03":"y"},"ios":{"2024-03":"y"}},"gmail":{"desktop-webmail":{"2024-03":"n"},"ios":{"2024-03":"n"},"android":{"2024-03":"n"},"mobile-webmail":{"2024-03":"n"}},"orange":{"desktop-webmail":{"2024-03":"u"},"ios":{"2024-03":"u"},"android":{"2024-03":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2024-03":"n"},"macos":{"16.56":"y"},"outlook-com":{"2024-03":"n"},"ios":{"2024-03":"n"},"android":{"2024-03":"n"}},"samsung-email":{"android":{"2024-03":"y"}},"sfr":{"desktop-webmail":{"2024-03":"u"},"ios":{"2024-03":"u"},"android":{"2024-03":"u"}},"thunderbird":{"macos":{"2024-03":"y"}},"aol":{"desktop-webmail":{"2024-03":"n"},"ios":{"2024-03":"n"},"android":{"2024-03":"n"}},"yahoo":{"desktop-webmail":{"2024-03":"n"},"ios":{"2024-03":"n"},"android":{"2024-03":"n"}},"protonmail":{"desktop-webmail":{"2024-03":"u"},"ios":{"2024-03":"u"},"android":{"2024-03":"u"}},"hey":{"desktop-webmail":{"2024-03":"u"}},"mail-ru":{"desktop-webmail":{"2024-03":"y"}},"fastmail":{"desktop-webmail":{"2024-03":"u"}},"laposte":{"desktop-webmail":{"2024-03":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":null
|
||||
},
|
||||
|
||||
{
|
||||
"slug":"css-shape-outside",
|
||||
"title":"shape-outside",
|
||||
"description":"Defines a shape, which may be non-rectangular, around which adjacent inline content should wrap.",
|
||||
"url":"https://www.caniemail.com/features/css-shape-outside/",
|
||||
"category":"css",
|
||||
"tags":[],
|
||||
"keywords":"shape",
|
||||
"last_test_date":"2024-02-28",
|
||||
"test_url":"https://www.caniemail.com/tests/css-shape-outside.html",
|
||||
"test_results_url":"https://testi.at/proj/6vdjc5l6ungvfjgu94",
|
||||
"stats":{"apple-mail":{"macos":{"15":"y"},"ios":{"15":"y"}},"gmail":{"desktop-webmail":{"2024-02":"n"},"ios":{"2024-02":"n"},"android":{"2024-02":"n"},"mobile-webmail":{"2024-02":"n"}},"orange":{"desktop-webmail":{"2024-02":"u"},"ios":{"2024-02":"u"},"android":{"2024-02":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2024-02":"n"},"macos":{"16.56":"y"},"outlook-com":{"2024-02":"n"},"ios":{"2024-02":"n"},"android":{"2024-02":"n"}},"samsung-email":{"android":{"2024-02":"y"}},"sfr":{"desktop-webmail":{"2024-02":"u"},"ios":{"2024-02":"u"},"android":{"2024-02":"u"}},"thunderbird":{"macos":{"2024-02":"y"}},"aol":{"desktop-webmail":{"2024-02":"n"},"ios":{"2024-02":"n"},"android":{"2024-02":"n"}},"yahoo":{"desktop-webmail":{"2024-02":"n"},"ios":{"2024-02":"n"},"android":{"2024-02":"n"}},"protonmail":{"desktop-webmail":{"2024-02":"u"},"ios":{"2024-02":"u"},"android":{"2024-02":"u"}},"hey":{"desktop-webmail":{"2024-02":"u"}},"mail-ru":{"desktop-webmail":{"2024-02":"y"}},"fastmail":{"desktop-webmail":{"2024-02":"u"}},"laposte":{"desktop-webmail":{"2024-02":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":null
|
||||
},
|
||||
|
||||
{
|
||||
"slug":"css-sytem-ui",
|
||||
"title":"system-ui, ui-serif, ui-sans-serif, ui-rounded, ui-monospace",
|
||||
@ -2334,7 +2414,7 @@
|
||||
"last_test_date":"2022-08-31",
|
||||
"test_url":"https://www.caniemail.com/tests/css-text-align-last.html",
|
||||
"test_results_url":"https://app.emailonacid.com/app/acidtest/LxplTmJT9Ilq9GUyn8Aq8MVK6EO427qmx1Ic4A7jc7bOJ/list",
|
||||
"stats":{"apple-mail":{"macos":{"2022-10":"y"},"ios":{"11":"n","12":"n","13":"n","14":"n","15":"n","16.0":"y"}},"gmail":{"desktop-webmail":{"2022-08":"y"},"ios":{"2022-08":"y"},"android":{"2022-08":"y"},"mobile-webmail":{"2021-08":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"macos":{"2022-08":"n","16.80":"n"},"outlook-com":{"2022-08":"y","2024-01":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"}},"yahoo":{"desktop-webmail":{"2022-08":"n"}},"aol":{"desktop-webmail":{"2022-08":"n"}},"samsung-email":{"android":{"6.1.51.1":"y"}},"free-fr":{"desktop-webmail":{"2022-08":"y"}},"t-online-de":{"desktop-webmail":{"2022-08":"y"}},"mail-ru":{"desktop-webmail":{"2022-08":"y"}}},
|
||||
"stats":{"apple-mail":{"macos":{"2022-10":"y"},"ios":{"11":"n","12":"n","13":"n","14":"n","15":"n","16.0":"y"}},"gmail":{"desktop-webmail":{"2022-08":"y"},"ios":{"2022-08":"y"},"android":{"2022-08":"y"},"mobile-webmail":{"2021-08":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"macos":{"2022-08":"n","16.80":"n"},"outlook-com":{"2022-08":"y","2024-01":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"}},"yahoo":{"desktop-webmail":{"2022-08":"n","2024-03":"n"},"ios":{"2024-03":"n"},"android":{"2024-03":"n"}},"aol":{"desktop-webmail":{"2022-08":"n"}},"samsung-email":{"android":{"6.1.51.1":"y"}},"free-fr":{"desktop-webmail":{"2022-08":"y"}},"t-online-de":{"desktop-webmail":{"2022-08":"y"}},"mail-ru":{"desktop-webmail":{"2022-08":"y"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":null
|
||||
},
|
||||
@ -2595,6 +2675,22 @@
|
||||
"notes_by_num":{"1":"Partial. Supports `under` but not `right` and `left`"}
|
||||
},
|
||||
|
||||
{
|
||||
"slug":"css-text-wrap",
|
||||
"title":"text-wrap",
|
||||
"description":"Controls how text inside an element is wrapped",
|
||||
"url":"https://www.caniemail.com/features/css-text-wrap/",
|
||||
"category":"css",
|
||||
"tags":[],
|
||||
"keywords":null,
|
||||
"last_test_date":"2024-04-03",
|
||||
"test_url":"https://www.caniemail.com/tests/css-text-wrap.html",
|
||||
"test_results_url":"https://testi.at/proj/xle5u5a5i9eh9opi7a",
|
||||
"stats":{"apple-mail":{"macos":{"2024-04":"y"},"ios":{"15":"y","14":"y"}},"gmail":{"desktop-webmail":{"2024-04":"n"},"ios":{"2024-04":"n"},"android":{"2024-04":"n"},"mobile-webmail":{"2024-04":"n"}},"orange":{"desktop-webmail":{"2024-04":"u"},"ios":{"2024-04":"u"},"android":{"2024-04":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2024-04":"n"},"macos":{"2024-04":"n"},"outlook-com":{"2024-04":"n","2024-01":"n"},"ios":{"2024-04":"n"},"android":{"2024-04":"n"}},"yahoo":{"desktop-webmail":{"2024-04":"n"},"ios":{"2024-04":"n"},"android":{"2024-04":"n"}},"aol":{"desktop-webmail":{"2024-04":"n"},"ios":{"2024-04":"n"},"android":{"2024-04":"n"}},"samsung-email":{"android":{"2024-04":"y"}},"sfr":{"desktop-webmail":{"2024-04":"u"},"ios":{"2024-04":"u"},"android":{"2024-04":"u"}},"thunderbird":{"macos":{"2024-04":"u"}},"protonmail":{"desktop-webmail":{"2024-04":"u"},"ios":{"2024-04":"u"},"android":{"2024-04":"u"}},"hey":{"desktop-webmail":{"2024-04":"u"}},"mail-ru":{"desktop-webmail":{"2024-04":"y"}},"fastmail":{"desktop-webmail":{"2024-04":"u"}},"laposte":{"desktop-webmail":{"2024-04":"u"}},"gmx":{"desktop-webmail":{"2024-04":"n"},"ios":{"2024-04":"u"},"android":{"2024-04":"u"}},"web-de":{"desktop-webmail":{"2024-04":"n"},"ios":{"2024-04":"u"},"android":{"2024-04":"u"}},"ionos-1and1":{"desktop-webmail":{"2024-04":"u"},"android":{"2024-04":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":null
|
||||
},
|
||||
|
||||
{
|
||||
"slug":"css-transform",
|
||||
"title":"transform",
|
||||
@ -2611,6 +2707,22 @@
|
||||
"notes_by_num":{"1":"Partial. `scale translate` is not supported."}
|
||||
},
|
||||
|
||||
{
|
||||
"slug":"css-transition",
|
||||
"title":"transition",
|
||||
"description":"Creates a visual transition when changing CSS properties.",
|
||||
"url":"https://www.caniemail.com/features/css-transition/",
|
||||
"category":"css",
|
||||
"tags":[],
|
||||
"keywords":"animation",
|
||||
"last_test_date":"2024-03-29",
|
||||
"test_url":"https://www.caniemail.com/tests/css-transition.html",
|
||||
"test_results_url":"https://app.emailonacid.com/app/acidtest/04RtAsXcEt68kJzhGogIDE8xcmwE7xuKdGPlz2b07ZfJY/list",
|
||||
"stats":{"apple-mail":{"macos":{"16.0":"y"},"ios":{"17.4":"y"}},"gmail":{"desktop-webmail":{"2024-03":"n"},"ios":{"2024-03":"n"},"android":{"2024-03":"n"},"mobile-webmail":{"2024-03":"n"}},"orange":{"desktop-webmail":{"2024-03":"n"},"ios":{"2024-03":"n"},"android":{"2024-03":"n"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2024-03":"n"},"macos":{"16.80":"n"},"outlook-com":{"2024-03":"n"},"ios":{"2024-03":"n"},"android":{"2024-03":"n"}},"samsung-email":{"android":{"6.1.90.16":"a #5"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"n"},"android":{"2024-03":"n"}},"thunderbird":{"macos":{"115.9.0":"y"}},"aol":{"desktop-webmail":{"2024-03":"a #1 #2"},"ios":{"2024-03":"a #1 #2"},"android":{"2024-03":"a #1 #2"}},"yahoo":{"desktop-webmail":{"2024-03":"a #1 #2"},"ios":{"2024-03":"a #1 #2"},"android":{"2024-03":"a #1 #2"}},"protonmail":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"n"},"android":{"2024-03":"u"}},"hey":{"desktop-webmail":{"2024-03":"a #3"}},"mail-ru":{"desktop-webmail":{"2024-03":"y"}},"fastmail":{"desktop-webmail":{"2024-03":"y #4"}},"laposte":{"desktop-webmail":{"2024-03":"y"}},"free-fr":{"desktop-webmail":{"2024-03":"y"}},"t-online-de":{"desktop-webmail":{"2024-03":"u"}},"gmx":{"desktop-webmail":{"2024-03":"u"},"ios":{"2024-03":"u"},"android":{"2024-03":"u"}},"web-de":{"desktop-webmail":{"2024-03":"u"},"ios":{"2024-03":"u"},"android":{"2024-03":"u"}},"ionos-1and1":{"desktop-webmail":{"2024-03":"u"},"android":{"2024-03":"u"}}},
|
||||
"notes":"This page accounts for the shorthand `transition` property and the longhand properties `transition-delay`, `transition-duration`, `transition-property` and `transition-timing-function`.",
|
||||
"notes_by_num":{"1":"Partial. Longhand properties are not supported.","2":"Buggy. The `all` keyword is not supported.","3":"Buggy. `transition-duration` is forced to `0` on a global reset style.","4":"Transition properties are supported but pseudo-classes like `:hover` are not.","5":"Partial. Not supported with Outlook accounts."}
|
||||
},
|
||||
|
||||
{
|
||||
"slug":"css-unit-calc",
|
||||
"title":"CSS calc() function",
|
||||
@ -2973,10 +3085,10 @@
|
||||
"keywords":null,
|
||||
"last_test_date":"2019-09-27",
|
||||
"test_url":"https://www.caniemail.com/tests/css-width-height.html",
|
||||
"test_results_url":"https://app.emailonacid.com/app/acidtest/UhsQmS14DHKFfotKEcCTnWaoAiS24FJMiApZ1OtmHR7vs/list",
|
||||
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.4":"y"}},"gmail":{"desktop-webmail":{"2019-09":"y"},"ios":{"2019-09":"y"},"android":{"2019-09":"y"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2019-09":"y","2021-03":"y"},"ios":{"2019-09":"y"},"android":{"2019-09":"y"}},"outlook":{"windows":{"2003":"y","2007":"a #1 #2","2010":"a #1 #2","2013":"a #1 #2","2016":"a #1 #2","2019":"a #1 #2"},"windows-mail":{"2019-09":"y"},"macos":{"2011":"y","2016":"y","16.80":"y"},"outlook-com":{"2019-09":"y","2024-01":"y"},"ios":{"2019-09":"y"},"android":{"2019-09":"y"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2019-09":"y"},"ios":{"2019-09":"y"},"android":{"2019-09":"y"}},"thunderbird":{"macos":{"60.3":"y"}},"aol":{"desktop-webmail":{"2019-09":"y"},"ios":{"2019-09":"y"},"android":{"2019-09":"y"}},"yahoo":{"desktop-webmail":{"2019-09":"y"},"ios":{"2019-09":"y"},"android":{"2019-09":"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":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"a #3"}},"gmx":{"desktop-webmail":{"2021-12":"y"},"ios":{"2021-12":"y"},"android":{"2021-12":"y"}},"web-de":{"desktop-webmail":{"2021-12":"y"},"ios":{"2021-12":"y"},"android":{"2021-12":"y"}},"ionos-1and1":{"desktop-webmail":{"2021-12":"y"},"android":{"2021-12":"y"}}},
|
||||
"test_results_url":"https://app.emailonacid.com/app/acidtest/N3bgM8CXDd1TNWZzO65F0RkiJwugaAuNYr8mvcYt1C3Da/list",
|
||||
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.4":"y"}},"gmail":{"desktop-webmail":{"2019-09":"y"},"ios":{"2019-09":"y"},"android":{"2019-09":"y"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2019-09":"y","2021-03":"y"},"ios":{"2019-09":"y"},"android":{"2019-09":"y"}},"outlook":{"windows":{"2003":"y","2007":"a #1","2010":"a #1","2013":"a #1","2016":"a #1","2019":"a #1"},"windows-mail":{"2019-09":"y"},"macos":{"2011":"y","2016":"y","16.80":"y"},"outlook-com":{"2019-09":"y","2024-01":"y"},"ios":{"2019-09":"y"},"android":{"2019-09":"y"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2019-09":"y"},"ios":{"2019-09":"y"},"android":{"2019-09":"y"}},"thunderbird":{"macos":{"60.3":"y"}},"aol":{"desktop-webmail":{"2019-09":"y"},"ios":{"2019-09":"y"},"android":{"2019-09":"y"}},"yahoo":{"desktop-webmail":{"2019-09":"y"},"ios":{"2019-09":"y"},"android":{"2019-09":"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":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"a #2"}},"gmx":{"desktop-webmail":{"2021-12":"y"},"ios":{"2021-12":"y"},"android":{"2021-12":"y"}},"web-de":{"desktop-webmail":{"2021-12":"y"},"ios":{"2021-12":"y"},"android":{"2021-12":"y"}},"ionos-1and1":{"desktop-webmail":{"2021-12":"y"},"android":{"2021-12":"y"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":{"1":"Buggy. Percentage width on `<img>` elements are based on the physical file's width, not on the parent element's width.","2":"Partial. Not supported on `<body>`, `<span>`, `<div>` or `<p>` elements.","3":"Buggy. The webmail has a generic style that sets `table { width:inherit; }`."}
|
||||
"notes_by_num":{"1":"Partial. Not supported on `<body>`, `<span>`, `<div>`, `<p>` or `<img>` elements.","2":"Buggy. The webmail has a generic style that sets `table { width:inherit; }`."}
|
||||
},
|
||||
|
||||
{
|
||||
@ -2995,6 +3107,22 @@
|
||||
"notes_by_num":{"1":"Supported. But Gmail adds `<wbr>` every 30 characters.","2":"Buggy. Supported but a `word-wrap:break-word` is applied, making it look like `break-all`.","3":"Partially supported. Only `word-break:break-all` works.","4":"Buggy. Every value is replaced by `break-word`."}
|
||||
},
|
||||
|
||||
{
|
||||
"slug":"css-word-spacing",
|
||||
"title":"word-spacing",
|
||||
"description":"Sets the length of space between words and between tags.",
|
||||
"url":"https://www.caniemail.com/features/css-word-spacing/",
|
||||
"category":"css",
|
||||
"tags":[],
|
||||
"keywords":"word",
|
||||
"last_test_date":"2024-03-13",
|
||||
"test_url":"https://www.caniemail.com/tests/css-word-spacing.html",
|
||||
"test_results_url":"https://testi.at/proj/8r8pfrlvcxklca32ij",
|
||||
"stats":{"apple-mail":{"macos":{"2024-03":"y"},"ios":{"2024-03":"y"}},"gmail":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"},"mobile-webmail":{"2024-03":"y"}},"orange":{"desktop-webmail":{"2024-03":"u"},"ios":{"2024-03":"u"},"android":{"2024-03":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2024-03":"n"},"macos":{"2024-03":"y"},"outlook-com":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"yahoo":{"desktop-webmail":{"2024-03":"a #1"},"ios":{"2024-03":"a #1"},"android":{"2024-03":"n"}},"aol":{"desktop-webmail":{"2024-03":"a #1"},"ios":{"2024-03":"a #1"},"android":{"2024-03":"n"}},"samsung-email":{"android":{"2024-03":"y"}},"sfr":{"desktop-webmail":{"2024-03":"u"},"ios":{"2024-03":"u"},"android":{"2024-03":"u"}},"thunderbird":{"macos":{"2024-03":"y"}},"protonmail":{"desktop-webmail":{"2024-03":"u"},"ios":{"2024-03":"u"},"android":{"2024-03":"u"}},"hey":{"desktop-webmail":{"2024-03":"u"}},"mail-ru":{"desktop-webmail":{"2024-03":"y"}},"fastmail":{"desktop-webmail":{"2024-03":"u"}},"laposte":{"desktop-webmail":{"2024-03":"u"}},"gmx":{"desktop-webmail":{"2024-03":"a #1"},"ios":{"2024-03":"u"},"android":{"2024-03":"u"}},"web-de":{"desktop-webmail":{"2024-03":"a #1"},"ios":{"2024-03":"u"},"android":{"2024-03":"u"}},"ionos-1and1":{"desktop-webmail":{"2024-03":"u"},"android":{"2024-03":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":{"1":"Does not support `rem` and `ch` values."}
|
||||
},
|
||||
|
||||
{
|
||||
"slug":"css-writing-mode",
|
||||
"title":"writing-mode",
|
||||
@ -3054,7 +3182,7 @@
|
||||
"last_test_date":"2024-01-03",
|
||||
"test_url":"https://www.caniemail.com/tests/html-acronym.html",
|
||||
"test_results_url":"https://testi.at/proj/ayebhgpxu58yce2bhd",
|
||||
"stats":{"apple-mail":{"macos":{"2024-01":"y"},"ios":{"2024-01":"y"}},"gmail":{"desktop-webmail":{"2024-01":"y"},"ios":{"2024-01":"y"},"android":{"2024-01":"y"},"mobile-webmail":{"2024-01":"y"}},"orange":{"desktop-webmail":{"2024-01":"u"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"outlook":{"windows":{"2013":"n #1","2016":"n #1","2019":"n #1","2021":"n #1"},"windows-mail":{"2024-01":"n #1"},"macos":{"2024-01":"y"},"outlook-com":{"2024-01":"y"},"ios":{"2024-01":"y"},"android":{"2024-01":"y"}},"samsung-email":{"android":{"2024-01":"y"}},"sfr":{"desktop-webmail":{"2024-01":"u"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"thunderbird":{"macos":{"2024-01":"y"}},"aol":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"n"},"android":{"2024-01":"n"}},"yahoo":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"n"},"android":{"2024-01":"n"}},"protonmail":{"desktop-webmail":{"2024-01":"u"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"hey":{"desktop-webmail":{"2024-01":"u"}},"mail-ru":{"desktop-webmail":{"2024-01":"n"}},"fastmail":{"desktop-webmail":{"2024-01":"u"}},"laposte":{"desktop-webmail":{"2024-01":"u"}},"free-fr":{"desktop-webmail":{"2024-01":"u"}},"t-online-de":{"desktop-webmail":{"2024-01":"y"}},"gmx":{"desktop-webmail":{"2024-01":"y"}},"web-de":{"desktop-webmail":{"2024-01":"y"}}},
|
||||
"stats":{"apple-mail":{"macos":{"2024-01":"y"},"ios":{"2024-01":"y"}},"gmail":{"desktop-webmail":{"2024-01":"y"},"ios":{"2024-01":"y"},"android":{"2024-01":"y"},"mobile-webmail":{"2024-01":"y"}},"orange":{"desktop-webmail":{"2024-01":"u"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"outlook":{"windows":{"2013":"n #1","2016":"n #1","2019":"n #1","2021":"n #1"},"windows-mail":{"2024-01":"n #1"},"macos":{"2024-01":"y"},"outlook-com":{"2024-01":"y"},"ios":{"2024-01":"y"},"android":{"2024-01":"y"}},"samsung-email":{"android":{"2024-01":"y"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"2024-01":"y"}},"aol":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"n"},"android":{"2024-01":"n"}},"yahoo":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"n"},"android":{"2024-01":"n"}},"protonmail":{"desktop-webmail":{"2024-01":"u"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"hey":{"desktop-webmail":{"2024-01":"u"}},"mail-ru":{"desktop-webmail":{"2024-01":"n"}},"fastmail":{"desktop-webmail":{"2024-01":"u"}},"laposte":{"desktop-webmail":{"2024-01":"u"}},"free-fr":{"desktop-webmail":{"2024-01":"u"}},"t-online-de":{"desktop-webmail":{"2024-01":"y"}},"gmx":{"desktop-webmail":{"2024-01":"y"}},"web-de":{"desktop-webmail":{"2024-01":"y"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":{"1":"Buggy. `title` attribute is removed but keeps `<acronym>` tag."}
|
||||
},
|
||||
@ -3358,7 +3486,7 @@
|
||||
"last_test_date":"2023-09-11",
|
||||
"test_url":"https://www.caniemail.com/tests/html-dfn.html",
|
||||
"test_results_url":"https://testi.at/proj/gy2dfo4j19d4176d08y",
|
||||
"stats":{"apple-mail":{"macos":{"2023-09":"y"},"ios":{"2023-09":"y"}},"gmail":{"desktop-webmail":{"2023-09":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"},"mobile-webmail":{"2023-09":"y"}},"orange":{"desktop-webmail":{"2023-09":"u"},"ios":{"2023-09":"u"},"android":{"2023-09":"u"}},"outlook":{"windows":{"2013":"a #1","2016":"a #1","2019":"a #1","2021":"a #1"},"windows-mail":{"2023-09":"a #1"},"macos":{"16.56":"y","16.80":"y"},"outlook-com":{"2023-09":"y","2024-01":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"}},"samsung-email":{"android":{"2023-09":"y"}},"sfr":{"desktop-webmail":{"2023-09":"u"},"ios":{"2023-09":"u"},"android":{"2023-09":"u"}},"thunderbird":{"macos":{"2023-09":"y"}},"aol":{"desktop-webmail":{"2023-09":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"}},"yahoo":{"desktop-webmail":{"2023-09":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"}},"protonmail":{"desktop-webmail":{"2023-09":"u"},"ios":{"2023-09":"u"},"android":{"2023-09":"u"}},"hey":{"desktop-webmail":{"2023-09":"u"}},"mail-ru":{"desktop-webmail":{"2023-09":"n"}},"fastmail":{"desktop-webmail":{"2023-09":"u"}},"laposte":{"desktop-webmail":{"2023-09":"u"}},"free-fr":{"desktop-webmail":{"2023-09":"u"}},"t-online-de":{"desktop-webmail":{"2023-09":"n"}},"gmx":{"desktop-webmail":{"2023-09":"y"}},"web-de":{"desktop-webmail":{"2023-09":"y"}}},
|
||||
"stats":{"apple-mail":{"macos":{"2023-09":"y"},"ios":{"2023-09":"y"}},"gmail":{"desktop-webmail":{"2023-09":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"},"mobile-webmail":{"2023-09":"y"}},"orange":{"desktop-webmail":{"2023-09":"u"},"ios":{"2023-09":"u"},"android":{"2023-09":"u"}},"outlook":{"windows":{"2013":"a #1","2016":"a #1","2019":"a #1","2021":"a #1"},"windows-mail":{"2023-09":"a #1"},"macos":{"16.56":"y","16.80":"y"},"outlook-com":{"2023-09":"y","2024-01":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"}},"samsung-email":{"android":{"2023-09":"y"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"2023-09":"y"}},"aol":{"desktop-webmail":{"2023-09":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"}},"yahoo":{"desktop-webmail":{"2023-09":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"}},"protonmail":{"desktop-webmail":{"2023-09":"u"},"ios":{"2023-09":"u"},"android":{"2023-09":"u"}},"hey":{"desktop-webmail":{"2023-09":"u"}},"mail-ru":{"desktop-webmail":{"2023-09":"n"}},"fastmail":{"desktop-webmail":{"2023-09":"u"}},"laposte":{"desktop-webmail":{"2023-09":"u"}},"free-fr":{"desktop-webmail":{"2023-09":"u"}},"t-online-de":{"desktop-webmail":{"2023-09":"n"}},"gmx":{"desktop-webmail":{"2023-09":"y"}},"web-de":{"desktop-webmail":{"2023-09":"y"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":{"1":"Partial. The `title` attribute is removed."}
|
||||
},
|
||||
@ -3486,7 +3614,7 @@
|
||||
"last_test_date":"2024-01-26",
|
||||
"test_url":"https://www.caniemail.com/tests/html-hidden-attribute.html",
|
||||
"test_results_url":"https://testi.at/proj/rlpli9r9trvs62rt7p",
|
||||
"stats":{"apple-mail":{"macos":{"20":"y","21":"y","22":"y","23":"y"},"ios":{"14":"y","15":"y","16":"y","17":"y"}},"gmail":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"n"},"android":{"2024-01":"n"},"mobile-webmail":{"2024-01":"n"}},"orange":{"desktop-webmail":{"2024-01":"u"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"outlook":{"windows":{"2003":"n","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2024-01":"n"},"macos":{"2016":"y","2024-01":"n"},"outlook-com":{"2024-01":"n"},"ios":{"2024-01":"n"},"android":{"2024-01":"n"}},"samsung-email":{"android":{"6.1":"y"}},"sfr":{"desktop-webmail":{"2024-01":"u"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"thunderbird":{"macos":{"60.3":"y"}},"aol":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"yahoo":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"n"},"android":{"2024-01":"n"}},"protonmail":{"desktop-webmail":{"2024-01":"u"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"hey":{"desktop-webmail":{"2024-01":"u"}},"mail-ru":{"desktop-webmail":{"2024-01":"a #1"}},"fastmail":{"desktop-webmail":{"2024-01":"u"}},"laposte":{"desktop-webmail":{"2024-01":"u"}},"gmx":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"web-de":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"ionos-1and1":{"desktop-webmail":{"2024-01":"u"},"android":{"2024-01":"u"}}},
|
||||
"stats":{"apple-mail":{"macos":{"20":"y","21":"y","22":"y","23":"y"},"ios":{"14":"y","15":"y","16":"y","17":"y"}},"gmail":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"n"},"android":{"2024-01":"n"},"mobile-webmail":{"2024-01":"n"}},"orange":{"desktop-webmail":{"2024-01":"u"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"outlook":{"windows":{"2003":"n","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2024-01":"n"},"macos":{"2016":"y","2024-01":"n"},"outlook-com":{"2024-01":"n"},"ios":{"2024-01":"n"},"android":{"2024-01":"n"}},"samsung-email":{"android":{"6.1":"y"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"60.3":"y"}},"aol":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"yahoo":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"n"},"android":{"2024-01":"n"}},"protonmail":{"desktop-webmail":{"2024-01":"u"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"hey":{"desktop-webmail":{"2024-01":"u"}},"mail-ru":{"desktop-webmail":{"2024-01":"a #1"}},"fastmail":{"desktop-webmail":{"2024-01":"u"}},"laposte":{"desktop-webmail":{"2024-01":"u"}},"gmx":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"web-de":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"ionos-1and1":{"desktop-webmail":{"2024-01":"u"},"android":{"2024-01":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":{"1":"Does not support the unquoted attribute value syntax `<div hidden></div>`"}
|
||||
},
|
||||
@ -3742,7 +3870,7 @@
|
||||
"last_test_date":"2023-09-18",
|
||||
"test_url":"https://www.caniemail.com/tests/html-meta-color-scheme.html",
|
||||
"test_results_url":"",
|
||||
"stats":{"apple-mail":{"macos":{"16":"y"},"ios":{"12.4":"n"}},"gmail":{"desktop-webmail":{"2023-09":"n"},"ios":{"2019-09":"n"},"android":{"2023-09":"n"},"mobile-webmail":{"2023-09":"n"}},"orange":{"desktop-webmail":{"2021-03":"u"},"ios":{"2020-01":"u"},"android":{"2020-01":"u"}},"outlook":{"windows":{"2003":"u","2007":"u","2010":"u","2013":"u","2016":"n","2019":"n"},"windows-mail":{"2023-09":"n"},"macos":{"2023":"n","16.80":"n"},"outlook-com":{"2023-09":"n","2024-01":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"n"}},"samsung-email":{"android":{"9.1":"n"}},"sfr":{"desktop-webmail":{"2020-01":"u"},"ios":{"2020-01":"u"},"android":{"2020-01":"u"}},"thunderbird":{"macos":{"102.11":"y"}},"yahoo":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"n"}},"aol":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"n"}},"protonmail":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"n"}},"hey":{"desktop-webmail":{"2023-09":"n"}},"mail-ru":{"desktop-webmail":{"2023-09":"n"}},"fastmail":{"desktop-webmail":{"2023-09":"n"}},"laposte":{"desktop-webmail":{"2021-08":"u"}},"gmx":{"desktop-webmail":{"2023-09":"n"},"ios":{"2022-11":"u"},"android":{"2022-11":"u"}},"web-de":{"desktop-webmail":{"2023-09":"n"},"ios":{"2022-11":"u"},"android":{"2022-11":"u"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"u"},"android":{"2022-11":"u"}}},
|
||||
"stats":{"apple-mail":{"macos":{"16":"y"},"ios":{"12.4":"n"}},"gmail":{"desktop-webmail":{"2023-09":"n"},"ios":{"2019-09":"n"},"android":{"2023-09":"n"},"mobile-webmail":{"2023-09":"n"}},"orange":{"desktop-webmail":{"2021-03":"u"},"ios":{"2020-01":"u"},"android":{"2020-01":"u"}},"outlook":{"windows":{"2003":"u","2007":"u","2010":"u","2013":"u","2016":"n","2019":"n"},"windows-mail":{"2023-09":"n"},"macos":{"2023":"n","16.80":"n"},"outlook-com":{"2023-09":"n","2024-01":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"n"}},"samsung-email":{"android":{"9.1":"n"}},"sfr":{"desktop-webmail":{"2024-03":"n"},"ios":{"2024-03":"n"},"android":{"2024-03":"n"}},"thunderbird":{"macos":{"102.11":"y"}},"yahoo":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"n"}},"aol":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"n"}},"protonmail":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"n"}},"hey":{"desktop-webmail":{"2023-09":"n"}},"mail-ru":{"desktop-webmail":{"2023-09":"n"}},"fastmail":{"desktop-webmail":{"2023-09":"n"}},"laposte":{"desktop-webmail":{"2021-08":"u"}},"gmx":{"desktop-webmail":{"2023-09":"n"},"ios":{"2022-11":"u"},"android":{"2022-11":"u"}},"web-de":{"desktop-webmail":{"2023-09":"n"},"ios":{"2022-11":"u"},"android":{"2022-11":"u"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"u"},"android":{"2022-11":"u"}}},
|
||||
"notes":"",
|
||||
"notes_by_num":null
|
||||
},
|
||||
@ -3822,7 +3950,7 @@
|
||||
"last_test_date":"2023-09-25",
|
||||
"test_url":"https://www.caniemail.com/tests/html-popover.html",
|
||||
"test_results_url":"",
|
||||
"stats":{"apple-mail":{"macos":{"16":"y"},"ios":{"16":"y"}},"gmail":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"u"},"mobile-webmail":{"2023-09":"u"}},"orange":{"desktop-webmail":{"2019-08":"u","2021-03":"u"},"ios":{"2019-08":"u"},"android":{"2019-08":"u"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2023-09":"n"},"macos":{"2023-09":"n","16.80":"n"},"outlook-com":{"2023-09":"n","2024-01":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"u"}},"yahoo":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2019-02":"u"}},"aol":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"u"}},"samsung-email":{"android":{"5.0.10.2":"u","6.1.90.16":"a #1"}},"sfr":{"desktop-webmail":{"2019-08":"u"},"ios":{"2019-08":"u"},"android":{"2019-08":"u"}},"thunderbird":{"macos":{"115.2":"n"}},"protonmail":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"u"}},"hey":{"desktop-webmail":{"2023-09":"n"}},"mail-ru":{"desktop-webmail":{"2024-01":"a"}},"fastmail":{"desktop-webmail":{"2023-09":"n"}},"laposte":{"desktop-webmail":{"2021-08":"u"}},"gmx":{"desktop-webmail":{"2022-06":"u"},"ios":{"2022-06":"u"},"android":{"2022-06":"u"}},"web-de":{"desktop-webmail":{"2022-06":"u"},"ios":{"2022-06":"u"},"android":{"2022-06":"u"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"u"},"android":{"2022-06":"u"}}},
|
||||
"stats":{"apple-mail":{"macos":{"16":"y"},"ios":{"16":"y"}},"gmail":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2024-03":"n"},"mobile-webmail":{"2024-03":"n"}},"orange":{"desktop-webmail":{"2019-08":"u","2021-03":"u"},"ios":{"2019-08":"u"},"android":{"2019-08":"u"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2023-09":"n"},"macos":{"2023-09":"n","16.80":"n"},"outlook-com":{"2023-09":"n","2024-01":"n"},"ios":{"2023-09":"n"},"android":{"2024-03":"n"}},"yahoo":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2024-03":"n"}},"aol":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"u"}},"samsung-email":{"android":{"5.0.10.2":"u","6.1.90.16":"a #1"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"n"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"115.2":"n"}},"protonmail":{"desktop-webmail":{"2023-09":"n"},"ios":{"2023-09":"n"},"android":{"2023-09":"u"}},"hey":{"desktop-webmail":{"2023-09":"n"}},"mail-ru":{"desktop-webmail":{"2024-01":"a"}},"fastmail":{"desktop-webmail":{"2023-09":"n"}},"laposte":{"desktop-webmail":{"2021-08":"u"}},"gmx":{"desktop-webmail":{"2022-06":"u"},"ios":{"2022-06":"u"},"android":{"2022-06":"u"}},"web-de":{"desktop-webmail":{"2022-06":"u"},"ios":{"2022-06":"u"},"android":{"2022-06":"u"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"u"},"android":{"2022-06":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":{"1":"Partial. Not supported with Hotmail/Outlook accounts.","2":"Partial. The `popovertarget` attribute is supported, but the `popover` attribute and the `dialog` element are not."}
|
||||
},
|
||||
|
@ -2,7 +2,6 @@
|
||||
package stats
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
@ -21,10 +20,10 @@ var (
|
||||
|
||||
mu sync.RWMutex
|
||||
|
||||
smtpAccepted int
|
||||
smtpAcceptedSize int
|
||||
smtpRejected int
|
||||
smtpIgnored int
|
||||
smtpAccepted float64
|
||||
smtpAcceptedSize float64
|
||||
smtpRejected float64
|
||||
smtpIgnored float64
|
||||
)
|
||||
|
||||
// AppInformation struct
|
||||
@ -37,29 +36,29 @@ type AppInformation struct {
|
||||
// Database path
|
||||
Database string
|
||||
// Database size in bytes
|
||||
DatabaseSize int64
|
||||
DatabaseSize float64
|
||||
// Total number of messages in the database
|
||||
Messages int
|
||||
Messages float64
|
||||
// Total number of messages in the database
|
||||
Unread int
|
||||
Unread float64
|
||||
// Tags and message totals per tag
|
||||
Tags map[string]int64
|
||||
// Runtime statistics
|
||||
RuntimeStats struct {
|
||||
// Mailpit server uptime in seconds
|
||||
Uptime int
|
||||
Uptime float64
|
||||
// Current memory usage in bytes
|
||||
Memory uint64
|
||||
// Database runtime messages deleted
|
||||
MessagesDeleted int
|
||||
MessagesDeleted float64
|
||||
// Accepted runtime SMTP messages
|
||||
SMTPAccepted int
|
||||
SMTPAccepted float64
|
||||
// Total runtime accepted messages size in bytes
|
||||
SMTPAcceptedSize int
|
||||
SMTPAcceptedSize float64
|
||||
// Rejected runtime SMTP messages
|
||||
SMTPRejected int
|
||||
SMTPRejected float64
|
||||
// Ignored runtime SMTP messages (when using --ignore-duplicate-ids)
|
||||
SMTPIgnored int
|
||||
SMTPIgnored float64
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,8 +71,7 @@ func Load() AppInformation {
|
||||
runtime.ReadMemStats(&m)
|
||||
|
||||
info.RuntimeStats.Memory = m.Sys - m.HeapReleased
|
||||
|
||||
info.RuntimeStats.Uptime = int(time.Since(startedAt).Seconds())
|
||||
info.RuntimeStats.Uptime = time.Since(startedAt).Seconds()
|
||||
info.RuntimeStats.MessagesDeleted = storage.StatsDeleted
|
||||
info.RuntimeStats.SMTPAccepted = smtpAccepted
|
||||
info.RuntimeStats.SMTPAcceptedSize = smtpAcceptedSize
|
||||
@ -96,16 +94,10 @@ func Load() AppInformation {
|
||||
}
|
||||
}
|
||||
|
||||
info.Database = config.DataFile
|
||||
|
||||
db, err := os.Stat(info.Database)
|
||||
if err == nil {
|
||||
info.DatabaseSize = db.Size()
|
||||
}
|
||||
|
||||
info.Database = config.Database
|
||||
info.DatabaseSize = storage.DbSize()
|
||||
info.Messages = storage.CountTotal()
|
||||
info.Unread = storage.CountUnread()
|
||||
|
||||
info.Tags = storage.GetAllTagsCount()
|
||||
|
||||
return info
|
||||
@ -120,7 +112,7 @@ func Track() {
|
||||
func LogSMTPAccepted(size int) {
|
||||
mu.Lock()
|
||||
smtpAccepted = smtpAccepted + 1
|
||||
smtpAcceptedSize = smtpAcceptedSize + size
|
||||
smtpAcceptedSize = smtpAcceptedSize + float64(size)
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ func dbCron() {
|
||||
|
||||
if deletedSize > 0 {
|
||||
total := totalMessagesSize()
|
||||
var deletedPercent int64
|
||||
var deletedPercent float64
|
||||
if total == 0 {
|
||||
deletedPercent = 100
|
||||
} else {
|
||||
@ -35,7 +35,7 @@ func dbCron() {
|
||||
}
|
||||
// only vacuum the DB if at least 1% of mail storage size has been deleted
|
||||
if deletedPercent >= 1 {
|
||||
logger.Log().Debugf("[db] deleted messages is %d%% of total size, reclaim space", deletedPercent)
|
||||
logger.Log().Debugf("[db] deleted messages is %f%% of total size, reclaim space", deletedPercent)
|
||||
vacuumDb()
|
||||
}
|
||||
}
|
||||
@ -55,15 +55,15 @@ func pruneMessages() {
|
||||
start := time.Now()
|
||||
|
||||
q := sqlf.Select("ID, Size").
|
||||
From("mailbox").
|
||||
From(tenant("mailbox")).
|
||||
OrderBy("Created DESC").
|
||||
Limit(5000).
|
||||
Offset(config.MaxMessages)
|
||||
|
||||
ids := []string{}
|
||||
var prunedSize int64
|
||||
var size int
|
||||
if err := q.Query(context.TODO(), db, func(row *sql.Rows) {
|
||||
var size float64
|
||||
if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
|
||||
var id string
|
||||
|
||||
if err := row.Scan(&id, &size); err != nil {
|
||||
@ -93,19 +93,19 @@ func pruneMessages() {
|
||||
args[i] = id
|
||||
}
|
||||
|
||||
_, err = tx.Query(`DELETE FROM mailbox WHERE ID IN (?`+strings.Repeat(",?", len(ids)-1)+`)`, args...) // #nosec
|
||||
_, err = tx.Exec(`DELETE FROM `+tenant("mailbox_data")+` WHERE ID IN (?`+strings.Repeat(",?", len(ids)-1)+`)`, args...) // #nosec
|
||||
if err != nil {
|
||||
logger.Log().Errorf("[db] %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_, err = tx.Query(`DELETE FROM mailbox_data WHERE ID IN (?`+strings.Repeat(",?", len(ids)-1)+`)`, args...) // #nosec
|
||||
_, err = tx.Exec(`DELETE FROM `+tenant("message_tags")+` WHERE ID IN (?`+strings.Repeat(",?", len(ids)-1)+`)`, args...) // #nosec
|
||||
if err != nil {
|
||||
logger.Log().Errorf("[db] %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_, err = tx.Query(`DELETE FROM message_tags WHERE ID IN (?`+strings.Repeat(",?", len(ids)-1)+`)`, args...) // #nosec
|
||||
_, err = tx.Exec(`DELETE FROM `+tenant("mailbox")+` WHERE ID IN (?`+strings.Repeat(",?", len(ids)-1)+`)`, args...) // #nosec
|
||||
if err != nil {
|
||||
logger.Log().Errorf("[db] %s", err.Error())
|
||||
return
|
||||
@ -137,6 +137,11 @@ func pruneMessages() {
|
||||
|
||||
// Vacuum the database to reclaim space from deleted messages
|
||||
func vacuumDb() {
|
||||
if sqlDriver == "rqlite" {
|
||||
// let rqlite handle vacuuming
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
// set WAL file checkpoint
|
||||
@ -147,7 +152,7 @@ func vacuumDb() {
|
||||
|
||||
// vacuum database
|
||||
if _, err := db.Exec("VACUUM"); err != nil {
|
||||
logger.Log().Errorf("[db] %s", err.Error())
|
||||
logger.Log().Errorf("[db] VACUUM: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@ -162,5 +167,5 @@ func vacuumDb() {
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
logger.Log().Debugf("[db] vacuumed database in %s", elapsed)
|
||||
logger.Log().Debugf("[db] vacuum completed in %s", elapsed)
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@ -17,14 +18,18 @@ import (
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/leporo/sqlf"
|
||||
|
||||
// sqlite (native) - https://gitlab.com/cznic/sqlite
|
||||
// sqlite - https://gitlab.com/cznic/sqlite
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
// rqlite - https://github.com/rqlite/gorqlite | https://rqlite.io/
|
||||
_ "github.com/rqlite/gorqlite/stdlib"
|
||||
)
|
||||
|
||||
var (
|
||||
db *sql.DB
|
||||
dbFile string
|
||||
dbIsTemp bool
|
||||
sqlDriver string
|
||||
dbLastAction time.Time
|
||||
|
||||
// zstd compression encoder & decoder
|
||||
@ -34,43 +39,61 @@ var (
|
||||
|
||||
// InitDB will initialise the database
|
||||
func InitDB() error {
|
||||
p := config.DataFile
|
||||
p := config.Database
|
||||
var dsn string
|
||||
|
||||
if p == "" {
|
||||
// when no path is provided then we create a temporary file
|
||||
// which will get deleted on Close(), SIGINT or SIGTERM
|
||||
p = fmt.Sprintf("%s-%d.db", path.Join(os.TempDir(), "mailpit"), time.Now().UnixNano())
|
||||
dbIsTemp = true
|
||||
sqlDriver = "sqlite"
|
||||
dsn = p
|
||||
logger.Log().Debugf("[db] using temporary database: %s", p)
|
||||
} else if strings.HasPrefix(p, "http://") || strings.HasPrefix(p, "https://") {
|
||||
sqlDriver = "rqlite"
|
||||
dsn = p
|
||||
logger.Log().Debugf("[db] opening rqlite database %s", p)
|
||||
} else {
|
||||
p = filepath.Clean(p)
|
||||
sqlDriver = "sqlite"
|
||||
dsn = fmt.Sprintf("file:%s?cache=shared", p)
|
||||
logger.Log().Debugf("[db] opening database %s", p)
|
||||
}
|
||||
|
||||
config.DataFile = p
|
||||
|
||||
logger.Log().Debugf("[db] opening database %s", p)
|
||||
config.Database = p
|
||||
|
||||
var err error
|
||||
|
||||
dsn := fmt.Sprintf("file:%s?cache=shared", p)
|
||||
|
||||
db, err = sql.Open("sqlite", dsn)
|
||||
db, err = sql.Open(sqlDriver, dsn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 1; i < 6; i++ {
|
||||
if err := Ping(); err != nil {
|
||||
logger.Log().Errorf("[db] %s", err.Error())
|
||||
logger.Log().Infof("[db] reconnecting in 5 seconds (%d/5)", i)
|
||||
time.Sleep(5 * time.Second)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// prevent "database locked" errors
|
||||
// @see https://github.com/mattn/go-sqlite3#faq
|
||||
db.SetMaxOpenConns(1)
|
||||
|
||||
if sqlDriver == "sqlite" {
|
||||
// SQLite performance tuning (https://phiresky.github.io/blog/2020/sqlite-performance-tuning/)
|
||||
_, err = db.Exec("PRAGMA journal_mode = WAL; PRAGMA synchronous = normal;")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create tables if necessary & apply migrations
|
||||
if err := dbApplyMigrations(); err != nil {
|
||||
if err := dbApplySchemas(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -99,6 +122,11 @@ func InitDB() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tenant applies an optional prefix to the table name
|
||||
func tenant(table string) string {
|
||||
return fmt.Sprintf("%s%s", config.TenantID, table)
|
||||
}
|
||||
|
||||
// Close will close the database, and delete if a temporary table
|
||||
func Close() {
|
||||
if db != nil {
|
||||
@ -138,10 +166,10 @@ func StatsGet() MailboxStats {
|
||||
}
|
||||
|
||||
// CountTotal returns the number of emails in the database
|
||||
func CountTotal() int {
|
||||
var total int
|
||||
func CountTotal() float64 {
|
||||
var total float64
|
||||
|
||||
_ = sqlf.From("mailbox").
|
||||
_ = sqlf.From(tenant("mailbox")).
|
||||
Select("COUNT(*)").To(&total).
|
||||
QueryRowAndClose(context.TODO(), db)
|
||||
|
||||
@ -149,10 +177,10 @@ func CountTotal() int {
|
||||
}
|
||||
|
||||
// CountUnread returns the number of emails in the database that are unread.
|
||||
func CountUnread() int {
|
||||
var total int
|
||||
func CountUnread() float64 {
|
||||
var total float64
|
||||
|
||||
_ = sqlf.From("mailbox").
|
||||
_ = sqlf.From(tenant("mailbox")).
|
||||
Select("COUNT(*)").To(&total).
|
||||
Where("Read = ?", 0).
|
||||
QueryRowAndClose(context.TODO(), db)
|
||||
@ -161,10 +189,10 @@ func CountUnread() int {
|
||||
}
|
||||
|
||||
// CountRead returns the number of emails in the database that are read.
|
||||
func CountRead() int {
|
||||
var total int
|
||||
func CountRead() float64 {
|
||||
var total float64
|
||||
|
||||
_ = sqlf.From("mailbox").
|
||||
_ = sqlf.From(tenant("mailbox")).
|
||||
Select("COUNT(*)").To(&total).
|
||||
Where("Read = ?", 1).
|
||||
QueryRowAndClose(context.TODO(), db)
|
||||
@ -172,11 +200,25 @@ func CountRead() int {
|
||||
return total
|
||||
}
|
||||
|
||||
// DbSize returns the size of the SQLite database.
|
||||
func DbSize() float64 {
|
||||
var total sql.NullFloat64
|
||||
|
||||
err := db.QueryRow("SELECT page_count * page_size AS size FROM pragma_page_count(), pragma_page_size()").Scan(&total)
|
||||
|
||||
if err != nil {
|
||||
logger.Log().Errorf("[db] %s", err.Error())
|
||||
return total.Float64
|
||||
}
|
||||
|
||||
return total.Float64
|
||||
}
|
||||
|
||||
// IsUnread returns whether a message is unread or not.
|
||||
func IsUnread(id string) bool {
|
||||
var unread int
|
||||
|
||||
_ = sqlf.From("mailbox").
|
||||
_ = sqlf.From(tenant("mailbox")).
|
||||
Select("COUNT(*)").To(&unread).
|
||||
Where("Read = ?", 0).
|
||||
Where("ID = ?", id).
|
||||
@ -189,7 +231,7 @@ func IsUnread(id string) bool {
|
||||
func MessageIDExists(id string) bool {
|
||||
var total int
|
||||
|
||||
_ = sqlf.From("mailbox").
|
||||
_ = sqlf.From(tenant("mailbox")).
|
||||
Select("COUNT(*)").To(&total).
|
||||
Where("MessageID = ?", id).
|
||||
QueryRowAndClose(context.TODO(), db)
|
||||
|
@ -4,6 +4,8 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -88,21 +90,27 @@ func Store(body *[]byte) (string, error) {
|
||||
defer tx.Rollback()
|
||||
|
||||
subject := env.GetHeader("Subject")
|
||||
size := len(*body)
|
||||
size := float64(len(*body))
|
||||
inline := len(env.Inlines)
|
||||
attachments := len(env.Attachments)
|
||||
snippet := tools.CreateSnippet(env.Text, env.HTML)
|
||||
|
||||
sql := fmt.Sprintf(`INSERT INTO %s
|
||||
(Created, ID, MessageID, Subject, Metadata, Size, Inline, Attachments, SearchText, Read, Snippet)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,0,?)`,
|
||||
tenant("mailbox"),
|
||||
) // #nosec
|
||||
|
||||
// insert mail summary data
|
||||
_, err = tx.Exec("INSERT INTO mailbox(Created, ID, MessageID, Subject, Metadata, Size, Inline, Attachments, SearchText, Read, Snippet) values(?,?,?,?,?,?,?,?,?,0,?)",
|
||||
created.UnixMilli(), id, messageID, subject, string(summaryJSON), size, inline, attachments, searchText, snippet)
|
||||
_, err = tx.Exec(sql, created.UnixMilli(), id, messageID, subject, string(summaryJSON), size, inline, attachments, searchText, snippet)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// insert compressed raw message
|
||||
compressed := dbEncoder.EncodeAll(*body, make([]byte, 0, size))
|
||||
_, err = tx.Exec("INSERT INTO mailbox_data(ID, Email) values(?,?)", id, string(compressed))
|
||||
encoded := dbEncoder.EncodeAll(*body, make([]byte, 0, int(size)))
|
||||
hexStr := hex.EncodeToString(encoded)
|
||||
_, err = tx.Exec(fmt.Sprintf(`INSERT INTO %s (ID, Email) VALUES(?, x'%s')`, tenant("mailbox_data"), hexStr), id) // #nosec
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -148,19 +156,19 @@ func List(start, limit int) ([]MessageSummary, error) {
|
||||
results := []MessageSummary{}
|
||||
tsStart := time.Now()
|
||||
|
||||
q := sqlf.From("mailbox m").
|
||||
q := sqlf.From(tenant("mailbox") + " m").
|
||||
Select(`m.Created, m.ID, m.MessageID, m.Subject, m.Metadata, m.Size, m.Attachments, m.Read, m.Snippet`).
|
||||
OrderBy("m.Created DESC").
|
||||
Limit(limit).
|
||||
Offset(start)
|
||||
|
||||
if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
|
||||
var created int64
|
||||
var created float64
|
||||
var id string
|
||||
var messageID string
|
||||
var subject string
|
||||
var metadata string
|
||||
var size int
|
||||
var size float64
|
||||
var attachments int
|
||||
var read int
|
||||
var snippet string
|
||||
@ -176,7 +184,7 @@ func List(start, limit int) ([]MessageSummary, error) {
|
||||
return
|
||||
}
|
||||
|
||||
em.Created = time.UnixMilli(created)
|
||||
em.Created = time.UnixMilli(int64(created))
|
||||
em.ID = id
|
||||
em.MessageID = messageID
|
||||
em.Subject = subject
|
||||
@ -241,12 +249,12 @@ func GetMessage(id string) (*Message, error) {
|
||||
date, err := env.Date()
|
||||
if err != nil {
|
||||
// return received datetime when message does not contain a date header
|
||||
q := sqlf.From("mailbox").
|
||||
q := sqlf.From(tenant("mailbox")).
|
||||
Select(`Created`).
|
||||
Where(`ID = ?`, id)
|
||||
|
||||
if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
|
||||
var created int64
|
||||
var created float64
|
||||
|
||||
if err := row.Scan(&created); err != nil {
|
||||
logger.Log().Errorf("[db] %s", err.Error())
|
||||
@ -255,7 +263,7 @@ func GetMessage(id string) (*Message, error) {
|
||||
|
||||
logger.Log().Debugf("[db] %s does not contain a date header, using received datetime", id)
|
||||
|
||||
date = time.UnixMilli(created)
|
||||
date = time.UnixMilli(int64(created))
|
||||
}); err != nil {
|
||||
logger.Log().Errorf("[db] %s", err.Error())
|
||||
}
|
||||
@ -273,7 +281,7 @@ func GetMessage(id string) (*Message, error) {
|
||||
ReturnPath: returnPath,
|
||||
Subject: env.GetHeader("Subject"),
|
||||
Tags: getMessageTags(id),
|
||||
Size: len(raw),
|
||||
Size: float64(len(raw)),
|
||||
Text: env.Text,
|
||||
}
|
||||
|
||||
@ -327,11 +335,10 @@ func GetMessage(id string) (*Message, error) {
|
||||
func GetMessageRaw(id string) ([]byte, error) {
|
||||
var i string
|
||||
var msg string
|
||||
q := sqlf.From("mailbox_data").
|
||||
q := sqlf.From(tenant("mailbox_data")).
|
||||
Select(`ID`).To(&i).
|
||||
Select(`Email`).To(&msg).
|
||||
Where(`ID = ?`, id)
|
||||
|
||||
err := q.QueryRowAndClose(context.Background(), db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -341,7 +348,17 @@ func GetMessageRaw(id string) ([]byte, error) {
|
||||
return nil, errors.New("message not found")
|
||||
}
|
||||
|
||||
raw, err := dbDecoder.DecodeAll([]byte(msg), nil)
|
||||
var data []byte
|
||||
if sqlDriver == "rqlite" {
|
||||
data, err = base64.StdEncoding.DecodeString(msg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding base64 message: %w", err)
|
||||
}
|
||||
} else {
|
||||
data = []byte(msg)
|
||||
}
|
||||
|
||||
raw, err := dbDecoder.DecodeAll(data, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decompressing message: %s", err.Error())
|
||||
}
|
||||
@ -398,7 +415,7 @@ func LatestID(r *http.Request) (string, error) {
|
||||
|
||||
search := strings.TrimSpace(r.URL.Query().Get("query"))
|
||||
if search != "" {
|
||||
messages, _, err = Search(search, 0, 1)
|
||||
messages, _, err = Search(search, r.URL.Query().Get("tz"), 0, 1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -421,7 +438,7 @@ func MarkRead(id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := sqlf.Update("mailbox").
|
||||
_, err := sqlf.Update(tenant("mailbox")).
|
||||
Set("Read", 1).
|
||||
Where("ID = ?", id).
|
||||
ExecAndClose(context.Background(), db)
|
||||
@ -442,7 +459,7 @@ func MarkAllRead() error {
|
||||
total = CountUnread()
|
||||
)
|
||||
|
||||
_, err := sqlf.Update("mailbox").
|
||||
_, err := sqlf.Update(tenant("mailbox")).
|
||||
Set("Read", 1).
|
||||
Where("Read = ?", 0).
|
||||
ExecAndClose(context.Background(), db)
|
||||
@ -451,7 +468,7 @@ func MarkAllRead() error {
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
logger.Log().Debugf("[db] marked %d messages as read in %s", total, elapsed)
|
||||
logger.Log().Debugf("[db] marked %v messages as read in %s", total, elapsed)
|
||||
|
||||
BroadcastMailboxStats()
|
||||
|
||||
@ -467,7 +484,7 @@ func MarkAllUnread() error {
|
||||
total = CountRead()
|
||||
)
|
||||
|
||||
_, err := sqlf.Update("mailbox").
|
||||
_, err := sqlf.Update(tenant("mailbox")).
|
||||
Set("Read", 0).
|
||||
Where("Read = ?", 1).
|
||||
ExecAndClose(context.Background(), db)
|
||||
@ -476,7 +493,7 @@ func MarkAllUnread() error {
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
logger.Log().Debugf("[db] marked %d messages as unread in %s", total, elapsed)
|
||||
logger.Log().Debugf("[db] marked %v messages as unread in %s", total, elapsed)
|
||||
|
||||
BroadcastMailboxStats()
|
||||
|
||||
@ -491,7 +508,7 @@ func MarkUnread(id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := sqlf.Update("mailbox").
|
||||
_, err := sqlf.Update(tenant("mailbox")).
|
||||
Set("Read", 0).
|
||||
Where("ID = ?", id).
|
||||
ExecAndClose(context.Background(), db)
|
||||
@ -507,52 +524,89 @@ func MarkUnread(id string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteOneMessage will delete a single message from a mailbox
|
||||
func DeleteOneMessage(id string) error {
|
||||
m, err := GetMessageRaw(id)
|
||||
// DeleteMessages deletes one or more messages in bulk
|
||||
func DeleteMessages(ids []string) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
args := make([]interface{}, len(ids))
|
||||
for i, id := range ids {
|
||||
args[i] = id
|
||||
}
|
||||
|
||||
sql := fmt.Sprintf(`SELECT ID, Size FROM %s WHERE ID IN (?%s)`, tenant("mailbox"), strings.Repeat(",?", len(args)-1)) // #nosec
|
||||
rows, err := db.Query(sql, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
toDelete := []string{}
|
||||
var totalSize float64
|
||||
|
||||
for rows.Next() {
|
||||
var id string
|
||||
var size float64
|
||||
if err := rows.Scan(&id, &size); err != nil {
|
||||
return err
|
||||
}
|
||||
toDelete = append(toDelete, id)
|
||||
totalSize = totalSize + size
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(toDelete) == 0 {
|
||||
return nil // nothing to delete
|
||||
}
|
||||
|
||||
size := len(m)
|
||||
// begin a transaction to ensure both the message
|
||||
// and data are deleted successfully
|
||||
tx, err := db.BeginTx(context.Background(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// roll back if it fails
|
||||
defer tx.Rollback()
|
||||
args = make([]interface{}, len(toDelete))
|
||||
for i, id := range toDelete {
|
||||
args[i] = id
|
||||
}
|
||||
|
||||
_, err = tx.Exec("DELETE FROM mailbox WHERE ID = ?", id)
|
||||
tables := []string{"mailbox", "mailbox_data", "message_tags"}
|
||||
|
||||
for _, t := range tables {
|
||||
sql = fmt.Sprintf(`DELETE FROM %s WHERE ID IN (?%s)`, tenant(t), strings.Repeat(",?", len(ids)-1))
|
||||
|
||||
_, err = tx.Exec(sql, args...) // #nosec
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("DELETE FROM mailbox_data WHERE ID = ?", id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
|
||||
if err == nil {
|
||||
logger.Log().Debugf("[db] deleted message %s", id)
|
||||
}
|
||||
|
||||
if err := DeleteAllMessageTags(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbLastAction = time.Now()
|
||||
addDeletedSize(int64(size))
|
||||
addDeletedSize(int64(totalSize))
|
||||
|
||||
logMessagesDeleted(1)
|
||||
logMessagesDeleted(len(toDelete))
|
||||
|
||||
_ = pruneUnusedTags()
|
||||
|
||||
elapsed := time.Since(start)
|
||||
|
||||
messages := "messages"
|
||||
if len(toDelete) == 1 {
|
||||
messages = "message"
|
||||
}
|
||||
|
||||
logger.Log().Debugf("[db] deleted %d %s in %s", len(toDelete), messages, elapsed)
|
||||
|
||||
BroadcastMailboxStats()
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAllMessages will delete all messages from a mailbox
|
||||
@ -562,7 +616,7 @@ func DeleteAllMessages() error {
|
||||
total int
|
||||
)
|
||||
|
||||
_ = sqlf.From("mailbox").
|
||||
_ = sqlf.From(tenant("mailbox")).
|
||||
Select("COUNT(*)").To(&total).
|
||||
QueryRowAndClose(context.TODO(), db)
|
||||
|
||||
@ -576,24 +630,14 @@ func DeleteAllMessages() error {
|
||||
// roll back if it fails
|
||||
defer tx.Rollback()
|
||||
|
||||
_, err = tx.Exec("DELETE FROM mailbox")
|
||||
tables := []string{"mailbox", "mailbox_data", "tags", "message_tags"}
|
||||
|
||||
for _, t := range tables {
|
||||
sql := fmt.Sprintf(`DELETE FROM %s`, tenant(t)) // #nosec
|
||||
_, err := tx.Exec(sql)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("DELETE FROM mailbox_data")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("DELETE FROM tags")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("DELETE FROM message_tags")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
|
@ -13,8 +13,6 @@ func TestTextEmailInserts(t *testing.T) {
|
||||
|
||||
start := time.Now()
|
||||
|
||||
assertEqualStats(t, 0, 0)
|
||||
|
||||
for i := 0; i < testRuns; i++ {
|
||||
if _, err := Store(&testTextEmail); err != nil {
|
||||
t.Log("error ", err)
|
||||
@ -22,19 +20,17 @@ func TestTextEmailInserts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
assertEqual(t, CountTotal(), testRuns, "Incorrect number of text emails stored")
|
||||
assertEqual(t, CountTotal(), float64(testRuns), "Incorrect number of text emails stored")
|
||||
|
||||
t.Logf("Inserted %d text emails in %s", testRuns, time.Since(start))
|
||||
|
||||
assertEqualStats(t, testRuns, testRuns)
|
||||
|
||||
delStart := time.Now()
|
||||
if err := DeleteAllMessages(); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
assertEqual(t, CountTotal(), 0, "incorrect number of text emails deleted")
|
||||
assertEqual(t, CountTotal(), float64(0), "incorrect number of text emails deleted")
|
||||
|
||||
t.Logf("deleted %d text emails in %s", testRuns, time.Since(delStart))
|
||||
|
||||
@ -56,19 +52,17 @@ func TestMimeEmailInserts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
assertEqual(t, CountTotal(), testRuns, "Incorrect number of mime emails stored")
|
||||
assertEqual(t, CountTotal(), float64(testRuns), "Incorrect number of mime emails stored")
|
||||
|
||||
t.Logf("Inserted %d text emails in %s", testRuns, time.Since(start))
|
||||
|
||||
assertEqualStats(t, testRuns, testRuns)
|
||||
|
||||
delStart := time.Now()
|
||||
if err := DeleteAllMessages(); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
assertEqual(t, CountTotal(), 0, "incorrect number of mime emails deleted")
|
||||
assertEqual(t, CountTotal(), float64(0), "incorrect number of mime emails deleted")
|
||||
|
||||
t.Logf("Deleted %d mime emails in %s", testRuns, time.Since(delStart))
|
||||
}
|
||||
@ -107,14 +101,14 @@ func TestRetrieveMimeEmail(t *testing.T) {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
assertEqual(t, len(attachmentData.Content), msg.Attachments[0].Size, "attachment size does not match")
|
||||
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, len(inlineData.Content), msg.Inline[0].Size, "inline attachment size does not match")
|
||||
assertEqual(t, float64(len(inlineData.Content)), msg.Inline[0].Size, "inline attachment size does not match")
|
||||
}
|
||||
|
||||
func TestMessageSummary(t *testing.T) {
|
||||
|
@ -1,189 +0,0 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/GuiaBolso/darwin"
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/leporo/sqlf"
|
||||
)
|
||||
|
||||
var (
|
||||
dbMigrations = []darwin.Migration{
|
||||
{
|
||||
Version: 1.0,
|
||||
Description: "Creating tables",
|
||||
Script: `CREATE TABLE IF NOT EXISTS mailbox (
|
||||
Sort INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ID TEXT NOT NULL,
|
||||
Data BLOB,
|
||||
Search TEXT,
|
||||
Read INTEGER
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sort ON mailbox (Sort);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_id ON mailbox (ID);
|
||||
CREATE INDEX IF NOT EXISTS idx_read ON mailbox (Read);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mailbox_data (
|
||||
ID TEXT KEY NOT NULL,
|
||||
Email BLOB
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_data_id ON mailbox_data (ID);`,
|
||||
},
|
||||
{
|
||||
Version: 1.1,
|
||||
Description: "Create tags column",
|
||||
Script: `ALTER TABLE mailbox ADD COLUMN Tags Text NOT NULL DEFAULT '[]';
|
||||
CREATE INDEX IF NOT EXISTS idx_tags ON mailbox (Tags);`,
|
||||
},
|
||||
{
|
||||
Version: 1.2,
|
||||
Description: "Creating new mailbox format",
|
||||
Script: `CREATE TABLE IF NOT EXISTS mailboxtmp (
|
||||
Created INTEGER NOT NULL,
|
||||
ID TEXT NOT NULL,
|
||||
MessageID TEXT NOT NULL,
|
||||
Subject TEXT NOT NULL,
|
||||
Metadata TEXT,
|
||||
Size INTEGER NOT NULL,
|
||||
Inline INTEGER NOT NULL,
|
||||
Attachments INTEGER NOT NULL,
|
||||
Read INTEGER,
|
||||
Tags TEXT,
|
||||
SearchText TEXT
|
||||
);
|
||||
INSERT INTO mailboxtmp
|
||||
(Created, ID, MessageID, Subject, Metadata, Size, Inline, Attachments, SearchText, Read, Tags)
|
||||
SELECT
|
||||
Sort, ID, '', json_extract(Data, '$.Subject'),Data,
|
||||
json_extract(Data, '$.Size'), json_extract(Data, '$.Inline'), json_extract(Data, '$.Attachments'),
|
||||
Search, Read, Tags
|
||||
FROM mailbox;
|
||||
|
||||
DROP TABLE IF EXISTS mailbox;
|
||||
ALTER TABLE mailboxtmp RENAME TO mailbox;
|
||||
CREATE INDEX IF NOT EXISTS idx_created ON mailbox (Created);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_id ON mailbox (ID);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_id ON mailbox (MessageID);
|
||||
CREATE INDEX IF NOT EXISTS idx_subject ON mailbox (Subject);
|
||||
CREATE INDEX IF NOT EXISTS idx_size ON mailbox (Size);
|
||||
CREATE INDEX IF NOT EXISTS idx_inline ON mailbox (Inline);
|
||||
CREATE INDEX IF NOT EXISTS idx_attachments ON mailbox (Attachments);
|
||||
CREATE INDEX IF NOT EXISTS idx_read ON mailbox (Read);
|
||||
CREATE INDEX IF NOT EXISTS idx_tags ON mailbox (Tags);`,
|
||||
},
|
||||
{
|
||||
Version: 1.3,
|
||||
Description: "Create snippet column",
|
||||
Script: `ALTER TABLE mailbox ADD COLUMN Snippet Text NOT NULL DEFAULT '';`,
|
||||
},
|
||||
{
|
||||
Version: 1.4,
|
||||
Description: "Create tag tables",
|
||||
Script: `CREATE TABLE IF NOT EXISTS tags (
|
||||
ID INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
Name TEXT COLLATE NOCASE
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_tag_name ON tags (Name);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS message_tags(
|
||||
Key INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
ID TEXT REFERENCES mailbox(ID),
|
||||
TagID INT REFERENCES tags(ID)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_tag_id ON message_tags (ID);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_tag_tagid ON message_tags (TagID);`,
|
||||
},
|
||||
{
|
||||
// assume deleted messages account for 50% of storage
|
||||
// to handle previously-deleted messages
|
||||
Version: 1.5,
|
||||
Description: "Create settings table",
|
||||
Script: `CREATE TABLE IF NOT EXISTS settings (
|
||||
Key TEXT,
|
||||
Value TEXT
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_settings_key ON settings (Key);
|
||||
INSERT INTO settings (Key, Value) VALUES("DeletedSize", (SELECT SUM(Size)/2 FROM mailbox));`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Create tables and apply migrations if required
|
||||
func dbApplyMigrations() error {
|
||||
driver := darwin.NewGenericDriver(db, darwin.SqliteDialect{})
|
||||
|
||||
d := darwin.New(driver, dbMigrations, nil)
|
||||
|
||||
return d.Migrate()
|
||||
}
|
||||
|
||||
// These functions are used to migrate data formats/structure on startup.
|
||||
func dataMigrations() {
|
||||
// ensure DeletedSize has a value if empty
|
||||
if SettingGet("DeletedSize") == "" {
|
||||
_ = SettingPut("DeletedSize", "0")
|
||||
}
|
||||
|
||||
migrateTagsToManyMany()
|
||||
}
|
||||
|
||||
// Migrate tags to ManyMany structure
|
||||
// Migration task implemented 12/2023
|
||||
// Can be removed end 06/2024 and Tags column & index dropped from mailbox
|
||||
func migrateTagsToManyMany() {
|
||||
toConvert := make(map[string][]string)
|
||||
q := sqlf.
|
||||
Select("ID, Tags").
|
||||
From("mailbox").
|
||||
Where("Tags != ?", "[]").
|
||||
Where("Tags IS NOT NULL")
|
||||
|
||||
if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
|
||||
var id string
|
||||
var jsonTags string
|
||||
if err := row.Scan(&id, &jsonTags); err != nil {
|
||||
logger.Log().Errorf("[migration] %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tags := []string{}
|
||||
|
||||
if err := json.Unmarshal([]byte(jsonTags), &tags); err != nil {
|
||||
logger.Log().Errorf("[json] %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
toConvert[id] = tags
|
||||
}); err != nil {
|
||||
logger.Log().Errorf("[migration] %s", err.Error())
|
||||
}
|
||||
|
||||
if len(toConvert) > 0 {
|
||||
logger.Log().Infof("[migration] converting %d message tags", len(toConvert))
|
||||
for id, tags := range toConvert {
|
||||
if err := SetMessageTags(id, tags); err != nil {
|
||||
logger.Log().Errorf("[migration] %s", err.Error())
|
||||
} else {
|
||||
if _, err := sqlf.Update("mailbox").
|
||||
Set("Tags", nil).
|
||||
Where("ID = ?", id).
|
||||
ExecAndClose(context.TODO(), db); err != nil {
|
||||
logger.Log().Errorf("[migration] %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Log().Info("[migration] tags conversion complete")
|
||||
}
|
||||
|
||||
// set all legacy `[]` tags to NULL
|
||||
if _, err := sqlf.Update("mailbox").
|
||||
Set("Tags", nil).
|
||||
Where("Tags = ?", "[]").
|
||||
ExecAndClose(context.TODO(), db); err != nil {
|
||||
logger.Log().Errorf("[migration] %s", err.Error())
|
||||
}
|
||||
}
|
@ -24,8 +24,8 @@ func BroadcastMailboxStats() {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
bcStatsDelay = false
|
||||
b := struct {
|
||||
Total int
|
||||
Unread int
|
||||
Total float64
|
||||
Unread float64
|
||||
Version string
|
||||
}{
|
||||
Total: CountTotal(),
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"os"
|
||||
|
||||
@ -24,7 +25,7 @@ func ReindexAll() {
|
||||
finished := 0
|
||||
|
||||
err := sqlf.Select("ID").To(&i).
|
||||
From("mailbox").
|
||||
From(tenant("mailbox")).
|
||||
OrderBy("Created DESC").
|
||||
QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
|
||||
ids = append(ids, i)
|
||||
@ -112,7 +113,7 @@ func ReindexAll() {
|
||||
|
||||
// insert mail summary data
|
||||
for _, u := range updates {
|
||||
_, err = tx.Exec("UPDATE mailbox SET SearchText = ?, Snippet = ?, Metadata = ? WHERE ID = ?", u.SearchText, u.Snippet, u.Metadata, u.ID)
|
||||
_, err = tx.Exec(fmt.Sprintf(`UPDATE %s SET SearchText = ?, Snippet = ?, Metadata = ? WHERE ID = ?`, tenant("mailbox")), u.SearchText, u.Snippet, u.Metadata, u.ID)
|
||||
if err != nil {
|
||||
logger.Log().Errorf("[db] %s", err.Error())
|
||||
continue
|
||||
|
222
internal/storage/schemas.go
Normal file
222
internal/storage/schemas.go
Normal file
@ -0,0 +1,222 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/axllent/semver"
|
||||
"github.com/leporo/sqlf"
|
||||
)
|
||||
|
||||
//go:embed schemas/*
|
||||
var schemaScripts embed.FS
|
||||
|
||||
// Create tables and apply schemas if required
|
||||
func dbApplySchemas() error {
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS ` + tenant("schemas") + ` (Version TEXT PRIMARY KEY NOT NULL)`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var legacyMigrationTable int
|
||||
err := db.QueryRow(`SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name=?)`, tenant("darwin_migrations")).Scan(&legacyMigrationTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if legacyMigrationTable == 1 {
|
||||
rows, err := db.Query(`SELECT version FROM ` + tenant("darwin_migrations"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
legacySchemas := []string{}
|
||||
|
||||
for rows.Next() {
|
||||
var oldID string
|
||||
if err := rows.Scan(&oldID); err == nil {
|
||||
legacySchemas = append(legacySchemas, semver.MajorMinor(oldID)+"."+semver.Patch(oldID))
|
||||
}
|
||||
}
|
||||
|
||||
legacySchemas = semver.SortMin(legacySchemas)
|
||||
|
||||
for _, v := range legacySchemas {
|
||||
var migrated int
|
||||
err := db.QueryRow(`SELECT EXISTS(SELECT 1 FROM `+tenant("schemas")+` WHERE Version = ?)`, v).Scan(&migrated)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if migrated == 0 {
|
||||
// copy to tenant("schemas")
|
||||
if _, err := db.Exec(`INSERT INTO `+tenant("schemas")+` (Version) VALUES (?)`, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delete legacy migration database after 01/10/2024
|
||||
if time.Now().After(time.Date(2024, 10, 1, 0, 0, 0, 0, time.Local)) {
|
||||
if _, err := db.Exec(`DROP TABLE IF EXISTS ` + tenant("darwin_migrations")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
schemaFiles, err := schemaScripts.ReadDir("schemas")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
temp := template.New("")
|
||||
temp.Funcs(
|
||||
template.FuncMap{
|
||||
"tenant": tenant,
|
||||
},
|
||||
)
|
||||
|
||||
type schema struct {
|
||||
Name string
|
||||
Semver string
|
||||
}
|
||||
|
||||
scripts := []schema{}
|
||||
|
||||
for _, s := range schemaFiles {
|
||||
if !s.Type().IsRegular() || !strings.HasSuffix(s.Name(), ".sql") {
|
||||
continue
|
||||
}
|
||||
|
||||
schemaID := strings.TrimRight(s.Name(), ".sql")
|
||||
|
||||
if !semver.IsValid(schemaID) {
|
||||
logger.Log().Warnf("[db] invalid schema name: %s", s.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
script := schema{s.Name(), semver.MajorMinor(schemaID) + "." + semver.Patch(schemaID)}
|
||||
scripts = append(scripts, script)
|
||||
}
|
||||
|
||||
// sort schemas by semver, low to high
|
||||
sort.Slice(scripts, func(i, j int) bool {
|
||||
return semver.Compare(scripts[j].Semver, scripts[i].Semver) == 1
|
||||
})
|
||||
|
||||
for _, s := range scripts {
|
||||
var complete int
|
||||
err := db.QueryRow(`SELECT EXISTS(SELECT 1 FROM `+tenant("schemas")+` WHERE Version = ?)`, s.Semver).Scan(&complete)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if complete == 1 {
|
||||
// already completed, ignore
|
||||
continue
|
||||
}
|
||||
// use path.Join for Windows compatibility, see https://github.com/golang/go/issues/44305
|
||||
b, err := schemaScripts.ReadFile(path.Join("schemas", s.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// parse import script
|
||||
t1, err := temp.Parse(string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
err = t1.Execute(buf, nil)
|
||||
|
||||
if _, err := db.Exec(buf.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := db.Exec(`INSERT INTO `+tenant("schemas")+` (Version) VALUES (?)`, s.Semver); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Log().Debugf("[db] applied schema: %s", s.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// These functions are used to migrate data formats/structure on startup.
|
||||
func dataMigrations() {
|
||||
// ensure DeletedSize has a value if empty
|
||||
if SettingGet("DeletedSize") == "" {
|
||||
_ = SettingPut("DeletedSize", "0")
|
||||
}
|
||||
|
||||
migrateTagsToManyMany()
|
||||
}
|
||||
|
||||
// Migrate tags to ManyMany structure
|
||||
// Migration task implemented 12/2023
|
||||
// TODO: Can be removed end 06/2024 and Tags column & index dropped from mailbox
|
||||
func migrateTagsToManyMany() {
|
||||
toConvert := make(map[string][]string)
|
||||
q := sqlf.
|
||||
Select("ID, Tags").
|
||||
From(tenant("mailbox")).
|
||||
Where("Tags != ?", "[]").
|
||||
Where("Tags IS NOT NULL")
|
||||
|
||||
if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
|
||||
var id string
|
||||
var jsonTags string
|
||||
if err := row.Scan(&id, &jsonTags); err != nil {
|
||||
logger.Log().Errorf("[migration] %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tags := []string{}
|
||||
|
||||
if err := json.Unmarshal([]byte(jsonTags), &tags); err != nil {
|
||||
logger.Log().Errorf("[json] %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
toConvert[id] = tags
|
||||
}); err != nil {
|
||||
logger.Log().Errorf("[migration] %s", err.Error())
|
||||
}
|
||||
|
||||
if len(toConvert) > 0 {
|
||||
logger.Log().Infof("[migration] converting %d message tags", len(toConvert))
|
||||
for id, tags := range toConvert {
|
||||
if err := SetMessageTags(id, tags); err != nil {
|
||||
logger.Log().Errorf("[migration] %s", err.Error())
|
||||
} else {
|
||||
if _, err := sqlf.Update(tenant("mailbox")).
|
||||
Set("Tags", nil).
|
||||
Where("ID = ?", id).
|
||||
ExecAndClose(context.TODO(), db); err != nil {
|
||||
logger.Log().Errorf("[migration] %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Log().Info("[migration] tags conversion complete")
|
||||
}
|
||||
|
||||
// set all legacy `[]` tags to NULL
|
||||
if _, err := sqlf.Update(tenant("mailbox")).
|
||||
Set("Tags", nil).
|
||||
Where("Tags = ?", "[]").
|
||||
ExecAndClose(context.TODO(), db); err != nil {
|
||||
logger.Log().Errorf("[migration] %s", err.Error())
|
||||
}
|
||||
}
|
19
internal/storage/schemas/1.0.0.sql
Normal file
19
internal/storage/schemas/1.0.0.sql
Normal file
@ -0,0 +1,19 @@
|
||||
-- CREATE TABLES
|
||||
CREATE TABLE IF NOT EXISTS {{ tenant "mailbox" }} (
|
||||
Sort INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ID TEXT NOT NULL,
|
||||
Data BLOB,
|
||||
Search TEXT,
|
||||
Read INTEGER
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS {{ tenant "idx_sort" }} ON {{ tenant "mailbox" }} (Sort);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS {{ tenant "idx_id" }} ON {{ tenant "mailbox" }} (ID);
|
||||
CREATE INDEX IF NOT EXISTS {{ tenant "idx_read" }} ON {{ tenant "mailbox" }} (Read);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS {{ tenant "mailbox_data" }} (
|
||||
ID TEXT KEY NOT NULL,
|
||||
Email BLOB
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS {{ tenant "idx_data_id" }} ON {{ tenant "mailbox_data" }} (ID);
|
3
internal/storage/schemas/1.1.0.sql
Normal file
3
internal/storage/schemas/1.1.0.sql
Normal file
@ -0,0 +1,3 @@
|
||||
-- CREATE TAGS COLUMN
|
||||
ALTER TABLE {{ tenant "mailbox" }} ADD COLUMN Tags Text NOT NULL DEFAULT '[]';
|
||||
CREATE INDEX IF NOT EXISTS {{ tenant "idx_tags" }} ON {{ tenant "mailbox" }} (Tags);
|
36
internal/storage/schemas/1.2.0.sql
Normal file
36
internal/storage/schemas/1.2.0.sql
Normal file
@ -0,0 +1,36 @@
|
||||
-- CREATING NEW MAILBOX FORMAT
|
||||
CREATE TABLE IF NOT EXISTS {{ tenant "mailboxtmp" }} (
|
||||
Created INTEGER NOT NULL,
|
||||
ID TEXT NOT NULL,
|
||||
MessageID TEXT NOT NULL,
|
||||
Subject TEXT NOT NULL,
|
||||
Metadata TEXT,
|
||||
Size INTEGER NOT NULL,
|
||||
Inline INTEGER NOT NULL,
|
||||
Attachments INTEGER NOT NULL,
|
||||
Read INTEGER,
|
||||
Tags TEXT,
|
||||
SearchText TEXT
|
||||
);
|
||||
|
||||
INSERT INTO {{ tenant "mailboxtmp" }}
|
||||
(Created, ID, MessageID, Subject, Metadata, Size, Inline, Attachments, SearchText, Read, Tags)
|
||||
SELECT
|
||||
Sort, ID, '', json_extract(Data, '$.Subject'),Data,
|
||||
json_extract(Data, '$.Size'), json_extract(Data, '$.Inline'), json_extract(Data, '$.Attachments'),
|
||||
Search, Read, Tags
|
||||
FROM {{ tenant "mailbox" }};
|
||||
|
||||
DROP TABLE IF EXISTS {{ tenant "mailbox" }};
|
||||
|
||||
ALTER TABLE {{ tenant "mailboxtmp" }} RENAME TO {{ tenant "mailbox" }};
|
||||
|
||||
CREATE INDEX IF NOT EXISTS {{ tenant "idx_created" }} ON {{ tenant "mailbox" }} (Created);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS {{ tenant "idx_id" }} ON {{ tenant "mailbox" }} (ID);
|
||||
CREATE INDEX IF NOT EXISTS {{ tenant "idx_message_id" }} ON {{ tenant "mailbox" }} (MessageID);
|
||||
CREATE INDEX IF NOT EXISTS {{ tenant "idx_subject" }} ON {{ tenant "mailbox" }} (Subject);
|
||||
CREATE INDEX IF NOT EXISTS {{ tenant "idx_size" }} ON {{ tenant "mailbox" }} (Size);
|
||||
CREATE INDEX IF NOT EXISTS {{ tenant "idx_inline" }} ON {{ tenant "mailbox" }} (Inline);
|
||||
CREATE INDEX IF NOT EXISTS {{ tenant "idx_attachments" }} ON {{ tenant "mailbox" }} (Attachments);
|
||||
CREATE INDEX IF NOT EXISTS {{ tenant "idx_read" }} ON {{ tenant "mailbox" }} (Read);
|
||||
CREATE INDEX IF NOT EXISTS {{ tenant "idx_tags" }} ON {{ tenant "mailbox" }} (Tags);
|
2
internal/storage/schemas/1.3.0.sql
Normal file
2
internal/storage/schemas/1.3.0.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- CREATE SNIPPET COLUMN
|
||||
ALTER TABLE {{ tenant "mailbox" }} ADD COLUMN Snippet TEXT NOT NULL DEFAULT '';
|
16
internal/storage/schemas/1.4.0.sql
Normal file
16
internal/storage/schemas/1.4.0.sql
Normal file
@ -0,0 +1,16 @@
|
||||
-- CREATE TAG TABLES
|
||||
CREATE TABLE IF NOT EXISTS {{ tenant "tags" }} (
|
||||
ID INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
Name TEXT COLLATE NOCASE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS {{ tenant "idx_tag_name" }} ON {{ tenant "tags" }} (Name);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS {{ tenant "message_tags" }} (
|
||||
Key INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
ID TEXT REFERENCES {{ tenant "mailbox" }} (ID),
|
||||
TagID INT REFERENCES {{ tenant "tags" }} (ID)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS {{ tenant "idx_message_tag_id" }} ON {{ tenant "message_tags" }} (ID);
|
||||
CREATE INDEX IF NOT EXISTS {{ tenant "idx_message_tag_tagid" }} ON {{ tenant "message_tags" }} (TagID);
|
7
internal/storage/schemas/1.5.0.sql
Normal file
7
internal/storage/schemas/1.5.0.sql
Normal file
@ -0,0 +1,7 @@
|
||||
-- CREATE SETTINGS TABLE
|
||||
CREATE TABLE IF NOT EXISTS {{ tenant "settings" }} (
|
||||
Key TEXT,
|
||||
Value TEXT
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS {{ tenant "idx_settings_key" }} ON {{ tenant "settings" }} (Key);
|
||||
INSERT INTO {{ tenant "settings" }} (Key, Value) VALUES ("DeletedSize", (SELECT SUM(Size)/2 FROM {{ tenant "mailbox" }}));
|
5
internal/storage/schemas/README.md
Normal file
5
internal/storage/schemas/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Migration scripts
|
||||
|
||||
- Scripts should be named using semver and have the `.sql` extension.
|
||||
- Inline comments should be prefixed with a `--`
|
||||
- All references to tables and indexes should be wrapped with a `{{ tenant "<name>" }}`
|
@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/araddon/dateparse"
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/axllent/mailpit/internal/tools"
|
||||
"github.com/leporo/sqlf"
|
||||
@ -17,7 +18,7 @@ import (
|
||||
// The search is broken up by segments (exact phrases can be quoted), and interprets specific terms such as:
|
||||
// is:read, is:unread, has:attachment, to:<term>, from:<term> & subject:<term>
|
||||
// Negative searches also also included by prefixing the search term with a `-` or `!`
|
||||
func Search(search string, start, limit int) ([]MessageSummary, int, error) {
|
||||
func Search(search, timezone string, start, limit int) ([]MessageSummary, int, error) {
|
||||
results := []MessageSummary{}
|
||||
allResults := []MessageSummary{}
|
||||
tsStart := time.Now()
|
||||
@ -26,16 +27,16 @@ func Search(search string, start, limit int) ([]MessageSummary, int, error) {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
q := searchQueryBuilder(search)
|
||||
q := searchQueryBuilder(search, timezone)
|
||||
var err error
|
||||
|
||||
if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
|
||||
var created int64
|
||||
var created float64
|
||||
var id string
|
||||
var messageID string
|
||||
var subject string
|
||||
var metadata string
|
||||
var size int
|
||||
var size float64
|
||||
var attachments int
|
||||
var snippet string
|
||||
var read int
|
||||
@ -52,7 +53,7 @@ func Search(search string, start, limit int) ([]MessageSummary, int, error) {
|
||||
return
|
||||
}
|
||||
|
||||
em.Created = time.UnixMilli(created)
|
||||
em.Created = time.UnixMilli(int64(created))
|
||||
em.ID = id
|
||||
em.MessageID = messageID
|
||||
em.Subject = subject
|
||||
@ -95,21 +96,20 @@ func Search(search string, start, limit int) ([]MessageSummary, int, error) {
|
||||
// The search is broken up by segments (exact phrases can be quoted), and interprets specific terms such as:
|
||||
// is:read, is:unread, has:attachment, to:<term>, from:<term> & subject:<term>
|
||||
// Negative searches also also included by prefixing the search term with a `-` or `!`
|
||||
func DeleteSearch(search string) error {
|
||||
q := searchQueryBuilder(search)
|
||||
func DeleteSearch(search, timezone string) error {
|
||||
q := searchQueryBuilder(search, timezone)
|
||||
|
||||
ids := []string{}
|
||||
deleteSize := 0
|
||||
deleteSize := float64(0)
|
||||
|
||||
if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
|
||||
var created int64
|
||||
var created float64
|
||||
var id string
|
||||
var messageID string
|
||||
var subject string
|
||||
var metadata string
|
||||
var size int
|
||||
var size float64
|
||||
var attachments int
|
||||
// var tags string
|
||||
var read int
|
||||
var snippet string
|
||||
var ignore string
|
||||
@ -160,21 +160,21 @@ func DeleteSearch(search string) error {
|
||||
delIDs[i] = id
|
||||
}
|
||||
|
||||
sqlDelete1 := `DELETE FROM mailbox WHERE ID IN (?` + strings.Repeat(",?", len(ids)-1) + `)` // #nosec
|
||||
sqlDelete1 := `DELETE FROM ` + tenant("mailbox") + ` WHERE ID IN (?` + strings.Repeat(",?", len(ids)-1) + `)` // #nosec
|
||||
|
||||
_, err = tx.Exec(sqlDelete1, delIDs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sqlDelete2 := `DELETE FROM mailbox_data WHERE ID IN (?` + strings.Repeat(",?", len(ids)-1) + `)` // #nosec
|
||||
sqlDelete2 := `DELETE FROM ` + tenant("mailbox_data") + ` WHERE ID IN (?` + strings.Repeat(",?", len(ids)-1) + `)` // #nosec
|
||||
|
||||
_, err = tx.Exec(sqlDelete2, delIDs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sqlDelete3 := `DELETE FROM message_tags WHERE ID IN (?` + strings.Repeat(",?", len(ids)-1) + `)` // #nosec
|
||||
sqlDelete3 := `DELETE FROM ` + tenant("message_tags") + ` WHERE ID IN (?` + strings.Repeat(",?", len(ids)-1) + `)` // #nosec
|
||||
|
||||
_, err = tx.Exec(sqlDelete3, delIDs...)
|
||||
if err != nil {
|
||||
@ -204,11 +204,20 @@ 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 {
|
||||
func searchQueryBuilder(searchString, timezone string) *sqlf.Stmt {
|
||||
// group strings with quotes as a single argument and remove quotes
|
||||
args := tools.ArgsParser(searchString)
|
||||
|
||||
q := sqlf.From("mailbox m").
|
||||
if timezone != "" {
|
||||
loc, err := time.LoadLocation(timezone)
|
||||
if err != nil {
|
||||
logger.Log().Warnf("ignoring invalid timezone:\"%s\"", timezone)
|
||||
} else {
|
||||
time.Local = loc
|
||||
}
|
||||
}
|
||||
|
||||
q := sqlf.From(tenant("mailbox") + " m").
|
||||
Select(`m.Created, m.ID, m.MessageID, m.Subject, m.Metadata, m.Size, m.Attachments, m.Read,
|
||||
m.Snippet,
|
||||
IFNULL(json_extract(Metadata, '$.To'), '{}') as ToJSON,
|
||||
@ -307,9 +316,9 @@ func searchQueryBuilder(searchString string) *sqlf.Stmt {
|
||||
w = cleanString(w[4:])
|
||||
if w != "" {
|
||||
if exclude {
|
||||
q.Where(`m.ID NOT IN (SELECT mt.ID FROM message_tags mt JOIN tags t ON mt.TagID = t.ID WHERE t.Name = ?)`, w)
|
||||
q.Where(`m.ID NOT IN (SELECT mt.ID FROM `+tenant("message_tags")+` mt JOIN `+tenant("tags")+` t ON mt.TagID = t.ID WHERE t.Name = ?)`, w)
|
||||
} else {
|
||||
q.Where(`m.ID IN (SELECT mt.ID FROM message_tags mt JOIN tags t ON mt.TagID = t.ID WHERE t.Name = ?)`, w)
|
||||
q.Where(`m.ID IN (SELECT mt.ID FROM `+tenant("message_tags")+` mt JOIN `+tenant("tags")+` t ON mt.TagID = t.ID WHERE t.Name = ?)`, w)
|
||||
}
|
||||
}
|
||||
} else if lw == "is:read" {
|
||||
@ -326,9 +335,9 @@ func searchQueryBuilder(searchString string) *sqlf.Stmt {
|
||||
}
|
||||
} 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)`)
|
||||
q.Where(`m.ID NOT IN (SELECT DISTINCT mt.ID FROM ` + tenant("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)`)
|
||||
q.Where(`m.ID IN (SELECT DISTINCT mt.ID FROM ` + tenant("message_tags") + ` mt JOIN tags t ON mt.TagID = t.ID)`)
|
||||
}
|
||||
} else if lw == "has:attachment" || lw == "has:attachments" {
|
||||
if exclude {
|
||||
@ -336,6 +345,36 @@ func searchQueryBuilder(searchString string) *sqlf.Stmt {
|
||||
} else {
|
||||
q.Where("Attachments > 0")
|
||||
}
|
||||
} else if strings.HasPrefix(lw, "after:") {
|
||||
w = cleanString(w[6:])
|
||||
if w != "" {
|
||||
t, err := dateparse.ParseLocal(w)
|
||||
if err != nil {
|
||||
logger.Log().Warnf("ignoring invalid after: date \"%s\"", w)
|
||||
} else {
|
||||
timestamp := t.UnixMilli()
|
||||
if exclude {
|
||||
q.Where(`m.Created <= ?`, timestamp)
|
||||
} else {
|
||||
q.Where(`m.Created >= ?`, timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if strings.HasPrefix(lw, "before:") {
|
||||
w = cleanString(w[7:])
|
||||
if w != "" {
|
||||
t, err := dateparse.ParseLocal(w)
|
||||
if err != nil {
|
||||
logger.Log().Warnf("ignoring invalid before: date \"%s\"", w)
|
||||
} else {
|
||||
timestamp := t.UnixMilli()
|
||||
if exclude {
|
||||
q.Where(`m.Created >= ?`, timestamp)
|
||||
} else {
|
||||
q.Where(`m.Created <= ?`, timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// search text
|
||||
if exclude {
|
||||
|
@ -69,7 +69,7 @@ func TestSearch(t *testing.T) {
|
||||
|
||||
search := uniqueSearches[searchIdx]
|
||||
|
||||
summaries, _, err := Search(search, 0, 100)
|
||||
summaries, _, err := Search(search, "", 0, 100)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
@ -85,7 +85,7 @@ func TestSearch(t *testing.T) {
|
||||
}
|
||||
|
||||
// search something that will return 200 results
|
||||
summaries, _, err := Search("This is the email body", 0, testRuns)
|
||||
summaries, _, err := Search("This is the email body", "", 0, testRuns)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
@ -109,7 +109,7 @@ func TestSearchDelete100(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
_, total, err := Search("from:sender@example.com", 0, 100)
|
||||
_, total, err := Search("from:sender@example.com", "", 0, 100)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
@ -117,12 +117,12 @@ func TestSearchDelete100(t *testing.T) {
|
||||
|
||||
assertEqual(t, total, 100, "100 search results expected")
|
||||
|
||||
if err := DeleteSearch("from:sender@example.com"); err != nil {
|
||||
if err := DeleteSearch("from:sender@example.com", ""); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
_, total, err = Search("from:sender@example.com", 0, 100)
|
||||
_, total, err = Search("from:sender@example.com", "", 0, 100)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
@ -143,7 +143,7 @@ func TestSearchDelete1100(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
_, total, err := Search("from:sender@example.com", 0, 100)
|
||||
_, total, err := Search("from:sender@example.com", "", 0, 100)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
@ -151,12 +151,12 @@ func TestSearchDelete1100(t *testing.T) {
|
||||
|
||||
assertEqual(t, total, 1100, "100 search results expected")
|
||||
|
||||
if err := DeleteSearch("from:sender@example.com"); err != nil {
|
||||
if err := DeleteSearch("from:sender@example.com", ""); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
_, total, err = Search("from:sender@example.com", 0, 100)
|
||||
_, total, err = Search("from:sender@example.com", "", 0, 100)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
// SettingGet returns a setting string value, blank is it does not exist
|
||||
func SettingGet(k string) string {
|
||||
var result sql.NullString
|
||||
err := sqlf.From("settings").
|
||||
err := sqlf.From(tenant("settings")).
|
||||
Select("Value").To(&result).
|
||||
Where("Key = ?", k).
|
||||
Limit(1).
|
||||
@ -26,7 +26,7 @@ func SettingGet(k string) string {
|
||||
|
||||
// SettingPut sets a setting string value, inserting if new
|
||||
func SettingPut(k, v string) error {
|
||||
_, err := db.Exec("INSERT INTO settings (Key, Value) VALUES(?, ?) ON CONFLICT(Key) DO UPDATE SET Value = ?", k, v, v)
|
||||
_, err := db.Exec(`INSERT INTO `+tenant("settings")+` (Key, Value) VALUES(?, ?) ON CONFLICT(Key) DO UPDATE SET Value = ?`, k, v, v)
|
||||
if err != nil {
|
||||
logger.Log().Errorf("[db] %s", err.Error())
|
||||
}
|
||||
@ -35,9 +35,9 @@ func SettingPut(k, v string) error {
|
||||
}
|
||||
|
||||
// The total deleted message size as an int64 value
|
||||
func getDeletedSize() int64 {
|
||||
var result sql.NullInt64
|
||||
err := sqlf.From("settings").
|
||||
func getDeletedSize() float64 {
|
||||
var result sql.NullFloat64
|
||||
err := sqlf.From(tenant("settings")).
|
||||
Select("Value").To(&result).
|
||||
Where("Key = ?", "DeletedSize").
|
||||
Limit(1).
|
||||
@ -47,13 +47,13 @@ func getDeletedSize() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return result.Int64
|
||||
return result.Float64
|
||||
}
|
||||
|
||||
// The total raw non-compressed messages size in bytes of all messages in the database
|
||||
func totalMessagesSize() int64 {
|
||||
var result sql.NullInt64
|
||||
err := sqlf.From("mailbox").
|
||||
func totalMessagesSize() float64 {
|
||||
var result sql.NullFloat64
|
||||
err := sqlf.From(tenant("mailbox")).
|
||||
Select("SUM(Size)").To(&result).
|
||||
QueryAndClose(context.TODO(), db, func(row *sql.Rows) {})
|
||||
if err != nil {
|
||||
@ -61,16 +61,16 @@ func totalMessagesSize() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return result.Int64
|
||||
return result.Float64
|
||||
}
|
||||
|
||||
// AddDeletedSize will add the value to the DeletedSize setting
|
||||
func addDeletedSize(v int64) {
|
||||
if _, err := db.Exec("INSERT OR IGNORE INTO settings (Key, Value) VALUES(?, ?)", "DeletedSize", 0); err != nil {
|
||||
if _, err := db.Exec(`INSERT OR IGNORE INTO `+tenant("settings")+` (Key, Value) VALUES(?, ?)`, "DeletedSize", 0); err != nil {
|
||||
logger.Log().Errorf("[db] %s", err.Error())
|
||||
}
|
||||
|
||||
if _, err := db.Exec("UPDATE settings SET Value = Value + ? WHERE Key = ?", v, "DeletedSize"); err != nil {
|
||||
if _, err := db.Exec(`UPDATE `+tenant("settings")+` SET Value = Value + ? WHERE Key = ?`, v, "DeletedSize"); err != nil {
|
||||
logger.Log().Errorf("[db] %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ type Message struct {
|
||||
// Message body HTML
|
||||
HTML string
|
||||
// Message size in bytes
|
||||
Size int
|
||||
Size float64
|
||||
// Inline message attachments
|
||||
Inline []Attachment
|
||||
// Message attachments
|
||||
@ -61,7 +61,7 @@ type Attachment struct {
|
||||
// Content ID
|
||||
ContentID string
|
||||
// Size in bytes
|
||||
Size int
|
||||
Size float64
|
||||
}
|
||||
|
||||
// MessageSummary struct for frontend messages
|
||||
@ -91,7 +91,7 @@ type MessageSummary struct {
|
||||
// Message tags
|
||||
Tags []string
|
||||
// Message size in bytes (total)
|
||||
Size int
|
||||
Size float64
|
||||
// Whether the message has any attachments
|
||||
Attachments int
|
||||
// Message snippet includes up to 250 characters
|
||||
@ -100,8 +100,8 @@ type MessageSummary struct {
|
||||
|
||||
// MailboxStats struct for quick mailbox total/read lookups
|
||||
type MailboxStats struct {
|
||||
Total int
|
||||
Unread int
|
||||
Total float64
|
||||
Unread float64
|
||||
Tags []string
|
||||
}
|
||||
|
||||
@ -124,7 +124,7 @@ func AttachmentSummary(a *enmime.Part) Attachment {
|
||||
}
|
||||
o.ContentType = a.ContentType
|
||||
o.ContentID = a.ContentID
|
||||
o.Size = len(a.Content)
|
||||
o.Size = float64(len(a.Content))
|
||||
|
||||
return o
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ func SetMessageTags(id string, tags []string) error {
|
||||
func AddMessageTag(id, name string) error {
|
||||
var tagID int
|
||||
|
||||
q := sqlf.From("tags").
|
||||
q := sqlf.From(tenant("tags")).
|
||||
Select("ID").To(&tagID).
|
||||
Where("Name = ?", name)
|
||||
|
||||
@ -68,7 +68,7 @@ func AddMessageTag(id, name string) error {
|
||||
if err := q.QueryRowAndClose(context.TODO(), db); err == nil {
|
||||
// check message does not already have this tag
|
||||
var count int
|
||||
if _, err := sqlf.From("message_tags").
|
||||
if _, err := sqlf.From(tenant("message_tags")).
|
||||
Select("COUNT(ID)").To(&count).
|
||||
Where("ID = ?", id).
|
||||
Where("TagID = ?", tagID).
|
||||
@ -82,7 +82,7 @@ func AddMessageTag(id, name string) error {
|
||||
|
||||
logger.Log().Debugf("[tags] adding tag \"%s\" to %s", name, id)
|
||||
|
||||
_, err := sqlf.InsertInto("message_tags").
|
||||
_, err := sqlf.InsertInto(tenant("message_tags")).
|
||||
Set("ID", id).
|
||||
Set("TagID", tagID).
|
||||
ExecAndClose(context.TODO(), db)
|
||||
@ -92,39 +92,20 @@ func AddMessageTag(id, name string) error {
|
||||
logger.Log().Debugf("[tags] adding tag \"%s\" to %s", name, id)
|
||||
|
||||
// tag dos not exist, add new one
|
||||
if err := sqlf.InsertInto("tags").
|
||||
if _, err := sqlf.InsertInto(tenant("tags")).
|
||||
Set("Name", name).
|
||||
Returning("ID").To(&tagID).
|
||||
QueryRowAndClose(context.TODO(), db); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check message does not already have this tag
|
||||
var count int
|
||||
if _, err := sqlf.From("message_tags").
|
||||
Select("COUNT(ID)").To(&count).
|
||||
Where("ID = ?", id).
|
||||
Where("TagID = ?", tagID).
|
||||
ExecAndClose(context.TODO(), db); err != nil {
|
||||
return err
|
||||
}
|
||||
if count != 0 {
|
||||
return nil // already exists
|
||||
}
|
||||
|
||||
// add tag to message
|
||||
_, err := sqlf.InsertInto("message_tags").
|
||||
Set("ID", id).
|
||||
Set("TagID", tagID).
|
||||
ExecAndClose(context.TODO(), db)
|
||||
return err
|
||||
return AddMessageTag(id, name)
|
||||
}
|
||||
|
||||
// DeleteMessageTag deleted a tag from a message
|
||||
func DeleteMessageTag(id, name string) error {
|
||||
if _, err := sqlf.DeleteFrom("message_tags").
|
||||
Where("message_tags.ID = ?", id).
|
||||
Where(`message_tags.Key IN (SELECT Key FROM message_tags LEFT JOIN tags ON TagID=tags.ID WHERE Name = ?)`, name).
|
||||
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).
|
||||
ExecAndClose(context.TODO(), db); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -134,8 +115,8 @@ func DeleteMessageTag(id, name string) error {
|
||||
|
||||
// DeleteAllMessageTags deleted all tags from a message
|
||||
func DeleteAllMessageTags(id string) error {
|
||||
if _, err := sqlf.DeleteFrom("message_tags").
|
||||
Where("message_tags.ID = ?", id).
|
||||
if _, err := sqlf.DeleteFrom(tenant("message_tags")).
|
||||
Where(tenant("message_tags.ID")+" = ?", id).
|
||||
ExecAndClose(context.TODO(), db); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -150,7 +131,7 @@ func GetAllTags() []string {
|
||||
|
||||
if err := sqlf.
|
||||
Select(`DISTINCT Name`).
|
||||
From("tags").To(&name).
|
||||
From(tenant("tags")).To(&name).
|
||||
OrderBy("Name").
|
||||
QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
|
||||
tags = append(tags, name)
|
||||
@ -169,10 +150,10 @@ func GetAllTagsCount() map[string]int64 {
|
||||
|
||||
if err := sqlf.
|
||||
Select(`Name`).To(&name).
|
||||
Select(`COUNT(message_tags.TagID) as total`).To(&total).
|
||||
From("tags").
|
||||
LeftJoin("message_tags", "tags.ID = message_tags.TagID").
|
||||
GroupBy("message_tags.TagID").
|
||||
Select(`COUNT(`+tenant("message_tags.TagID")+`) as total`).To(&total).
|
||||
From(tenant("tags")).
|
||||
LeftJoin(tenant("message_tags"), tenant("tags.ID")+" = "+tenant("message_tags.TagID")).
|
||||
GroupBy(tenant("message_tags.TagID")).
|
||||
OrderBy("Name").
|
||||
QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
|
||||
tags[name] = total
|
||||
@ -186,10 +167,10 @@ func GetAllTagsCount() map[string]int64 {
|
||||
|
||||
// PruneUnusedTags will delete all unused tags from the database
|
||||
func pruneUnusedTags() error {
|
||||
q := sqlf.From("tags").
|
||||
Select("tags.ID, tags.Name, COUNT(message_tags.ID) as COUNT").
|
||||
LeftJoin("message_tags", "tags.ID = message_tags.TagID").
|
||||
GroupBy("tags.ID")
|
||||
q := sqlf.From(tenant("tags")).
|
||||
Select(tenant("tags.ID")+", "+tenant("tags.Name")+", COUNT("+tenant("message_tags.ID")+") as COUNT").
|
||||
LeftJoin(tenant("message_tags"), tenant("tags.ID")+" = "+tenant("message_tags.TagID")).
|
||||
GroupBy(tenant("tags.ID"))
|
||||
|
||||
toDel := []int{}
|
||||
|
||||
@ -213,7 +194,7 @@ func pruneUnusedTags() error {
|
||||
|
||||
if len(toDel) > 0 {
|
||||
for _, id := range toDel {
|
||||
if _, err := sqlf.DeleteFrom("tags").
|
||||
if _, err := sqlf.DeleteFrom(tenant("tags")).
|
||||
Where("ID = ?", id).
|
||||
ExecAndClose(context.TODO(), db); err != nil {
|
||||
return err
|
||||
@ -246,24 +227,24 @@ func findTagsInRawMessage(message *[]byte) string {
|
||||
func (d DBMailSummary) tagsFromPlusAddresses() string {
|
||||
tags := []string{}
|
||||
for _, c := range d.To {
|
||||
matches := addressPlusRe.FindAllStringSubmatch(c.String(), 1)
|
||||
matches := addressPlusRe.FindAllStringSubmatch(c.Address, 1)
|
||||
if len(matches) == 1 {
|
||||
tags = append(tags, strings.Split(matches[0][2], "+")...)
|
||||
}
|
||||
}
|
||||
for _, c := range d.Cc {
|
||||
matches := addressPlusRe.FindAllStringSubmatch(c.String(), 1)
|
||||
matches := addressPlusRe.FindAllStringSubmatch(c.Address, 1)
|
||||
if len(matches) == 1 {
|
||||
tags = append(tags, strings.Split(matches[0][2], "+")...)
|
||||
}
|
||||
}
|
||||
for _, c := range d.Bcc {
|
||||
matches := addressPlusRe.FindAllStringSubmatch(c.String(), 1)
|
||||
matches := addressPlusRe.FindAllStringSubmatch(c.Address, 1)
|
||||
if len(matches) == 1 {
|
||||
tags = append(tags, strings.Split(matches[0][2], "+")...)
|
||||
}
|
||||
}
|
||||
matches := addressPlusRe.FindAllStringSubmatch(d.From.String(), 1)
|
||||
matches := addressPlusRe.FindAllStringSubmatch(d.From.Address, 1)
|
||||
if len(matches) == 1 {
|
||||
tags = append(tags, strings.Split(matches[0][2], "+")...)
|
||||
}
|
||||
@ -279,9 +260,9 @@ func getMessageTags(id string) []string {
|
||||
|
||||
if err := sqlf.
|
||||
Select(`Name`).To(&name).
|
||||
From("Tags").
|
||||
LeftJoin("message_tags", "Tags.ID=message_tags.TagID").
|
||||
Where(`message_tags.ID = ?`, id).
|
||||
From(tenant("Tags")).
|
||||
LeftJoin(tenant("message_tags"), tenant("Tags.ID")+"="+tenant("message_tags.TagID")).
|
||||
Where(tenant("message_tags.ID")+` = ?`, id).
|
||||
OrderBy("Name").
|
||||
QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
|
||||
tags = append(tags, name)
|
||||
|
@ -19,7 +19,7 @@ var (
|
||||
func setup() {
|
||||
logger.NoLogging = true
|
||||
config.MaxMessages = 0
|
||||
config.DataFile = ""
|
||||
config.Database = os.Getenv("MP_DATABASE")
|
||||
|
||||
if err := InitDB(); err != nil {
|
||||
panic(err)
|
||||
@ -27,6 +27,11 @@ func setup() {
|
||||
|
||||
var err error
|
||||
|
||||
// ensure DB is empty
|
||||
if err := DeleteAllMessages(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
testTextEmail, err = os.ReadFile("testdata/plain-text.eml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -53,11 +58,11 @@ func assertEqual(t *testing.T, a interface{}, b interface{}, message string) {
|
||||
|
||||
func assertEqualStats(t *testing.T, total int, unread int) {
|
||||
s := StatsGet()
|
||||
if total != s.Total {
|
||||
t.Fatalf("Incorrect total mailbox stats: \"%d\" != \"%d\"", total, s.Total)
|
||||
if float64(total) != s.Total {
|
||||
t.Fatalf("Incorrect total mailbox stats: \"%v\" != \"%v\"", total, s.Total)
|
||||
}
|
||||
|
||||
if unread != s.Unread {
|
||||
t.Fatalf("Incorrect unread mailbox stats: \"%d\" != \"%d\"", unread, s.Unread)
|
||||
if float64(unread) != s.Unread {
|
||||
t.Fatalf("Incorrect unread mailbox stats: \"%v\" != \"%v\"", unread, s.Unread)
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ var (
|
||||
// for stats to prevent import cycle
|
||||
mu sync.RWMutex
|
||||
// StatsDeleted for counting the number of messages deleted
|
||||
StatsDeleted int
|
||||
StatsDeleted float64
|
||||
)
|
||||
|
||||
// Return a header field as a []*mail.Address, or "null" is not found/empty
|
||||
@ -73,7 +73,7 @@ func cleanString(str string) string {
|
||||
// LogMessagesDeleted logs the number of messages deleted
|
||||
func logMessagesDeleted(n int) {
|
||||
mu.Lock()
|
||||
StatsDeleted = StatsDeleted + n
|
||||
StatsDeleted = StatsDeleted + float64(n)
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
|
1394
package-lock.json
generated
1394
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -67,7 +67,7 @@ func GetMessages(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
res.Start = start
|
||||
res.Messages = messages
|
||||
res.Count = len(messages) // legacy - now undocumented in API specs
|
||||
res.Count = float64(len(messages)) // legacy - now undocumented in API specs
|
||||
res.Total = stats.Total
|
||||
res.Unread = stats.Unread
|
||||
res.Tags = stats.Tags
|
||||
@ -109,6 +109,11 @@ func Search(w http.ResponseWriter, r *http.Request) {
|
||||
// required: false
|
||||
// type: integer
|
||||
// default: 50
|
||||
// + name: tz
|
||||
// in: query
|
||||
// description: Timezone for `before:` & `after:` queries, eg: "Pacific/Auckland"
|
||||
// required: false
|
||||
// type: string
|
||||
//
|
||||
// Responses:
|
||||
// 200: MessagesSummaryResponse
|
||||
@ -121,7 +126,7 @@ func Search(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
start, limit := getStartLimit(r)
|
||||
|
||||
messages, results, err := storage.Search(search, start, limit)
|
||||
messages, results, err := storage.Search(search, r.URL.Query().Get("tz"), start, limit)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
@ -133,9 +138,9 @@ func Search(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
res.Start = start
|
||||
res.Messages = messages
|
||||
res.Count = len(messages) // legacy - now undocumented in API specs
|
||||
res.Count = float64(len(messages)) // legacy - now undocumented in API specs
|
||||
res.Total = stats.Total // total messages in mailbox
|
||||
res.MessagesCount = results
|
||||
res.MessagesCount = float64(results)
|
||||
res.Unread = stats.Unread
|
||||
res.Tags = stats.Tags
|
||||
|
||||
@ -173,7 +178,7 @@ func DeleteSearch(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := storage.DeleteSearch(search); err != nil {
|
||||
if err := storage.DeleteSearch(search, r.URL.Query().Get("tz")); err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
@ -337,7 +342,11 @@ func GetHeaders(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
bytes, _ := json.Marshal(m.Header)
|
||||
bytes, err := json.Marshal(m.Header)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
_, _ = w.Write(bytes)
|
||||
@ -428,13 +437,11 @@ func DeleteMessages(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
for _, id := range data.IDs {
|
||||
if err := storage.DeleteOneMessage(id); err != nil {
|
||||
if err := storage.DeleteMessages(data.IDs); err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "application/plain")
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
|
@ -10,18 +10,18 @@ import (
|
||||
// MessagesSummary is a summary of a list of messages
|
||||
type MessagesSummary struct {
|
||||
// Total number of messages in mailbox
|
||||
Total int `json:"total"`
|
||||
Total float64 `json:"total"`
|
||||
|
||||
// Total number of unread messages in mailbox
|
||||
Unread int `json:"unread"`
|
||||
Unread float64 `json:"unread"`
|
||||
|
||||
// Legacy - now undocumented in API specs but left for backwards compatibility.
|
||||
// Removed from API documentation 2023-07-12
|
||||
// swagger:ignore
|
||||
Count int `json:"count"`
|
||||
Count float64 `json:"count"`
|
||||
|
||||
// Total number of messages matching current query
|
||||
MessagesCount int `json:"messages_count"`
|
||||
MessagesCount float64 `json:"messages_count"`
|
||||
|
||||
// Pagination offset
|
||||
Start int `json:"start"`
|
||||
|
@ -19,7 +19,7 @@ func RedirectToLatestMessage(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
search := strings.TrimSpace(r.URL.Query().Get("query"))
|
||||
if search != "" {
|
||||
messages, _, err = storage.Search(search, 0, 1)
|
||||
messages, _, err = storage.Search(search, "", 0, 1)
|
||||
if err != nil {
|
||||
httpError(w, err.Error())
|
||||
return
|
||||
|
@ -78,11 +78,10 @@ func Run() {
|
||||
|
||||
type message struct {
|
||||
ID string
|
||||
Size int
|
||||
Size float64
|
||||
}
|
||||
|
||||
func handleClient(conn net.Conn) {
|
||||
|
||||
var (
|
||||
user = ""
|
||||
state = 1
|
||||
@ -92,7 +91,7 @@ func handleClient(conn net.Conn) {
|
||||
defer func() {
|
||||
if state == UPDATE {
|
||||
for _, id := range toDelete {
|
||||
_ = storage.DeleteOneMessage(id)
|
||||
_ = storage.DeleteMessages([]string{id})
|
||||
}
|
||||
if len(toDelete) > 0 {
|
||||
// update web UI to remove deleted messages
|
||||
@ -178,19 +177,19 @@ func handleClient(conn net.Conn) {
|
||||
}
|
||||
|
||||
} else if cmd == "STAT" && state == TRANSACTION {
|
||||
totalSize := 0
|
||||
totalSize := float64(0)
|
||||
for _, m := range messages {
|
||||
totalSize = totalSize + m.Size
|
||||
}
|
||||
|
||||
sendResponse(conn, fmt.Sprintf("+OK %d %d", len(messages), totalSize))
|
||||
sendResponse(conn, fmt.Sprintf("+OK %d %d", len(messages), int64(totalSize)))
|
||||
|
||||
} else if cmd == "LIST" && state == TRANSACTION {
|
||||
totalSize := 0
|
||||
totalSize := float64(0)
|
||||
for _, m := range messages {
|
||||
totalSize = totalSize + m.Size
|
||||
}
|
||||
sendData(conn, fmt.Sprintf("+OK %d messages (%d octets)", len(messages), totalSize))
|
||||
sendData(conn, fmt.Sprintf("+OK %d messages (%d octets)", len(messages), int64(totalSize)))
|
||||
|
||||
// print all sizes
|
||||
for row, m := range messages {
|
||||
@ -200,7 +199,7 @@ func handleClient(conn net.Conn) {
|
||||
sendData(conn, ".")
|
||||
|
||||
} else if cmd == "UIDL" && state == TRANSACTION {
|
||||
totalSize := 0
|
||||
totalSize := float64(0)
|
||||
for _, m := range messages {
|
||||
totalSize = totalSize + m.Size
|
||||
}
|
||||
|
@ -193,20 +193,6 @@ func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
if auth.UICredentials != nil {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
|
||||
if !ok {
|
||||
basicAuthResponse(w)
|
||||
return
|
||||
}
|
||||
|
||||
if !auth.UICredentials.Match(user, pass) {
|
||||
basicAuthResponse(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
fn(w, r)
|
||||
return
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@ -204,11 +205,15 @@ func TestAPIv1Search(t *testing.T) {
|
||||
func setup() {
|
||||
logger.NoLogging = true
|
||||
config.MaxMessages = 0
|
||||
config.DataFile = ""
|
||||
config.Database = os.Getenv("MP_DATABASE")
|
||||
|
||||
if err := storage.InitDB(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := storage.DeleteAllMessages(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertStatsEqual(t *testing.T, uri string, unread, total int) {
|
||||
@ -225,8 +230,8 @@ func assertStatsEqual(t *testing.T, uri string, unread, total int) {
|
||||
return
|
||||
}
|
||||
|
||||
assertEqual(t, unread, m.Unread, "wrong unread count")
|
||||
assertEqual(t, total, m.Total, "wrong total count")
|
||||
assertEqual(t, float64(unread), m.Unread, "wrong unread count")
|
||||
assertEqual(t, float64(total), m.Total, "wrong total count")
|
||||
}
|
||||
|
||||
func assertSearchEqual(t *testing.T, uri, query string, count int) {
|
||||
@ -246,7 +251,7 @@ func assertSearchEqual(t *testing.T, uri, query string, count int) {
|
||||
return
|
||||
}
|
||||
|
||||
assertEqual(t, count, m.MessagesCount, "wrong search results count")
|
||||
assertEqual(t, float64(count), m.MessagesCount, "wrong search results count")
|
||||
}
|
||||
|
||||
func insertEmailData(t *testing.T) {
|
||||
|
@ -16,6 +16,7 @@ export default {
|
||||
beforeMount() {
|
||||
document.title = document.title + ' - ' + location.hostname
|
||||
mailbox.showTagColors = !localStorage.getItem('hideTagColors') == '1'
|
||||
mailbox.timeZone = localStorage.getItem('timezone') ? localStorage.getItem('timezone') : Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
// load global config
|
||||
this.get(this.resolve('/api/v1/webui'), false, function (response) {
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
<script>
|
||||
import Attachments from './Attachments.vue'
|
||||
import Headers from './Headers.vue'
|
||||
@ -32,6 +31,7 @@ export default {
|
||||
srcURI: false,
|
||||
iframes: [], // for resizing
|
||||
canSaveTags: false, // prevent auto-saving tags on render
|
||||
availableTags: [],
|
||||
messageTags: [],
|
||||
loadHeaders: false,
|
||||
htmlScore: false,
|
||||
@ -90,7 +90,7 @@ export default {
|
||||
|
||||
// manually refresh tags
|
||||
self.get(self.resolve(`/api/v1/tags`), false, function (response) {
|
||||
mailbox.tags = response.data
|
||||
self.availableTags = response.data
|
||||
self.$nextTick(function () {
|
||||
Tags.init('select[multiple]')
|
||||
// delay tag change detection to allow Tags to load
|
||||
@ -239,7 +239,8 @@ export default {
|
||||
<th class="small">From</th>
|
||||
<td class="privacy">
|
||||
<span v-if="message.From">
|
||||
<span v-if="message.From.Name" class="text-spaces">{{ message.From.Name + " " }}</span>
|
||||
<span v-if="message.From.Name" class="text-spaces">{{ message.From.Name + " "
|
||||
}}</span>
|
||||
<span v-if="message.From.Address" class="small">
|
||||
<<a :href="searchURI(message.From.Address)" class="text-body">
|
||||
{{ message.From.Address }}
|
||||
@ -341,7 +342,7 @@ export default {
|
||||
data-separator="|,|">
|
||||
<option value="">Type a tag...</option>
|
||||
<!-- you need at least one option with the placeholder -->
|
||||
<option v-for="t in mailbox.tags" :value="t">{{ t }}</option>
|
||||
<option v-for="t in availableTags" :value="t">{{ t }}</option>
|
||||
</select>
|
||||
<div class="invalid-feedback">Invalid tag name</div>
|
||||
</td>
|
||||
@ -423,7 +424,8 @@ export default {
|
||||
Raw
|
||||
</button>
|
||||
<div class="dropdown d-xl-none">
|
||||
<button class="nav-link dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<button class="nav-link dropdown-toggle" type="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
Checks
|
||||
</button>
|
||||
<ul class="dropdown-menu checks">
|
||||
@ -465,8 +467,9 @@ export default {
|
||||
</ul>
|
||||
</div>
|
||||
<button class="d-none d-xl-inline-block nav-link position-relative" id="nav-html-check-tab"
|
||||
data-bs-toggle="tab" data-bs-target="#nav-html-check" type="button" role="tab" aria-controls="nav-html"
|
||||
aria-selected="false" v-if="!mailbox.uiConfig.DisableHTMLCheck && message.HTML != ''">
|
||||
data-bs-toggle="tab" data-bs-target="#nav-html-check" type="button" role="tab"
|
||||
aria-controls="nav-html" aria-selected="false"
|
||||
v-if="!mailbox.uiConfig.DisableHTMLCheck && message.HTML != ''">
|
||||
HTML Check
|
||||
<span class="badge rounded-pill p-1" :class="htmlScoreColor" v-if="htmlScore !== false">
|
||||
<small>{{ Math.floor(htmlScore) }}%</small>
|
||||
@ -482,8 +485,8 @@ export default {
|
||||
</span>
|
||||
</button>
|
||||
<button class="d-none d-xl-inline-block nav-link position-relative" id="nav-spam-check-tab"
|
||||
data-bs-toggle="tab" data-bs-target="#nav-spam-check" type="button" role="tab" aria-controls="nav-html"
|
||||
aria-selected="false" v-if="mailbox.uiConfig.SpamAssassin">
|
||||
data-bs-toggle="tab" data-bs-target="#nav-spam-check" type="button" role="tab"
|
||||
aria-controls="nav-html" aria-selected="false" v-if="mailbox.uiConfig.SpamAssassin">
|
||||
Spam Analysis
|
||||
<span class="badge rounded-pill" :class="spamScoreColor" v-if="spamScore !== false">
|
||||
<small>{{ spamScore }}</small>
|
||||
@ -505,22 +508,25 @@ export default {
|
||||
<div v-if="message.HTML != ''" class="tab-pane fade show" id="nav-html" role="tabpanel"
|
||||
aria-labelledby="nav-html-tab" tabindex="0">
|
||||
<div id="responsive-view" :class="scaleHTMLPreview" :style="responsiveSizes[scaleHTMLPreview]">
|
||||
<iframe target-blank="" class="tab-pane d-block" id="preview-html" :srcdoc="sanitizeHTML(message.HTML)"
|
||||
v-on:load="resizeIframe" frameborder="0" style="width: 100%; height: 100%; background: #fff;">
|
||||
<iframe target-blank="" class="tab-pane d-block" id="preview-html"
|
||||
:srcdoc="sanitizeHTML(message.HTML)" v-on:load="resizeIframe" frameborder="0"
|
||||
style="width: 100%; height: 100%; background: #fff;">
|
||||
</iframe>
|
||||
</div>
|
||||
<Attachments v-if="allAttachments(message).length" :message="message"
|
||||
:attachments="allAttachments(message)"></Attachments>
|
||||
:attachments="allAttachments(message)">
|
||||
</Attachments>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-html-source" role="tabpanel" aria-labelledby="nav-html-source-tab"
|
||||
tabindex="0" v-if="message.HTML">
|
||||
<pre><code class="language-html">{{ message.HTML }}</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-plain-text" role="tabpanel" aria-labelledby="nav-plain-text-tab" tabindex="0"
|
||||
:class="message.HTML == '' ? 'show' : ''">
|
||||
<div class="tab-pane fade" id="nav-plain-text" role="tabpanel" aria-labelledby="nav-plain-text-tab"
|
||||
tabindex="0" :class="message.HTML == '' ? 'show' : ''">
|
||||
<div class="text-view" v-html="textToHTML(message.Text)"></div>
|
||||
<Attachments v-if="allAttachments(message).length" :message="message"
|
||||
:attachments="allAttachments(message)"></Attachments>
|
||||
:attachments="allAttachments(message)">
|
||||
</Attachments>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-headers" role="tabpanel" aria-labelledby="nav-headers-tab" tabindex="0">
|
||||
<Headers v-if="loadHeaders" :message="message"></Headers>
|
||||
@ -536,8 +542,8 @@ export default {
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-spam-check" role="tabpanel" aria-labelledby="nav-spam-check-tab"
|
||||
tabindex="0">
|
||||
<SpamAssassin v-if="mailbox.uiConfig.SpamAssassin" :message="message" @setSpamScore="(n) => spamScore = n"
|
||||
@set-badge-style="(v) => spamScoreColor = v" />
|
||||
<SpamAssassin v-if="mailbox.uiConfig.SpamAssassin" :message="message"
|
||||
@setSpamScore="(n) => spamScore = n" @set-badge-style="(v) => spamScoreColor = v" />
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-link-check" role="tabpanel" aria-labelledby="nav-html-check-tab"
|
||||
tabindex="0">
|
||||
|
@ -19,6 +19,7 @@ export const mailbox = reactive({
|
||||
appInfo: {}, // application information
|
||||
uiConfig: {}, // configuration for UI
|
||||
lastMessage: false, // return scrolling
|
||||
timeZone: '', // browser timezone
|
||||
})
|
||||
|
||||
watch(
|
||||
|
@ -59,6 +59,9 @@ export default {
|
||||
}
|
||||
|
||||
this.apiURI = this.resolve(`/api/v1/search`) + '?query=' + encodeURIComponent(s)
|
||||
if (mailbox.timeZone != '' && (s.indexOf('after:') != -1 || s.indexOf('before:') != -1)) {
|
||||
this.apiURI += '&tz=' + encodeURIComponent(mailbox.timeZone)
|
||||
}
|
||||
this.loadMessages()
|
||||
}
|
||||
}
|
||||
|
@ -550,6 +550,12 @@
|
||||
"description": "Limit results",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Timezone for `before:` \u0026 `after:` queries, eg: \"Pacific/Auckland\"",
|
||||
"name": "tz",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@ -773,8 +779,8 @@
|
||||
},
|
||||
"DatabaseSize": {
|
||||
"description": "Database size in bytes",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"LatestVersion": {
|
||||
"description": "Latest Mailpit version",
|
||||
@ -782,8 +788,8 @@
|
||||
},
|
||||
"Messages": {
|
||||
"description": "Total number of messages in the database",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"RuntimeStats": {
|
||||
"description": "Runtime statistics",
|
||||
@ -796,33 +802,33 @@
|
||||
},
|
||||
"MessagesDeleted": {
|
||||
"description": "Database runtime messages deleted",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"SMTPAccepted": {
|
||||
"description": "Accepted runtime SMTP messages",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"SMTPAcceptedSize": {
|
||||
"description": "Total runtime accepted messages size in bytes",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"SMTPIgnored": {
|
||||
"description": "Ignored runtime SMTP messages (when using --ignore-duplicate-ids)",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"SMTPRejected": {
|
||||
"description": "Rejected runtime SMTP messages",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"Uptime": {
|
||||
"description": "Mailpit server uptime in seconds",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -836,8 +842,8 @@
|
||||
},
|
||||
"Unread": {
|
||||
"description": "Total number of messages in the database",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"Version": {
|
||||
"description": "Current Mailpit version",
|
||||
@ -868,8 +874,8 @@
|
||||
},
|
||||
"Size": {
|
||||
"description": "Size in bytes",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
}
|
||||
},
|
||||
"x-go-package": "github.com/axllent/mailpit/internal/storage"
|
||||
@ -1176,8 +1182,8 @@
|
||||
},
|
||||
"Size": {
|
||||
"description": "Message size in bytes",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"Subject": {
|
||||
"description": "Message subject",
|
||||
@ -1268,8 +1274,8 @@
|
||||
},
|
||||
"Size": {
|
||||
"description": "Message size in bytes (total)",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"Snippet": {
|
||||
"description": "Message snippet includes up to 250 characters",
|
||||
@ -1310,8 +1316,8 @@
|
||||
},
|
||||
"messages_count": {
|
||||
"description": "Total number of messages matching current query",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"x-go-name": "MessagesCount"
|
||||
},
|
||||
"start": {
|
||||
@ -1330,14 +1336,14 @@
|
||||
},
|
||||
"total": {
|
||||
"description": "Total number of messages in mailbox",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"x-go-name": "Total"
|
||||
},
|
||||
"unread": {
|
||||
"description": "Total number of unread messages in mailbox",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"x-go-name": "Unread"
|
||||
}
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user