1
0
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:
Ralph Slooten 2024-04-12 15:19:46 +12:00
commit affe19beb5
44 changed files with 2105 additions and 906 deletions

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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 {

View File

@ -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")

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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."}
},

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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)
// 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
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)

View File

@ -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()
_, err = tx.Exec("DELETE FROM mailbox WHERE ID = ?", id)
if err != nil {
return err
args = make([]interface{}, len(toDelete))
for i, id := range toDelete {
args[i] = id
}
_, err = tx.Exec("DELETE FROM mailbox_data WHERE ID = ?", id)
if err != nil {
return err
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.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")
if err != nil {
return err
}
tables := []string{"mailbox", "mailbox_data", "tags", "message_tags"}
_, 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
for _, t := range tables {
sql := fmt.Sprintf(`DELETE FROM %s`, tenant(t)) // #nosec
_, err := tx.Exec(sql)
if err != nil {
return err
}
}
if err := tx.Commit(); err != nil {

View File

@ -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) {

View File

@ -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())
}
}

View File

@ -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(),

View File

@ -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
View 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())
}
}

View 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);

View 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);

View 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);

View File

@ -0,0 +1,2 @@
-- CREATE SNIPPET COLUMN
ALTER TABLE {{ tenant "mailbox" }} ADD COLUMN Snippet TEXT NOT NULL DEFAULT '';

View 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);

View 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" }}));

View 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>" }}`

View File

@ -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 {

View File

@ -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()

View File

@ -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())
}
}

View File

@ -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
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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.Total = stats.Total // total messages in mailbox
res.MessagesCount = results
res.Count = float64(len(messages)) // legacy - now undocumented in API specs
res.Total = stats.Total // total messages in mailbox
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,11 +437,9 @@ func DeleteMessages(w http.ResponseWriter, r *http.Request) {
return
}
} else {
for _, id := range data.IDs {
if err := storage.DeleteOneMessage(id); err != nil {
httpError(w, err.Error())
return
}
if err := storage.DeleteMessages(data.IDs); err != nil {
httpError(w, err.Error())
return
}
}

View File

@ -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"`

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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) {

View File

@ -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) {

View File

@ -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">
&lt;<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">

View File

@ -19,6 +19,7 @@ export const mailbox = reactive({
appInfo: {}, // application information
uiConfig: {}, // configuration for UI
lastMessage: false, // return scrolling
timeZone: '', // browser timezone
})
watch(

View File

@ -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()
}
}

View File

@ -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"
}
},