Compare commits

..

52 Commits

Author SHA1 Message Date
Ralph Slooten
fb2fe099b1 Merge branch 'release/v1.14.4' 2024-03-12 17:18:37 +13:00
Ralph Slooten
6879afb4a0 Release v1.14.4 2024-03-12 17:18:37 +13:00
Ralph Slooten
edc529fbde Chore: Update caniemail test data 2024-03-12 17:11:43 +13:00
Ralph Slooten
a324d817b3 Feature: Allow setting SMTP relay configuration values via environment variables (#262) 2024-03-12 17:10:13 +13:00
Ralph Slooten
053779c656 Chore: Reorder CLI flags to group by related functionality 2024-03-12 17:10:07 +13:00
Ralph Slooten
ddf2227397 Merge branch 'release/v1.14.3' 2024-03-10 18:47:08 +13:00
Ralph Slooten
bd892e3a48 Release v1.14.3 2024-03-10 18:47:08 +13:00
Ralph Slooten
b6454c902c Chore: Update node dependencies 2024-03-10 18:45:49 +13:00
Ralph Slooten
25f8a47c73 Chore: Update Go dependencies 2024-03-10 18:43:50 +13:00
Ralph Slooten
28710d0462 Fix: Prevent crash when calculating deleted space percentage (divide by zero) 2024-03-10 18:41:27 +13:00
Ralph Slooten
cf18f529f4 Merge tag 'v1.14.2' into develop
Release v1.14.2
2024-03-10 08:11:41 +13:00
Ralph Slooten
c1b03212d5 Merge branch 'release/v1.14.2' 2024-03-10 08:11:35 +13:00
Ralph Slooten
026d676901 Release v1.14.2 2024-03-10 08:11:31 +13:00
Ralph Slooten
e660d6bedd Chore: Allow setting of multiple message tags via plus addresses (#253) 2024-03-10 08:05:11 +13:00
Ralph Slooten
d1d0ce4737 Fix: Prevent runtime error when calculating total messages size of empty table (#263) 2024-03-10 07:48:44 +13:00
Ralph Slooten
bdea197a0f Merge tag 'v1.14.1' into develop
Release v1.14.1
2024-03-02 23:12:34 +13:00
Ralph Slooten
9c9530081c Merge branch 'release/v1.14.1' 2024-03-02 23:12:31 +13:00
Ralph Slooten
ed8cac2454 Release v1.14.1 2024-03-02 23:12:29 +13:00
Ralph Slooten
3bbed37907 Add edge Docker hash 2024-03-02 23:02:47 +13:00
Ralph Slooten
4fa8014735 Fix: Handle null values in Mailpit settings, set DeletedSize=0 if null 2024-03-02 22:51:30 +13:00
Ralph Slooten
23b1261cf9 Chore: Tag names now allow . and must be a minimum of 1 character 2024-03-02 22:51:30 +13:00
Ralph Slooten
85473762c5 Update go-release-action 2024-03-02 22:51:29 +13:00
Ralph Slooten
f076d52603 Chore: Update node dependencies 2024-03-02 22:51:29 +13:00
Ralph Slooten
cf93f99cc2 Chore: Update Go dependencies 2024-03-02 22:51:28 +13:00
Ralph Slooten
0f725ef1d8 Feature: Option to enforce TitleCasing for all newly created tags 2024-03-01 17:22:13 +13:00
Ralph Slooten
0353520aeb Feature: Set message tags using plus addressing (#253) 2024-03-01 17:21:21 +13:00
Ralph Slooten
bfd5837710 Update README 2024-02-24 23:47:03 +13:00
Ralph Slooten
321bc338e6 Merge tag 'v1.14.0' into develop
Release v1.14.0
2024-02-24 23:27:04 +13:00
Ralph Slooten
75a6cfb31c Merge branch 'release/v1.14.0' 2024-02-24 23:26:59 +13:00
Ralph Slooten
7cb71ad5bf Release v1.14.0 2024-02-24 23:26:57 +13:00
Ralph Slooten
9892375366 Chore: Update node dependencies 2024-02-24 23:20:27 +13:00
Ralph Slooten
e55d4aab59 Chore: Update Go dependencies 2024-02-24 23:16:04 +13:00
Ralph Slooten
d521eca2d1 Merge branch 'feature/pop3' into develop 2024-02-24 23:11:38 +13:00
Ralph Slooten
e8c306b7ab Update README 2024-02-24 23:10:58 +13:00
Ralph Slooten
f548bbb874 Feature: Optional POP3 server (#249)
Originally requested in #72
2024-02-24 23:10:48 +13:00
Ralph Slooten
f067b76c58 Update cron logic 2024-02-17 23:19:32 +13:00
Ralph Slooten
5458b1044f Docker: Add edge Docker images for latest unreleased features 2024-02-17 22:48:59 +13:00
Ralph Slooten
294f9a21e6 Chore: Refactor storage library 2024-02-17 22:36:32 +13:00
Ralph Slooten
26a2095674 Chore: Security improvements (gosec) 2024-02-17 12:38:30 +13:00
Ralph Slooten
b2a0d73572 Chore: Switch to short uuid format for database IDs 2024-02-17 11:48:42 +13:00
Ralph Slooten
400d5a36c1 Chore: Better handling of automatic database compression (vacuuming) after deleting messages 2024-02-17 11:12:37 +13:00
Ralph Slooten
9861bf96e1 Merge tag 'v1.13.3' into develop
Release v1.13.3
2024-02-09 23:22:13 +13:00
Ralph Slooten
e410fd42dc Merge branch 'release/v1.13.3' 2024-02-09 23:21:58 +13:00
Ralph Slooten
d049cb627f Release v1.13.3 2024-02-09 23:21:57 +13:00
Ralph Slooten
a70d9abdf2 Chore: Update node dependencies 2024-02-09 23:14:32 +13:00
Ralph Slooten
d75efb8181 Chore: Update Go dependencies 2024-02-09 23:11:45 +13:00
Ralph Slooten
a856ce0cfa Merge branch 'feature/reply-to' into develop 2024-02-09 23:09:46 +13:00
Ralph Slooten
5d9aba726e Feature: Add reply-to:<search> search filter (#247) 2024-02-09 23:09:14 +13:00
Ralph Slooten
667218b30b API: Include Reply-To information in message summaries for message list & websocket events 2024-02-09 23:08:34 +13:00
Ralph Slooten
522733f537 Chore: Compress database only when >= 1% of total message size has been deleted 2024-02-05 23:56:10 +13:00
Ralph Slooten
848ce11a69 Chore: Update "About" modal layout when new version is available 2024-02-05 22:55:49 +13:00
Ralph Slooten
2d44159ecc Merge tag 'v1.13.2' into develop
Release v1.13.2
2024-02-05 22:33:50 +13:00
44 changed files with 2929 additions and 1614 deletions

34
.github/workflows/build-docker-edge.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
on:
push:
branches: [ develop ]
name: Build docker edge images
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/386,linux/amd64,linux/arm64
build-args: |
"VERSION=edge-${{ github.sha }}"
push: true
tags: |
axllent/mailpit:edge

View File

@@ -33,7 +33,7 @@ jobs:
- run: npm run package
# build the binaries
- uses: wangyoucao577/go-release-action@v1.46
- uses: wangyoucao577/go-release-action@v1.49
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}

View File

@@ -2,6 +2,82 @@
Notable changes to Mailpit will be documented in this file.
## [v1.14.4]
### Chore
- Update caniemail test data
- Reorder CLI flags to group by related functionality
### Feature
- Allow setting SMTP relay configuration values via environment variables ([#262](https://github.com/axllent/mailpit/issues/262))
## [v1.14.3]
### Chore
- Update node dependencies
- Update Go dependencies
### Fix
- Prevent crash when calculating deleted space percentage (divide by zero)
## [v1.14.2]
### Chore
- Allow setting of multiple message tags via plus addresses ([#253](https://github.com/axllent/mailpit/issues/253))
### Fix
- Prevent runtime error when calculating total messages size of empty table ([#263](https://github.com/axllent/mailpit/issues/263))
## [v1.14.1]
### Chore
- Tag names now allow `.` and must be a minimum of 1 character
- Update node dependencies
- Update Go dependencies
### Feature
- Option to enforce TitleCasing for all newly created tags
- Set message tags using plus addressing ([#253](https://github.com/axllent/mailpit/issues/253))
### Fix
- Handle null values in Mailpit settings, set DeletedSize=0 if null
## [v1.14.0]
### Chore
- Update node dependencies
- Update Go dependencies
- Refactor storage library
- Security improvements (gosec)
- Switch to short uuid format for database IDs
- Better handling of automatic database compression (vacuuming) after deleting messages
### Docker
- Add edge Docker images for latest unreleased features
### Feature
- Optional POP3 server ([#249](https://github.com/axllent/mailpit/issues/249))
## [v1.13.3]
### API
- Include Reply-To information in message summaries for message list & websocket events
### Chore
- Update node dependencies
- Update Go dependencies
- Compress database only when >= 1% of total message size has been deleted
- Update "About" modal layout when new version is available
### Feature
- Add reply-to:<search> search filter ([#247](https://github.com/axllent/mailpit/issues/247))
## [v1.13.2]
### Chore

View File

@@ -16,6 +16,6 @@ COPY --from=builder /mailpit /mailpit
RUN apk add --no-cache tzdata
EXPOSE 1025/tcp 8025/tcp
EXPOSE 1025/tcp 1110/tcp 8025/tcp
ENTRYPOINT ["/mailpit"]

View File

@@ -50,8 +50,9 @@ via either HTTPS or `localhost` only)
"accept any" mode)
- [SMTP relaying](https://mailpit.axllent.org/docs/configuration/smtp-relay/) (message release) - relay messages via a different SMTP server
including an optional allowlist of accepted recipients
- Fast SMTP processing & storing - approximately 70-100 emails per second depending on CPU, network speed & email size,
- Fast SMTP processing & storing - ingesting 100-200 emails per second depending on CPU, network speed & email size,
easily handling tens of thousands of emails
- Optional [POP3 server](https://mailpit.axllent.org/docs/configuration/pop3/) to download captured message directly into your email client
- Configurable automatic email pruning (default keeps the most recent 500 emails)
- A simple [REST API](https://mailpit.axllent.org/docs/api-v1/) for integration testing
- Optional [webhook](https://mailpit.axllent.org/docs/integration/webhook/) for received messages

View File

@@ -10,6 +10,7 @@ import (
"github.com/axllent/mailpit/internal/auth"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/storage"
"github.com/axllent/mailpit/internal/tools"
"github.com/axllent/mailpit/server"
"github.com/axllent/mailpit/server/smtpd"
"github.com/axllent/mailpit/server/webhook"
@@ -78,25 +79,31 @@ func init() {
// load and warn deprecated ENV vars
initDeprecatedConfigFromEnv()
// load ENV vars
// load environment variables
initConfigFromEnv()
rootCmd.Flags().StringVarP(&config.DataFile, "db-file", "d", config.DataFile, "Database file to store persistent data")
rootCmd.Flags().StringVarP(&config.SMTPListen, "smtp", "s", config.SMTPListen, "SMTP bind interface and port")
rootCmd.Flags().StringVarP(&config.HTTPListen, "listen", "l", config.HTTPListen, "HTTP bind interface and port for UI")
rootCmd.Flags().IntVarP(&config.MaxMessages, "max", "m", config.MaxMessages, "Max number of messages to store")
rootCmd.Flags().StringVar(&config.Webroot, "webroot", config.Webroot, "Set the webroot for web UI & API")
rootCmd.Flags().StringVar(&server.AccessControlAllowOrigin, "api-cors", server.AccessControlAllowOrigin, "Set API CORS Access-Control-Allow-Origin header")
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)")
rootCmd.Flags().BoolVar(&config.DisableHTMLCheck, "disable-html-check", config.DisableHTMLCheck, "Disable the HTML check functionality (web UI & API)")
rootCmd.Flags().BoolVar(&config.BlockRemoteCSSAndFonts, "block-remote-css-and-fonts", config.BlockRemoteCSSAndFonts, "Block access to remote CSS & fonts")
rootCmd.Flags().StringVar(&config.EnableSpamAssassin, "enable-spamassassin", config.EnableSpamAssassin, "Enable integration with SpamAssassin")
rootCmd.Flags().StringVar(&logger.LogFile, "log-file", logger.LogFile, "Log output to file instead of stdout")
rootCmd.Flags().BoolVarP(&logger.QuietLogging, "quiet", "q", logger.QuietLogging, "Quiet logging (errors only)")
rootCmd.Flags().BoolVarP(&logger.VerboseLogging, "verbose", "v", logger.VerboseLogging, "Verbose logging")
// Web UI / API
rootCmd.Flags().StringVarP(&config.HTTPListen, "listen", "l", config.HTTPListen, "HTTP bind interface and port for UI")
rootCmd.Flags().StringVar(&config.Webroot, "webroot", config.Webroot, "Set the webroot for web UI & API")
rootCmd.Flags().StringVar(&config.UIAuthFile, "ui-auth-file", config.UIAuthFile, "A password file for web UI & API authentication")
rootCmd.Flags().StringVar(&config.UITLSCert, "ui-tls-cert", config.UITLSCert, "TLS certificate for web UI (HTTPS) - requires ui-tls-key")
rootCmd.Flags().StringVar(&config.UITLSKey, "ui-tls-key", config.UITLSKey, "TLS key for web UI (HTTPS) - requires ui-tls-cert")
rootCmd.Flags().StringVar(&server.AccessControlAllowOrigin, "api-cors", server.AccessControlAllowOrigin, "Set API CORS Access-Control-Allow-Origin header")
rootCmd.Flags().BoolVar(&config.DisableHTMLCheck, "disable-html-check", config.DisableHTMLCheck, "Disable the HTML check functionality (web UI & API)")
rootCmd.Flags().BoolVar(&config.BlockRemoteCSSAndFonts, "block-remote-css-and-fonts", config.BlockRemoteCSSAndFonts, "Block access to remote CSS & fonts")
rootCmd.Flags().StringVar(&config.EnableSpamAssassin, "enable-spamassassin", config.EnableSpamAssassin, "Enable integration with SpamAssassin")
rootCmd.Flags().BoolVar(&config.AllowUntrustedTLS, "allow-untrusted-tls", config.AllowUntrustedTLS, "Do not verify HTTPS certificates (link checker & screenshots)")
// SMTP server
rootCmd.Flags().StringVarP(&config.SMTPListen, "smtp", "s", config.SMTPListen, "SMTP bind interface and port")
rootCmd.Flags().StringVar(&config.SMTPAuthFile, "smtp-auth-file", config.SMTPAuthFile, "A password file for SMTP authentication")
rootCmd.Flags().BoolVar(&config.SMTPAuthAcceptAny, "smtp-auth-accept-any", config.SMTPAuthAcceptAny, "Accept any SMTP username and password, including none")
rootCmd.Flags().StringVar(&config.SMTPTLSCert, "smtp-tls-cert", config.SMTPTLSCert, "TLS certificate for SMTP (STARTTLS) - requires smtp-tls-key")
@@ -108,19 +115,25 @@ func init() {
rootCmd.Flags().StringVar(&config.SMTPAllowedRecipients, "smtp-allowed-recipients", config.SMTPAllowedRecipients, "Only allow SMTP recipients matching a regular expression (default allow all)")
rootCmd.Flags().BoolVar(&smtpd.DisableReverseDNS, "smtp-disable-rdns", smtpd.DisableReverseDNS, "Disable SMTP reverse DNS lookups")
// SMTP relay
rootCmd.Flags().StringVar(&config.SMTPRelayConfigFile, "smtp-relay-config", config.SMTPRelayConfigFile, "SMTP configuration file to allow releasing messages")
rootCmd.Flags().BoolVar(&config.SMTPRelayAllIncoming, "smtp-relay-all", config.SMTPRelayAllIncoming, "Relay all incoming messages via external SMTP server (caution!)")
// POP3 server
rootCmd.Flags().StringVar(&config.POP3Listen, "pop3", config.POP3Listen, "POP3 server bind interface and port")
rootCmd.Flags().StringVar(&config.POP3AuthFile, "pop3-auth-file", config.POP3AuthFile, "A password file for POP3 server authentication (enables POP3 server)")
rootCmd.Flags().StringVar(&config.POP3TLSCert, "pop3-tls-cert", config.POP3TLSCert, "Optional TLS certificate for POP3 server - requires pop3-tls-key")
rootCmd.Flags().StringVar(&config.POP3TLSKey, "pop3-tls-key", config.POP3TLSKey, "Optional TLS key for POP3 server - requires pop3-tls-cert")
// Tagging
rootCmd.Flags().StringVarP(&config.SMTPCLITags, "tag", "t", config.SMTPCLITags, "Tag new messages matching filters")
rootCmd.Flags().BoolVar(&tools.TagsTitleCase, "tags-title-case", tools.TagsTitleCase, "Convert new tags automatically to TitleCase")
// Webhook
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")
rootCmd.Flags().BoolVar(&config.AllowUntrustedTLS, "allow-untrusted-tls", config.AllowUntrustedTLS, "Do not verify HTTPS certificates (link checker & screenshots)")
rootCmd.Flags().StringVarP(&config.SMTPCLITags, "tag", "t", config.SMTPCLITags, "Tag new messages matching filters")
rootCmd.Flags().StringVar(&logger.LogFile, "log-file", logger.LogFile, "Log output to file instead of stdout")
rootCmd.Flags().BoolVarP(&logger.QuietLogging, "quiet", "q", logger.QuietLogging, "Quiet logging (errors only)")
rootCmd.Flags().BoolVarP(&logger.VerboseLogging, "verbose", "v", logger.VerboseLogging, "Verbose logging")
// deprecated flags 2023/03/12
// 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")
rootCmd.Flags().StringVar(&config.SMTPTLSCert, "smtp-ssl-cert", config.SMTPTLSCert, "SSL certificate for SMTP - requires smtp-ssl-key")
@@ -137,38 +150,72 @@ func init() {
// Load settings from environment
func initConfigFromEnv() {
// inherit from environment if provided
// General
config.DataFile = os.Getenv("MP_DATA_FILE")
if len(os.Getenv("MP_SMTP_BIND_ADDR")) > 0 {
config.SMTPListen = os.Getenv("MP_SMTP_BIND_ADDR")
}
if len(os.Getenv("MP_UI_BIND_ADDR")) > 0 {
config.HTTPListen = os.Getenv("MP_UI_BIND_ADDR")
}
if len(os.Getenv("MP_MAX_MESSAGES")) > 0 {
config.MaxMessages, _ = strconv.Atoi(os.Getenv("MP_MAX_MESSAGES"))
}
if len(os.Getenv("MP_TAG")) > 0 {
config.SMTPCLITags = os.Getenv("MP_TAG")
if getEnabledFromEnv("MP_USE_MESSAGE_DATES") {
config.UseMessageDates = true
}
if getEnabledFromEnv("MP_IGNORE_DUPLICATE_IDS") {
config.IgnoreDuplicateIDs = true
}
if len(os.Getenv("MP_LOG_FILE")) > 0 {
logger.LogFile = os.Getenv("MP_LOG_FILE")
}
if getEnabledFromEnv("MP_QUIET") {
logger.QuietLogging = true
}
if getEnabledFromEnv("MP_VERBOSE") {
logger.VerboseLogging = true
}
// UI
// Web UI & API
if len(os.Getenv("MP_UI_BIND_ADDR")) > 0 {
config.HTTPListen = os.Getenv("MP_UI_BIND_ADDR")
}
if len(os.Getenv("MP_WEBROOT")) > 0 {
config.Webroot = os.Getenv("MP_WEBROOT")
}
config.UIAuthFile = os.Getenv("MP_UI_AUTH_FILE")
auth.SetUIAuth(os.Getenv("MP_UI_AUTH"))
if err := auth.SetUIAuth(os.Getenv("MP_UI_AUTH")); err != nil {
logger.Log().Errorf(err.Error())
}
config.UITLSCert = os.Getenv("MP_UI_TLS_CERT")
config.UITLSKey = os.Getenv("MP_UI_TLS_KEY")
if len(os.Getenv("MP_API_CORS")) > 0 {
server.AccessControlAllowOrigin = os.Getenv("MP_API_CORS")
}
if getEnabledFromEnv("MP_DISABLE_HTML_CHECK") {
config.DisableHTMLCheck = true
}
if getEnabledFromEnv("MP_BLOCK_REMOTE_CSS_AND_FONTS") {
config.BlockRemoteCSSAndFonts = true
}
if len(os.Getenv("MP_ENABLE_SPAMASSASSIN")) > 0 {
config.EnableSpamAssassin = os.Getenv("MP_ENABLE_SPAMASSASSIN")
}
if getEnabledFromEnv("MP_ALLOW_UNTRUSTED_TLS") {
config.AllowUntrustedTLS = true
}
// SMTP
// SMTP server
if len(os.Getenv("MP_SMTP_BIND_ADDR")) > 0 {
config.SMTPListen = os.Getenv("MP_SMTP_BIND_ADDR")
}
config.SMTPAuthFile = os.Getenv("MP_SMTP_AUTH_FILE")
auth.SetSMTPAuth(os.Getenv("MP_SMTP_AUTH"))
if err := auth.SetSMTPAuth(os.Getenv("MP_SMTP_AUTH")); err != nil {
logger.Log().Errorf(err.Error())
}
if getEnabledFromEnv("MP_SMTP_AUTH_ACCEPT_ANY") {
config.SMTPAuthAcceptAny = true
}
config.SMTPTLSCert = os.Getenv("MP_SMTP_TLS_CERT")
config.SMTPTLSKey = os.Getenv("MP_SMTP_TLS_KEY")
if getEnabledFromEnv("MP_SMTP_TLS_REQUIRED") {
config.SMTPTLSRequired = true
}
if getEnabledFromEnv("MP_SMTP_AUTH_ACCEPT_ANY") {
config.SMTPAuthAcceptAny = true
}
if getEnabledFromEnv("MP_SMTP_AUTH_ALLOW_INSECURE") {
config.SMTPAuthAllowInsecure = true
}
@@ -185,11 +232,43 @@ func initConfigFromEnv() {
smtpd.DisableReverseDNS = true
}
// Relay server config
// SMTP relay
config.SMTPRelayConfigFile = os.Getenv("MP_SMTP_RELAY_CONFIG")
if getEnabledFromEnv("MP_SMTP_RELAY_ALL") {
config.SMTPRelayAllIncoming = true
}
config.SMTPRelayConfig = config.SMTPRelayConfigStruct{}
config.SMTPRelayConfig.Host = os.Getenv("MP_SMTP_RELAY_HOST")
if len(os.Getenv("MP_SMTP_RELAY_PORT")) > 0 {
config.SMTPRelayConfig.Port, _ = strconv.Atoi(os.Getenv("MP_SMTP_RELAY_PORT"))
}
config.SMTPRelayConfig.STARTTLS = getEnabledFromEnv("MP_SMTP_RELAY_STARTTLS")
config.SMTPRelayConfig.AllowInsecure = getEnabledFromEnv("MP_SMTP_RELAY_ALLOW_INSECURE")
config.SMTPRelayConfig.Auth = os.Getenv("MP_SMTP_RELAY_AUTH")
config.SMTPRelayConfig.Username = os.Getenv("MP_SMTP_RELAY_USERNAME")
config.SMTPRelayConfig.Password = os.Getenv("MP_SMTP_RELAY_PASSWORD")
config.SMTPRelayConfig.Secret = os.Getenv("MP_SMTP_RELAY_SECRET")
config.SMTPRelayConfig.ReturnPath = os.Getenv("MP_SMTP_RELAY_RETURN_PATH")
config.SMTPRelayConfig.AllowedRecipients = os.Getenv("MP_SMTP_RELAY_ALLOWED_RECIPIENTS")
// POP3 server
if len(os.Getenv("MP_POP3_BIND_ADDR")) > 0 {
config.POP3Listen = os.Getenv("MP_POP3_BIND_ADDR")
}
config.POP3AuthFile = os.Getenv("MP_POP3_AUTH_FILE")
if err := auth.SetPOP3Auth(os.Getenv("MP_POP3_AUTH")); err != nil {
logger.Log().Errorf(err.Error())
}
config.POP3TLSCert = os.Getenv("MP_POP3_TLS_CERT")
config.POP3TLSKey = os.Getenv("MP_POP3_TLS_KEY")
// Tagging
if len(os.Getenv("MP_TAG")) > 0 {
config.SMTPCLITags = os.Getenv("MP_TAG")
}
if getEnabledFromEnv("MP_TAGS_TITLE_CASE") {
tools.TagsTitleCase = getEnabledFromEnv("MP_TAGS_TITLE_CASE")
}
// Webhook
if len(os.Getenv("MP_WEBHOOK_URL")) > 0 {
@@ -198,41 +277,6 @@ func initConfigFromEnv() {
if len(os.Getenv("MP_WEBHOOK_LIMIT")) > 0 {
webhook.RateLimit, _ = strconv.Atoi(os.Getenv("MP_WEBHOOK_LIMIT"))
}
// Misc options
if len(os.Getenv("MP_WEBROOT")) > 0 {
config.Webroot = os.Getenv("MP_WEBROOT")
}
if len(os.Getenv("MP_API_CORS")) > 0 {
server.AccessControlAllowOrigin = os.Getenv("MP_API_CORS")
}
if getEnabledFromEnv("MP_USE_MESSAGE_DATES") {
config.UseMessageDates = true
}
if getEnabledFromEnv("MP_IGNORE_DUPLICATE_IDS") {
config.IgnoreDuplicateIDs = true
}
if getEnabledFromEnv("MP_DISABLE_HTML_CHECK") {
config.DisableHTMLCheck = true
}
if getEnabledFromEnv("MP_BLOCK_REMOTE_CSS_AND_FONTS") {
config.BlockRemoteCSSAndFonts = true
}
if len(os.Getenv("MP_ENABLE_SPAMASSASSIN")) > 0 {
config.EnableSpamAssassin = os.Getenv("MP_ENABLE_SPAMASSASSIN")
}
if getEnabledFromEnv("MP_ALLOW_UNTRUSTED_TLS") {
config.AllowUntrustedTLS = true
}
if len(os.Getenv("MP_LOG_FILE")) > 0 {
logger.LogFile = os.Getenv("MP_LOG_FILE")
}
if getEnabledFromEnv("MP_QUIET") {
logger.QuietLogging = true
}
if getEnabledFromEnv("MP_VERBOSE") {
logger.VerboseLogging = true
}
}
// load deprecated settings from environment and warn

View File

@@ -4,6 +4,7 @@ package config
import (
"errors"
"fmt"
"net"
"net/url"
"os"
"path"
@@ -84,7 +85,7 @@ var (
SMTPCLITags string
// ValidTagRegexp represents a valid tag
ValidTagRegexp = regexp.MustCompile(`^([a-zA-Z0-9\-\ \_]){3,}$`)
ValidTagRegexp = regexp.MustCompile(`^([a-zA-Z0-9\-\ \_\.]){1,}$`)
// SMTPTags are expressions to apply tags to new mail
SMTPTags []AutoTag
@@ -93,7 +94,7 @@ var (
SMTPRelayConfigFile string
// SMTPRelayConfig to parse a yaml file and store config of relay SMTP server
SMTPRelayConfig smtpRelayConfigStruct
SMTPRelayConfig SMTPRelayConfigStruct
// SMTPStrictRFCHeaders will return an error if the email headers contain <CR><CR><LF> (\r\r\n)
// @see https://github.com/axllent/mailpit/issues/87 & https://github.com/axllent/mailpit/issues/153
@@ -112,6 +113,18 @@ var (
// Use with extreme caution!
SMTPRelayAllIncoming = false
// POP3Listen address - if set then Mailpit will start the POP3 server and listen on this address
POP3Listen = "[::]:1110"
// POP3AuthFile for POP3 authentication
POP3AuthFile string
// POP3TLSCert TLS certificate
POP3TLSCert string
// POP3TLSKey TLS certificate key
POP3TLSKey string
// EnableSpamAssassin must be either <host>:<port> or "postmark"
EnableSpamAssassin string
@@ -141,18 +154,20 @@ type AutoTag struct {
}
// SMTPRelayConfigStruct struct for parsing yaml & storing variables
type smtpRelayConfigStruct struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
STARTTLS bool `yaml:"starttls"`
AllowInsecure bool `yaml:"allow-insecure"`
Auth string `yaml:"auth"` // none, plain, login, cram-md5
Username string `yaml:"username"` // plain & cram-md5
Password string `yaml:"password"` // plain
Secret string `yaml:"secret"` // cram-md5
ReturnPath string `yaml:"return-path"` // allow overriding the bounce address
RecipientAllowlist string `yaml:"recipient-allowlist"` // regex, if set needs to match for mails to be relayed
RecipientAllowlistRegexp *regexp.Regexp
type SMTPRelayConfigStruct struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
STARTTLS bool `yaml:"starttls"`
AllowInsecure bool `yaml:"allow-insecure"`
Auth string `yaml:"auth"` // none, plain, login, cram-md5
Username string `yaml:"username"` // plain & cram-md5
Password string `yaml:"password"` // plain
Secret string `yaml:"secret"` // cram-md5
ReturnPath string `yaml:"return-path"` // allow overriding the bounce address
AllowedRecipients string `yaml:"allowed-recipients"` // regex, if set needs to match for mails to be relayed
AllowedRecipientsRegexp *regexp.Regexp // compiled regexp using AllowedRecipients
// DEPRECATED 2024/03/12
RecipientAllowlist string `yaml:"recipient-allowlist"`
}
// VerifyConfig wil do some basic checking
@@ -179,13 +194,17 @@ func VerifyConfig() error {
}
if UIAuthFile != "" {
UIAuthFile = filepath.Clean(UIAuthFile)
if !isFile(UIAuthFile) {
return fmt.Errorf("[ui] HTTP password file not found: %s", UIAuthFile)
}
b, err := os.ReadFile(UIAuthFile)
if err != nil {
return err
}
if err := auth.SetUIAuth(string(b)); err != nil {
return err
}
@@ -196,6 +215,9 @@ func VerifyConfig() error {
}
if UITLSCert != "" {
UITLSCert = filepath.Clean(UITLSCert)
UITLSKey = filepath.Clean(UITLSKey)
if !isFile(UITLSCert) {
return fmt.Errorf("[ui] TLS certificate not found: %s", UITLSCert)
}
@@ -210,6 +232,9 @@ func VerifyConfig() error {
}
if SMTPTLSCert != "" {
SMTPTLSCert = filepath.Clean(SMTPTLSCert)
SMTPTLSKey = filepath.Clean(SMTPTLSKey)
if !isFile(SMTPTLSCert) {
return fmt.Errorf("[smtp] TLS certificate not found: %s", SMTPTLSCert)
}
@@ -226,6 +251,8 @@ func VerifyConfig() error {
}
if SMTPAuthFile != "" {
SMTPAuthFile = filepath.Clean(SMTPAuthFile)
if !isFile(SMTPAuthFile) {
return fmt.Errorf("[smtp] password file not found: %s", SMTPAuthFile)
}
@@ -248,6 +275,46 @@ func VerifyConfig() error {
return errors.New("[smtp] authentication requires TLS encryption, run with `--smtp-auth-allow-insecure` to allow insecure authentication")
}
// POP3 server
if POP3TLSCert != "" {
POP3TLSCert = filepath.Clean(POP3TLSCert)
POP3TLSKey = filepath.Clean(POP3TLSKey)
if !isFile(POP3TLSCert) {
return fmt.Errorf("[pop3] TLS certificate not found: %s", POP3TLSCert)
}
if !isFile(POP3TLSKey) {
return fmt.Errorf("[pop3] TLS key not found: %s", POP3TLSKey)
}
}
if POP3TLSCert != "" && POP3TLSKey == "" || POP3TLSCert == "" && POP3TLSKey != "" {
return errors.New("[pop3] You must provide both a POP3 TLS certificate and a key")
}
if POP3Listen != "" {
_, err := net.ResolveTCPAddr("tcp", POP3Listen)
if err != nil {
return fmt.Errorf("[pop3] %s", err.Error())
}
}
if POP3AuthFile != "" {
POP3AuthFile = filepath.Clean(POP3AuthFile)
if !isFile(POP3AuthFile) {
return fmt.Errorf("[pop3] password file not found: %s", POP3AuthFile)
}
b, err := os.ReadFile(POP3AuthFile)
if err != nil {
return err
}
if err := auth.SetPOP3Auth(string(b)); err != nil {
return err
}
}
// Web root
validWebrootRe := regexp.MustCompile(`[^0-9a-zA-Z\/\-\_\.@]`)
if validWebrootRe.MatchString(Webroot) {
return fmt.Errorf("invalid characters in Webroot (%s). Valid chars include: [a-z A-Z 0-9 _ . - / @]", Webroot)
@@ -306,6 +373,11 @@ func VerifyConfig() error {
return err
}
// separate relay config validation to account for environment variables
if err := validateRelayConfig(); err != nil {
return err
}
if !ReleaseEnabled && SMTPRelayAllIncoming {
return errors.New("[smtp] relay config must be set to relay all messages")
}
@@ -318,14 +390,16 @@ func VerifyConfig() error {
return nil
}
// Parse & validate the SMTPRelayConfigFile (if set)
// Parse the SMTPRelayConfigFile (if set)
func parseRelayConfig(c string) error {
if c == "" {
return nil
}
c = filepath.Clean(c)
if !isFile(c) {
return fmt.Errorf("[smtp] relay configuration not found: %s", SMTPRelayConfigFile)
return fmt.Errorf("[smtp] relay configuration not found: %s", c)
}
data, err := os.ReadFile(c)
@@ -341,6 +415,23 @@ func parseRelayConfig(c string) error {
return errors.New("[smtp] relay host not set")
}
// DEPRECATED 2024/03/12
if SMTPRelayConfig.RecipientAllowlist != "" {
logger.Log().Warn("[smtp] relay 'recipient-allowlist' is deprecated, use 'allowed_recipients' instead")
if SMTPRelayConfig.AllowedRecipients == "" {
SMTPRelayConfig.AllowedRecipients = SMTPRelayConfig.RecipientAllowlist
}
}
return nil
}
// Validate the SMTPRelayConfig (if Host is set)
func validateRelayConfig() error {
if SMTPRelayConfig.Host == "" {
return nil
}
if SMTPRelayConfig.Port == 0 {
SMTPRelayConfig.Port = 25 // default
}
@@ -351,17 +442,17 @@ func parseRelayConfig(c string) error {
SMTPRelayConfig.Auth = "none"
} else if SMTPRelayConfig.Auth == "plain" {
if SMTPRelayConfig.Username == "" || SMTPRelayConfig.Password == "" {
return fmt.Errorf("[smtp] relay host username or password not set for PLAIN authentication (%s)", c)
return fmt.Errorf("[smtp] relay host username or password not set for PLAIN authentication")
}
} else if SMTPRelayConfig.Auth == "login" {
SMTPRelayConfig.Auth = "login"
if SMTPRelayConfig.Username == "" || SMTPRelayConfig.Password == "" {
return fmt.Errorf("[smtp] relay host username or password not set for LOGIN authentication (%s)", c)
return fmt.Errorf("[smtp] relay host username or password not set for LOGIN authentication")
}
} else if strings.HasPrefix(SMTPRelayConfig.Auth, "cram") {
SMTPRelayConfig.Auth = "cram-md5"
if SMTPRelayConfig.Username == "" || SMTPRelayConfig.Secret == "" {
return fmt.Errorf("[smtp] relay host username or secret not set for CRAM-MD5 authentication (%s)", c)
return fmt.Errorf("[smtp] relay host username or secret not set for CRAM-MD5 authentication")
}
} else {
return fmt.Errorf("[smtp] relay authentication method not supported: %s", SMTPRelayConfig.Auth)
@@ -371,15 +462,15 @@ func parseRelayConfig(c string) error {
logger.Log().Infof("[smtp] enabling message relaying via %s:%d", SMTPRelayConfig.Host, SMTPRelayConfig.Port)
allowlistRegexp, err := regexp.Compile(SMTPRelayConfig.RecipientAllowlist)
allowlistRegexp, err := regexp.Compile(SMTPRelayConfig.AllowedRecipients)
if SMTPRelayConfig.RecipientAllowlist != "" {
if SMTPRelayConfig.AllowedRecipients != "" {
if err != nil {
return fmt.Errorf("[smtp] failed to compile relay recipient allowlist regexp: %s", err.Error())
}
SMTPRelayConfig.RecipientAllowlistRegexp = allowlistRegexp
logger.Log().Infof("[smtp] relay recipient allowlist is active with the following regexp: %s", SMTPRelayConfig.RecipientAllowlist)
SMTPRelayConfig.AllowedRecipientsRegexp = allowlistRegexp
logger.Log().Infof("[smtp] relay recipient allowlist is active with the following regexp: %s", SMTPRelayConfig.AllowedRecipients)
}

31
go.mod
View File

@@ -4,16 +4,16 @@ go 1.20
require (
github.com/GuiaBolso/darwin v0.0.0-20191218124601-fd6d2aa3d244
github.com/PuerkitoBio/goquery v1.8.1
github.com/PuerkitoBio/goquery v1.9.1
github.com/axllent/semver v0.0.1
github.com/disintegration/imaging v1.6.2
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.1
github.com/jhillyerd/enmime v1.1.0
github.com/klauspost/compress v1.17.5
github.com/jhillyerd/enmime v1.2.0
github.com/klauspost/compress v1.17.7
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/sirupsen/logrus v1.9.3
@@ -21,11 +21,11 @@ require (
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.20.0
golang.org/x/net v0.22.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.28.0
modernc.org/sqlite v1.29.3
)
require (
@@ -36,10 +36,11 @@ require (
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
github.com/gorilla/css v1.0.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
@@ -48,23 +49,19 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/reiver/go-oi v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.6 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
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.18.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/image v0.15.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/tools v0.17.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/tools v0.18.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
modernc.org/cc/v3 v3.41.0 // indirect
modernc.org/ccgo/v3 v3.16.15 // indirect
modernc.org/libc v1.41.0 // indirect
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b // indirect
modernc.org/libc v1.43.1 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
)

77
go.sum
View File

@@ -5,10 +5,9 @@ github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1e
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.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
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.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
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/axllent/semver v0.0.1 h1:QqF+KSGxgj8QZzSXAvKFqjGWE5792ksOnQhludToK8E=
@@ -53,8 +52,8 @@ github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW
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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
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=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
@@ -64,16 +63,16 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jhillyerd/enmime v1.1.0 h1:ubaIzg68VY7CMCe2YbHe6nkRvU9vujixTkNz3EBvZOw=
github.com/jhillyerd/enmime v1.1.0/go.mod h1:FRFuUPCLh8PByQv+8xRcLO9QHqaqTqreYhopv5eyk4I=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/compress v1.17.5 h1:d4vBd+7CHydUqpFBgUEKkSdtSugf9YFmSkvUYPquI5E=
github.com/klauspost/compress v1.17.5/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
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/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=
@@ -83,13 +82,15 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leporo/sqlf v1.4.0 h1:SyWnX/8GSGOzVmanG0Ub1c04mR9nNl6Tq3IeFKX2/4c=
github.com/leporo/sqlf v1.4.0/go.mod h1:pgN9yKsAnQ+2ewhbZogr98RcasUjPsHF3oXwPPhHvBw=
github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c=
github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y=
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.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mhale/smtpd v0.8.2 h1:rHKOMHeFoDvcq8Na9ErCbNcjlWTSyGtznOmJpWsOzuc=
github.com/mhale/smtpd v0.8.2/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
@@ -107,8 +108,8 @@ github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e/go.mod h1:+5vNVvE
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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -143,37 +144,32 @@ 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.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
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/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=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
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.7.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.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -181,15 +177,14 @@ 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.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
@@ -201,8 +196,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -212,27 +207,23 @@ 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=
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0=
modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY=
modernc.org/cc/v4 v4.19.3 h1:vE9kmJqUcyvNOf8F2Hn8od14SOMq34BiqcZ2tMzLk5c=
modernc.org/ccgo/v4 v4.9.9 h1:HGZLD/Ws06nfcbjAKpnR5On3KQRSKUrD+oiHgEvRANI=
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.43.1 h1:t1JKWKv2dxw3xj3OXmA/abCLTyZGEWSjUcOMh8kZ8zc=
modernc.org/libc v1.43.1/go.mod h1:KpVOBS+2L3K2i2oZac6eycs//ukjVzwrhobyw+mi81c=
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/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sqlite v1.29.3 h1:6L71d3zXVB8oubdVSuwiurNyYRetQ3It8l1FSwylwQ0=
modernc.org/sqlite v1.29.3/go.mod h1:MjUIBKZ+tU/lqjNLbVAAMjsQPdWdA/ciwdhsT9kBwk8=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=

View File

@@ -13,6 +13,8 @@ var (
UICredentials *htpasswd.File
// SMTPCredentials passwords
SMTPCredentials *htpasswd.File
// POP3Credentials passwords
POP3Credentials *htpasswd.File
)
// SetUIAuth will set Basic Auth credentials required for the UI & API
@@ -53,6 +55,25 @@ func SetSMTPAuth(s string) error {
return nil
}
// SetPOP3Auth will set POP3 server credentials
func SetPOP3Auth(s string) error {
var err error
credentials := credentialsFromString(s)
if len(credentials) == 0 {
return nil
}
r := strings.NewReader(strings.Join(credentials, "\n"))
POP3Credentials, err = htpasswd.NewFromReader(r, htpasswd.DefaultSystems, nil)
if err != nil {
return err
}
return nil
}
func credentialsFromString(s string) []string {
// split string by any whitespace character
re := regexp.MustCompile(`\s+`)

View File

@@ -1,6 +1,6 @@
{
"api_version":"1.0.4",
"last_update_date":"2024-01-31 20:28:42 +0000",
"last_update_date":"2024-03-09 07:21:32 +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":[
{
@@ -240,7 +240,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/hMLCNCSKZYHkLgLOpIWltlnYjtagbNsrwzMxalc2VbghN/list",
"stats":{"apple-mail":{"macos":{"10.3":"y"},"ios":{"10.3":"y","12.2":"y"}},"gmail":{"desktop-webmail":{"2019-08":"a #1 #7","2020-01":"a #7"},"ios":{"2019-08":"a #1 #6 #7","2020-01":"a #6 #7"},"android":{"2019-08":"a #1 #6 #7","2022-07":"a #6 #7"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-08":"y #5","2021-03":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"outlook":{"windows":{"2003":"a #1","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-01":"n"},"macos":{"2011":"y","2016":"y","16.80":"a #1 #10"},"outlook-com":{"2019-08":"a #1","2023-12":"a #1 #10"},"ios":{"2019-08":"a #1"},"android":{"2019-08":"a #1"}},"samsung-email":{"android":{"5.0.10.2":"y","6.0":"y","6.1.90.16":"a #9"}},"sfr":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"n"},"android":{"2019-08":"n"}},"thunderbird":{"macos":{"60.3":"y","78.5":"n","102.11":"n"}},"yahoo":{"desktop-webmail":{"2019-08":"a #1 #2","2020-01":"a #2"},"ios":{"2019-08":"a #1 #2","2020-01":"a #2"},"android":{"2019-08":"a #1 #2 #3","2020-01":"a #2 #3"}},"aol":{"desktop-webmail":{"2019-02":"a #1 #2","2020-01":"a #2"},"ios":{"2019-02":"a #1 #2","2020-01":"a #2"},"android":{"2019-02":"a #1 #2","2020-01":"a #2"}},"protonmail":{"desktop-webmail":{"2020-03":"n","2023-05":"a #8"},"ios":{"2020-03":"n","2023-05":"a #8"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"a #1 #7"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"y #1"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"y #1"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Does not support nested media queries.","2":"Partial. Only supports `screen`, `min-width`, `max-width`, `min-height` and `max-height` based media queries.","3":"Buggy. Requires a double `<head>` hack to work.","4":"Partial. Does not support simple `@media {}` declarations.","5":"Buggy. The first rule inside a media query is not prefixed.","6":"Partial. Not supported with non Gmail accounts.","7":"Partial. Does not support height based media queries.","8":"Partial. Does not support landscape media query.","9":"Partial. Not supported with Hotmail/Outlook accounts.","10":"Partial. Nested media queries are removed."}
"notes_by_num":{"1":"Partial. Does not support nested media queries.","2":"Partial. Only supports `screen`, `min-width`, `max-width`, `min-height` and `max-height` based media queries.","3":"Buggy. Requires a double `<head>` hack to work.","4":"Partial. Does not support simple `@media {}` declarations.","5":"Buggy. The first rule inside a media query is not prefixed.","6":"Partial. Not supported with non Google accounts.","7":"Partial. Does not support height based media queries.","8":"Partial. Does not support landscape media query.","9":"Partial. Not supported with Hotmail/Outlook accounts.","10":"Partial. Nested media queries are removed."}
},
{
@@ -272,7 +272,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/oxaaoE6R3ur4T9fAPzVsQ3G2R7p1c9axDm7LLgC3cKw0F/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"a #1"},"android":{"2019-02":"a #1"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-08":"y","2021-03":"n"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2019-02":"n"},"macos":{"2019-02":"y","16.80":"y"},"outlook-com":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"n"}},"aol":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"n"}},"samsung-email":{"android":{"5.0.10.2":"y","6.0.04.6":"y"}},"sfr":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"thunderbird":{"macos":{"60.5.0":"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":"y"}},"gmx":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts."}
},
{
@@ -288,7 +288,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/oxaaoE6R3ur4T9fAPzVsQ3G2R7p1c9axDm7LLgC3cKw0F/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"a #1"},"android":{"2019-02":"a #1"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-08":"y","2021-03":"n"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2019-02":"n"},"macos":{"2019-02":"y","16.80":"y"},"outlook-com":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"n"}},"aol":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"n"}},"samsung-email":{"android":{"5.0.10.2":"y","6.0.04.6":"y"}},"sfr":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"thunderbird":{"macos":{"60.5.0":"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":"y"}},"gmx":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts. But it can be used in the `background` shorthand property instead."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts. But it can be used in the `background` shorthand property instead."}
},
{
@@ -320,7 +320,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/04SuPXr8tEGhWRlJ2Us6dA8BzgREpyxHYEmSBeyNuWyWo/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"y","2023-07":"a #6","2023-08":"y"},"ios":{"2018-09":"a #1","2018-10":"y","2019-02":"y"},"android":{"2018-09":"a #1","2018-10":"y","2019-02":"y"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2019-08":"y","2021-03":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"outlook":{"windows":{"2007":"n #5","2010":"n #5","2013":"n #5","2016":"n #5","2019":"n #5"},"windows-mail":{"2019-02":"n"},"macos":{"2019-02":"y","16.80":"y"},"outlook-com":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"a #3 #4","2021-10":"a #3"},"ios":{"2019-02":"a #3 #4","2021-10":"a #3"},"android":{"2019-02":"a #3 #4","2021-10":"a #3"}},"aol":{"desktop-webmail":{"2019-02":"a #3 #4","2021-10":"a #3"},"ios":{"2019-02":"a #3 #4","2021-10":"a #3"},"android":{"2019-02":"a #3 #4","2021-10":"a #3"}},"samsung-email":{"android":{"5.0.10.2":"a #2","6.0.04.6":"y"}},"sfr":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"thunderbird":{"macos":{"60.5.0":"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":"y"}},"t-online-de":{"desktop-webmail":{"2021-11":"n"}},"free-fr":{"desktop-webmail":{"2021-11":"n"}},"gmx":{"desktop-webmail":{"2022-06":"a #3"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"a #3"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts.","2":"Buggy. Requires at least one `<img>` element in the email to download all images.","3":"Partial. Does not support multiple values. The comma between two values is removed.","4":"Partial. Images URL must be between quotes.","5":"Background images can be used in VML. See [backgrounds.cm](https://backgrounds.cm/) and [VML documentation](https://docs.microsoft.com/en-us/windows/win32/vml/web-workshop---how-to-use-vml-on-web-pages-----fill--element).","6":"Partial and buggy. Removes the entire `style` attribute or `<style>` tag when a `url()` function with a valid image URL is present. See [Gmail rolling out changes that strip CSS with background images](https://freshinbox.com/blog/gmail-rolling-out-changes-that-strip-background-image-css/) and [Gmail and background images](https://parcel.io/blog/gmail-and-background-images)."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts.","2":"Buggy. Requires at least one `<img>` element in the email to download all images.","3":"Partial. Does not support multiple values. The comma between two values is removed.","4":"Partial. Images URL must be between quotes.","5":"Background images can be used in VML. See [backgrounds.cm](https://backgrounds.cm/) and [VML documentation](https://docs.microsoft.com/en-us/windows/win32/vml/web-workshop---how-to-use-vml-on-web-pages-----fill--element).","6":"Partial and buggy. Removes the entire `style` attribute or `<style>` tag when a `url()` function with a valid image URL is present. See [Gmail rolling out changes that strip CSS with background images](https://freshinbox.com/blog/gmail-rolling-out-changes-that-strip-background-image-css/) and [Gmail and background images](https://parcel.io/blog/gmail-and-background-images)."}
},
{
@@ -384,7 +384,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/oxaaoE6R3ur4T9fAPzVsQ3G2R7p1c9axDm7LLgC3cKw0F/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"a #1"},"android":{"2019-02":"a #1"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-08":"y","2021-03":"n"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"outlook":{"windows":{"2007":"n #3","2010":"n #3","2013":"n #3","2016":"n #3","2019":"n #3"},"windows-mail":{"2019-02":"n"},"macos":{"2019-02":"y","16.80":"y"},"outlook-com":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"y #2"},"ios":{"2019-02":"y #2"},"android":{"2019-02":"y #2"}},"aol":{"desktop-webmail":{"2019-02":"y #2"},"ios":{"2019-02":"y #2"},"android":{"2019-02":"y #2"}},"samsung-email":{"android":{"5.0.10.2":"y","6.0.04.6":"y"}},"sfr":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"thunderbird":{"macos":{"60.5.0":"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":"y"}},"gmx":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts. But it can be used in the `background` shorthand property instead.","2":"Partial. Does not support multiple values. The comma between two values is removed.","3":"Background images size can be used in VML with the `size` attribute. See [VML documentation](https://docs.microsoft.com/en-us/windows/win32/vml/size-attribute--fill--vml)."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts. But it can be used in the `background` shorthand property instead.","2":"Partial. Does not support multiple values. The comma between two values is removed.","3":"Background images size can be used in VML with the `size` attribute. See [VML documentation](https://docs.microsoft.com/en-us/windows/win32/vml/size-attribute--fill--vml)."}
},
{
@@ -419,6 +419,22 @@
"notes_by_num":null
},
{
"slug":"css-border-collapse",
"title":"border-collapse",
"description":"Sets whether cells inside a `<table>` have shared or separate borders.",
"url":"https://www.caniemail.com/features/css-border-collapse/",
"category":"css",
"tags":[],
"keywords":"table",
"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"}}},
"notes":null,
"notes_by_num":null
},
{
"slug":"css-border-image",
"title":"border-image",
@@ -515,6 +531,22 @@
"notes_by_num":{"1":"Round corners can be used in VML with the `RoundRect` element. See [buttons.cm](https://buttons.cm/) and [VML documentation](https://docs.microsoft.com/en-us/windows/win32/vml/msdn-online-vml-roundrect-element).","2":"Partial support. Shorthand for setting elliptical borders with the slash `/` notation is not supported e.g. `border-radius: 27% 73% 70% 30% / 30% 34% 66% 70%;`."}
},
{
"slug":"css-border-spacing",
"title":"border-spacing",
"description":"Sets the distance between the borders of adjacent cells in a `<table>`.",
"url":"https://www.caniemail.com/features/css-border-spacing/",
"category":"css",
"tags":[],
"keywords":"table",
"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"}}},
"notes":null,
"notes_by_num":null
},
{
"slug":"css-border",
"title":"border",
@@ -544,7 +576,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/TxbmgM1vnD44aLEEOdI06riwAqm3qmfet8jFoYTQ65bRp/list",
"stats":{"apple-mail":{"macos":{"12.4":"y","16.0":"y"},"ios":{"12.1":"y","17.2":"y"}},"gmail":{"desktop-webmail":{"2019-02":"n","2023-01":"n"},"ios":{"2019-02":"a #1","2023-01":"a #1"},"android":{"2021-08":"a #1","2023-01":"a #1"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-08":"y","2021-03":"n"},"ios":{"2021-03":"n","2023-12":"n"},"android":{"2019-08":"y","2023-12":"n"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2019-02":"n"},"macos":{"2019-02":"y","16.80":"y"},"outlook-com":{"2019-02":"n","2023-01":"n","2023-12":"y"},"ios":{"2019-02":"n","2023-01":"n","2023-12":"y"},"android":{"2019-02":"n","2023-01":"n","2023-12":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"n","2023-01":"n"},"ios":{"2019-02":"n","2023-01":"n"},"android":{"2019-02":"n","2023-01":"n"}},"aol":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n","2023-01":"n"},"android":{"2019-02":"n","2023-01":"n"}},"samsung-email":{"android":{"5.0.10.2":"y","6.1.50.25":"y"}},"sfr":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"thunderbird":{"macos":{"60.5":"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":"y"}},"gmx":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Only supported with non Gmail accounts."}
"notes_by_num":{"1":"Partial. Only supported with non Google accounts."}
},
{
@@ -688,7 +720,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/Kw9bvIPLsmmwVoXhbXpIu1FM31v4nV2KXMaEvPQPezSO9/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y","2020-11":"a #1"},"android":{"2019-02":"y","2020-11":"a #1"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2019-08":"y","2021-03":"n"},"ios":{"2019-08":"y","2021-11":"n"},"android":{"2019-08":"y","2021-11":"n"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2019-02":"n"},"macos":{"2019-02":"y","16.80":"y"},"outlook-com":{"2019-02":"y"},"ios":{"2019-02":"a","2020-11":"y"},"android":{"2019-02":"a","2020-11":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"n","2020-11":"y #2"},"ios":{"2019-02":"n","2020-11":"y #2"},"android":{"2019-02":"n","2020-11":"y #2"}},"aol":{"desktop-webmail":{"2019-02":"n","2020-11":"y #2"},"ios":{"2019-02":"n","2020-11":"y #2"},"android":{"2019-02":"n","2020-11":"y #2"}},"samsung-email":{"android":{"5.0.10.2":"y"}},"sfr":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"thunderbird":{"macos":{"60.5":"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":"y"}},"gmx":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Not supported with non Gmail accounts.","2":"`display:inline-flex` is not supported."}
"notes_by_num":{"1":"Not supported with non Google accounts.","2":"`display:inline-flex` is not supported."}
},
{
@@ -736,7 +768,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/eyGBFSORjrHd635gw4udynxX8ykC5bzlMUUrF6yi7Kspu/list",
"stats":{"apple-mail":{"macos":{"15":"y"},"ios":{"15":"y"}},"gmail":{"desktop-webmail":{"2021-12":"y"},"ios":{"2021-12":"a #1"},"android":{"2021-12":"a #1"},"mobile-webmail":{"2021-12":"y"}},"orange":{"desktop-webmail":{"2021-12":"a #2 #3"},"ios":{"2021-12":"a #2"},"android":{"2021-12":"a #2"}},"outlook":{"windows":{"2007":"a #4 #5","2010":"a #4 #5","2013":"a #4 #5","2016":"a #4 #5","2019":"a #4 #5"},"windows-mail":{"2021-12":"a #4 #5"},"macos":{"16.56":"y","16.80":"y"},"outlook-com":{"2021-12":"y"},"ios":{"2021-12":"y"},"android":{"2021-12":"y"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2021-12":"y"},"ios":{"2021-12":"y"},"android":{"2021-12":"y"}},"thunderbird":{"macos":{"91.2":"y"}},"aol":{"desktop-webmail":{"2021-12":"a #6"},"ios":{"2021-12":"a #6"},"android":{"2021-12":"a #6"}},"yahoo":{"desktop-webmail":{"2021-12":"a #6"},"ios":{"2021-12":"a #6"},"android":{"2021-12":"a #6"}},"protonmail":{"desktop-webmail":{"2021-12":"y"},"ios":{"2021-12":"y"},"android":{"2021-12":"y"}},"hey":{"desktop-webmail":{"2021-12":"y"}},"mail-ru":{"desktop-webmail":{"2021-12":"y"}},"fastmail":{"desktop-webmail":{"2021-12":"a #7"}},"laposte":{"desktop-webmail":{"2021-12":"y"}},"gmx":{"desktop-webmail":{"2022-06":"a #2"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"a #2"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. `flex`, `grid`, `flow-root`, `contents`, `inline flow-root`, `inline flex`, `inline grid`, `initial`, `revert`, `unset` are not supported with Non Gmail Accounts.","2":"Partial. `inline-flex`, `inline-grid`, `flex`, `grid`, `flow-root`, `contents`, `inline flow-root`, `inline flex`, `inline grid`, `initial`, `revert`, `unset` values are not supported.","3":"Buggy. Only the first value is kept with the two-value syntax.","4":"Buggy. `display:none` does not inherit to inner tables.","5":"Partial. Only supports `display:none` (but not on `<img>`).","6":"Partial. `flow-root`, `inline-flex`, `inline-grid`, `inline flow`, `contents`, `revert` are not supported.","7":"Partial. Two-value syntax are combined into a single one with a dash."}
"notes_by_num":{"1":"Partial. `flex`, `grid`, `flow-root`, `contents`, `inline flow-root`, `inline flex`, `inline grid`, `initial`, `revert`, `unset` are not supported with non Google accounts.","2":"Partial. `inline-flex`, `inline-grid`, `flex`, `grid`, `flow-root`, `contents`, `inline flow-root`, `inline flex`, `inline grid`, `initial`, `revert`, `unset` values are not supported.","3":"Buggy. Only the first value is kept with the two-value syntax.","4":"Buggy. `display:none` does not inherit to inner tables.","5":"Partial. Only supports `display:none` (but not on `<img>`).","6":"Partial. `flow-root`, `inline-flex`, `inline-grid`, `inline flow`, `contents`, `revert` are not supported.","7":"Partial. Two-value syntax are combined into a single one with a dash."}
},
{
@@ -747,12 +779,12 @@
"category":"css",
"tags":[],
"keywords":null,
"last_test_date":"2019-02-28",
"last_test_date":"2024-01-03",
"test_url":"https://www.caniemail.com/tests/css-visual-effects.html",
"test_results_url":"https://app.emailonacid.com/app/acidtest/DhTRmGsVH6uobU4pHD3CasJywfBL4HnEjA1LOF8f9ctso/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"n"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-08":"y","2021-03":"n"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2019-02":"n"},"macos":{"2019-02":"y","16.80":"n"},"outlook-com":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"n"}},"thunderbird":{"macos":{"60.8":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"n"}},"aol":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"n"}},"samsung-email":{"android":{"5.0.10.2":"y"}},"sfr":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"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":"n"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"test_results_url":"https://testi.at/proj/73yrcloa1z681dkzu6",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"n"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-08":"y","2021-03":"n"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2019-02":"n"},"macos":{"2019-02":"y","16.80":"n"},"outlook-com":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"n"}},"thunderbird":{"macos":{"60.8":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"n"}},"aol":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"n"}},"samsung-email":{"android":{"5.0.10.2":"y"}},"sfr":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"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":{"2024-01":"a #1"}},"fastmail":{"desktop-webmail":{"2021-07":"n"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":null
"notes_by_num":{"1":"Partial. Multiple values don't work."}
},
{
@@ -864,7 +896,7 @@
"test_results_url":"https: //app.emailonacid.com/app/acidtest/DkqbHs69ek5UnK6uhZ7Uj0n5GVQNTP4Z1FvgXvnKyEoTM/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"a #1","2020-12":"y"},"android":{"2019-02":"a #1","2020-12":"y"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2020-01":"y","2021-03":"y"},"ios":{"2020-01":"y"},"android":{"2020-01":"y"}},"outlook":{"windows":{"2007":"y","2010":"y","2013":"y","2016":"y","2019":"y"},"windows-mail":{"2019-02":"y"},"macos":{"2019-02":"y","16.80":"y"},"outlook-com":{"2019-02":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"aol":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"samsung-email":{"android":{"5.0.10.2":"y"}},"sfr":{"desktop-webmail":{"2020-01":"y"},"ios":{"2020-01":"y"},"android":{"2020-01":"y"}},"thunderbird":{"macos":{"68.4":"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":"y"}},"gmx":{"desktop-webmail":{"2022-06":"y"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"y"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts."}
},
{
@@ -880,7 +912,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/zSEJIfc49LeYUVU5ncqsWBDRRTZlqq01sYRUSICWOs74Y/list",
"stats":{"apple-mail":{"macos":{"13":"n","14.0":"y"},"ios":{"13":"n","14.5":"y"}},"gmail":{"desktop-webmail":{"2021-05":"y"},"ios":{"2021-05":"a #1"},"android":{"2021-05":"a #1"},"mobile-webmail":{"2021-05":"y"}},"orange":{"desktop-webmail":{"2021-05":"n"},"ios":{"2021-05":"n"},"android":{"2021-05":"n"}},"outlook":{"windows":{"2003":"n","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2021-05":"n"},"macos":{"2011":"n","2016":"n","16.80":"n"},"outlook-com":{"2021-05":"n","2023-12":"n"},"ios":{"2021-05":"n"},"android":{"2021-05":"n"}},"samsung-email":{"android":{"6.0":"n"}},"sfr":{"desktop-webmail":{"2021-05":"y"},"ios":{"2021-05":"y"},"android":{"2021-05":"y"}},"thunderbird":{"macos":{"78.10":"y"}},"aol":{"desktop-webmail":{"2021-05":"n"},"ios":{"2021-05":"n"},"android":{"2021-05":"n"}},"yahoo":{"desktop-webmail":{"2021-05":"n"},"ios":{"2021-05":"n"},"android":{"2021-05":"n"}},"protonmail":{"desktop-webmail":{"2021-05":"y"},"ios":{"2021-05":"y"},"android":{"2021-05":"y"}},"hey":{"desktop-webmail":{"2021-05":"y"}},"mail-ru":{"desktop-webmail":{"2021-05":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"n"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with Non Gmail Accounts."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts."}
},
{
@@ -896,7 +928,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/zSEJIfc49LeYUVU5ncqsWBDRRTZlqq01sYRUSICWOs74Y/list",
"stats":{"apple-mail":{"macos":{"13":"y","14.0":"y"},"ios":{"13":"y","14.5":"y"}},"gmail":{"desktop-webmail":{"2021-05":"y"},"ios":{"2021-05":"a #1"},"android":{"2021-05":"a #1"},"mobile-webmail":{"2021-05":"y"}},"orange":{"desktop-webmail":{"2021-05":"n"},"ios":{"2021-05":"n"},"android":{"2021-05":"n"}},"outlook":{"windows":{"2003":"n","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2021-05":"n"},"macos":{"2011":"n","2016":"y","16.80":"n"},"outlook-com":{"2021-05":"n","2023-12":"n"},"ios":{"2021-05":"n"},"android":{"2021-05":"n"}},"samsung-email":{"android":{"6.0":"n"}},"sfr":{"desktop-webmail":{"2021-05":"y"},"ios":{"2021-05":"y"},"android":{"2021-05":"y"}},"thunderbird":{"macos":{"78.10":"y"}},"aol":{"desktop-webmail":{"2021-05":"n"},"ios":{"2021-05":"n"},"android":{"2021-05":"n"}},"yahoo":{"desktop-webmail":{"2021-05":"n"},"ios":{"2021-05":"n"},"android":{"2021-05":"n"}},"protonmail":{"desktop-webmail":{"2021-05":"y"},"ios":{"2021-05":"y"},"android":{"2021-05":"y"}},"hey":{"desktop-webmail":{"2021-05":"y"}},"mail-ru":{"desktop-webmail":{"2021-05":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"n"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with Non Gmail Accounts."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts."}
},
{
@@ -912,7 +944,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/zSEJIfc49LeYUVU5ncqsWBDRRTZlqq01sYRUSICWOs74Y/list",
"stats":{"apple-mail":{"macos":{"13":"y","14.0":"y"},"ios":{"13":"y","14.5":"y"}},"gmail":{"desktop-webmail":{"2021-05":"y"},"ios":{"2021-05":"a #1"},"android":{"2021-05":"a #1"},"mobile-webmail":{"2021-05":"y"}},"orange":{"desktop-webmail":{"2021-05":"n"},"ios":{"2021-05":"n"},"android":{"2021-05":"n"}},"outlook":{"windows":{"2003":"n","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2021-05":"n"},"macos":{"2011":"n","2016":"y","16.80":"n"},"outlook-com":{"2021-05":"n","2023-12":"n"},"ios":{"2021-05":"n"},"android":{"2021-05":"n"}},"samsung-email":{"android":{"6.0":"n"}},"sfr":{"desktop-webmail":{"2021-05":"y"},"ios":{"2021-05":"y"},"android":{"2021-05":"y"}},"thunderbird":{"macos":{"78.10":"y"}},"aol":{"desktop-webmail":{"2021-05":"n"},"ios":{"2021-05":"n"},"android":{"2021-05":"n"}},"yahoo":{"desktop-webmail":{"2021-05":"n"},"ios":{"2021-05":"n"},"android":{"2021-05":"n"}},"protonmail":{"desktop-webmail":{"2021-05":"y"},"ios":{"2021-05":"y"},"android":{"2021-05":"y"}},"hey":{"desktop-webmail":{"2021-05":"y"}},"mail-ru":{"desktop-webmail":{"2021-05":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"n"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with Non Gmail Accounts."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts."}
},
{
@@ -1024,7 +1056,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/tcE2poX3tOf6HlwmIH6GQqveQN6SwMrsqCp4ylpkFVc3W/list",
"stats":{"apple-mail":{"macos":{"12.4":"y #2"},"ios":{"12.1":"y #2"}},"gmail":{"desktop-webmail":{"2022-12":"y"},"ios":{"2022-12":"a #3"},"android":{"2022-12":"y"},"mobile-webmail":{"2022-12":"y"}},"orange":{"desktop-webmail":{"2023-01":"n"},"ios":{"2023-01":"n"},"android":{"2022-12":"n"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2022-12":"n"},"macos":{"2022-12":"y","16.80":"y"},"outlook-com":{"2022-12":"y","2023-12":"y"},"ios":{"2022-12":"a #1"},"android":{"2022-12":"a #1"}},"yahoo":{"desktop-webmail":{"2022-12":"n"},"ios":{"2022-12":"n"},"android":{"2022-12":"n"}},"aol":{"desktop-webmail":{"2022-12":"n"},"ios":{"2022-12":"n"},"android":{"2022-12":"n"}},"samsung-email":{"android":{"6.1":"n"}},"sfr":{"desktop-webmail":{"2022-12":"y"},"ios":{"2022-12":"y"},"android":{"2022-12":"y"}},"thunderbird":{"macos":{"102.6":"y"}},"protonmail":{"desktop-webmail":{"2022-12":"y"},"ios":{"2022-12":"y #2"},"android":{"2022-12":"y"}},"hey":{"desktop-webmail":{"2022-12":"y"}},"mail-ru":{"desktop-webmail":{"2022-12":"y"}},"fastmail":{"desktop-webmail":{"2022-12":"y"}},"laposte":{"desktop-webmail":{"2022-12":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Supports `min-content` and `fit-content` only.","2":"Buggy. `width: min-content` acts as if `word-break: break-all` had been set on the element.","3":"Partial. Not supported with non Gmail accounts."}
"notes_by_num":{"1":"Partial. Supports `min-content` and `fit-content` only.","2":"Buggy. `width: min-content` acts as if `word-break: break-all` had been set on the element.","3":"Partial. Not supported with non Google accounts."}
},
{
@@ -1360,7 +1392,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/Yh6M44osu9gXxAcqLb2TBazoUxeQYOXHgdiEWg2wYbEhj/list",
"stats":{"apple-mail":{"macos":{"11":"y","12":"y","13":"y"},"ios":{"11":"y","12":"y","13":"y","14":"y"}},"gmail":{"desktop-webmail":{"2020-12":"y"},"ios":{"2020-12":"a #1"},"android":{"2020-12":"a #1"},"mobile-webmail":{"2020-12":"n"}},"orange":{"desktop-webmail":{"2020-12":"y","2021-03":"n"},"ios":{"2020-12":"y"},"android":{"2020-12":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-12":"n"},"macos":{"2020-12":"y","16.80":"y"},"outlook-com":{"2020-12":"y","2024-01":"y"},"ios":{"2020-12":"y"},"android":{"2020-12":"y"}},"yahoo":{"desktop-webmail":{"2020-12":"n"},"ios":{"2020-12":"n"},"android":{"2020-12":"n"}},"aol":{"desktop-webmail":{"2020-12":"n"},"ios":{"2020-12":"n"},"android":{"2020-12":"n"}},"samsung-email":{"android":{"6.1.30.30":"y"}},"sfr":{"desktop-webmail":{"2020-12":"y"},"ios":{"2020-12":"y"},"android":{"2020-12":"y"}},"thunderbird":{"macos":{"78.5":"y"}},"protonmail":{"desktop-webmail":{"2020-12":"y"},"ios":{"2020-12":"y"},"android":{"2020-12":"y"}},"hey":{"desktop-webmail":{"2020-12":"y"}},"mail-ru":{"desktop-webmail":{"2020-12":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"n"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with Non Gmail Accounts."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts."}
},
{
@@ -1408,7 +1440,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/Rg26n7zpfSw6bcxjGdDU9eF0aieX8XR7QoXfSfjbOEKXt/list",
"stats":{"apple-mail":{"macos":{"13":"y","14":"y"},"ios":{"13":"y","14.6":"y"}},"gmail":{"desktop-webmail":{"2021-07":"y"},"ios":{"2021-07":"a #1"},"android":{"2021-07":"a #1"},"mobile-webmail":{"2021-07":"n"}},"orange":{"desktop-webmail":{"2021-07":"n"},"ios":{"2021-07":"n"},"android":{"2021-07":"n"}},"outlook":{"windows":{"2003":"n","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2021-07":"n"},"macos":{"2016":"y","2019":"y","16.80":"y"},"outlook-com":{"2021-07":"y","2024-01":"y"},"ios":{"2021-07":"y"},"android":{"2021-07":"y"}},"samsung-email":{"android":{"7.0":"y"}},"sfr":{"desktop-webmail":{"2021-07":"y"},"ios":{"2021-07":"y"},"android":{"2021-07":"y"}},"thunderbird":{"macos":{"78.10":"y"}},"aol":{"desktop-webmail":{"2021-07":"n"},"ios":{"2021-07":"n"},"android":{"2021-07":"n"}},"yahoo":{"desktop-webmail":{"2021-07":"n"},"ios":{"2021-07":"n"},"android":{"2021-07":"n"}},"protonmail":{"desktop-webmail":{"2021-07":"y"},"ios":{"2021-07":"y"},"android":{"2021-07":"y"}},"hey":{"desktop-webmail":{"2021-07":"y"}},"mail-ru":{"desktop-webmail":{"2021-07":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts."}
},
{
@@ -1424,7 +1456,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/Rg26n7zpfSw6bcxjGdDU9eF0aieX8XR7QoXfSfjbOEKXt/list",
"stats":{"apple-mail":{"macos":{"13":"y","14":"y"},"ios":{"13":"y","14.6":"y"}},"gmail":{"desktop-webmail":{"2021-07":"y"},"ios":{"2021-07":"a #1"},"android":{"2021-07":"a #1"},"mobile-webmail":{"2021-07":"n"}},"orange":{"desktop-webmail":{"2021-07":"n"},"ios":{"2021-07":"n"},"android":{"2021-07":"n"}},"outlook":{"windows":{"2003":"n","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2021-07":"n"},"macos":{"2016":"n","2019":"y","16.80":"y"},"outlook-com":{"2021-07":"y","2024-01":"y"},"ios":{"2021-07":"y"},"android":{"2021-07":"y"}},"samsung-email":{"android":{"7.0":"y"}},"sfr":{"desktop-webmail":{"2021-07":"y"},"ios":{"2021-07":"y"},"android":{"2021-07":"y"}},"thunderbird":{"macos":{"78.10":"y"}},"aol":{"desktop-webmail":{"2021-07":"n"},"ios":{"2021-07":"n"},"android":{"2021-07":"n"}},"yahoo":{"desktop-webmail":{"2021-07":"n"},"ios":{"2021-07":"n"},"android":{"2021-07":"n"}},"protonmail":{"desktop-webmail":{"2021-07":"y"},"ios":{"2021-07":"y"},"android":{"2021-07":"y"}},"hey":{"desktop-webmail":{"2021-07":"y"}},"mail-ru":{"desktop-webmail":{"2021-07":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts."}
},
{
@@ -1696,7 +1728,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/cl8ZYgIGE372fkVVuJkwNJDd7B4JUpo23Nz6qANcSlRUA/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y","13.1":"y"}},"gmail":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"n"},"android":{"2019-10":"a #1"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-10":"y","2021-03":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2019-10":"n"},"macos":{"2019-02":"y","16.80":"a #2"},"outlook-com":{"2019-10":"a #2","2024-01":"a #2"},"ios":{"2019-10":"n"},"android":{"2019-10":"y"}},"thunderbird":{"macos":{"60.8":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"aol":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"n"},"android":{"2019-02":"y"}},"samsung-email":{"android":{"5.0.10.2":"y","6.0.04.6":"y"}},"sfr":{"desktop-webmail":{"2020-01":"y"},"ios":{"2020-01":"n"},"android":{"2020-01":"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":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"y"},"android":{"2022-07":"y"}},"web-de":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"y"},"android":{"2022-07":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-07":"y"},"android":{"2022-07":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts.","2":"Partial. Only supported on type selectors."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts.","2":"Partial. Only supported on type selectors."}
},
{
@@ -2080,7 +2112,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/ZOGHmMjaZIUfa2M44xDIdv9lwqol3UQN00PDO7G5kK21Y/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"13.3":"y"}},"gmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"a #1"},"android":{"2020-03":"a #1"},"mobile-webmail":{"2020-03":"n"}},"orange":{"desktop-webmail":{"2020-03":"y","2021-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"outlook":{"windows":{"2003":"y","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-03":"n"},"macos":{"2011":"y","2016":"y","16.80":"y"},"outlook-com":{"2020-03":"y","2024-01":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"samsung-email":{"android":{"7.0":"y"}},"sfr":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"n"},"android":{"2020-03":"n"}},"thunderbird":{"macos":{"68.5":"y"}},"aol":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"yahoo":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"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":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"a #2"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"a #2"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts.","2":"Partial. Only when used as class or ID selectors"}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts.","2":"Partial. Only when used as class or ID selectors"}
},
{
@@ -2096,7 +2128,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/xHppIhPmgvxZQPvA3geS9WGCicLxVuR87NTZu70eWaAF8/list",
"stats":{"apple-mail":{"macos":{"14.0":"y"},"ios":{"14.3":"y"}},"gmail":{"desktop-webmail":{"2020-12":"a #5","2023-02":"a #5"},"ios":{"2020-12":"a #5 #6","2023-02":"a #5 #6"},"android":{"2020-12":"a #5 #6","2023-02":"a #5 #6"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2020-12":"y","2021-03":"y"},"ios":{"2020-12":"y"},"android":{"2020-12":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-12":"n"},"macos":{"2011":"y","2016":"y","16.80":"a #1 #2 #3"},"outlook-com":{"2020-12":"a #1 #2 #3","2024-01":"a #1 #2 #3"},"ios":{"2020-12":"a #1 #2"},"android":{"2020-12":"a #1 #2"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2020-12":"y"},"ios":{"2020-12":"n"},"android":{"2020-12":"n"}},"thunderbird":{"macos":{"78.5":"y"}},"aol":{"desktop-webmail":{"2020-12":"a #4"},"ios":{"2020-12":"a #4"},"android":{"2020-12":"a #4"}},"yahoo":{"desktop-webmail":{"2020-12":"a #4"},"ios":{"2020-12":"a #4"},"android":{"2020-12":"a #4"}},"protonmail":{"desktop-webmail":{"2020-03":"n"},"ios":{"2020-03":"n"},"android":{"2020-03":"n"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"a #4"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"n"},"android":{"2022-06":"y"}}},
"notes":"This page reflects support for different syntaxes: `[attr]`, `[attr=\"value\"]`, `[attr~=\"value\"]`, `[attr|=\"value\"]`, `[attr^=\"value\"]`, `[attr$=\"value\"]`, `[attr*=\"value\"]`.",
"notes_by_num":{"1":"Partial. Can not be used with a class selector (`.test[class]`). Must be used solo (`[class]`) or with an element selector (`td[class]`).","2":"Partial. Only supports `[attr]`, `[attr=value]`, `[attr~=value]`, `[attr|=value]` syntaxes.","3":"Buggy. A `class=\"test\"` in the HTML is prefixed `class=\"x_test\"`, but an attribute selector stays unprefixed `[class=\"test\"]`.","4":"Partial. Only supports `[attr=value]` syntax.","5":"Partial. Only supports `[attr~=value]` syntax. Only `class` as an attribute name is known to be supported.","6":"Partial. Doesn't work with Non Gmail Accounts."}
"notes_by_num":{"1":"Partial. Can not be used with a class selector (`.test[class]`). Must be used solo (`[class]`) or with an element selector (`td[class]`).","2":"Partial. Only supports `[attr]`, `[attr=value]`, `[attr~=value]`, `[attr|=value]` syntaxes.","3":"Buggy. A `class=\"test\"` in the HTML is prefixed `class=\"x_test\"`, but an attribute selector stays unprefixed `[class=\"test\"]`.","4":"Partial. Only supports `[attr=value]` syntax.","5":"Partial. Only supports `[attr~=value]` syntax. Only `class` as an attribute name is known to be supported.","6":"Partial. Doesn't work with non Google accounts."}
},
{
@@ -2112,7 +2144,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/kQbnzGXMSxMg2NDGmrcxIXOHrROwokTB29RcSssnkoPlS/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"13.1":"y"}},"gmail":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"a #1"},"android":{"2019-10":"a #1"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-10":"y","2021-03":"n"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"outlook":{"windows":{"2003":"y","2007":"n #3","2010":"n #3","2013":"n #3","2016":"n #3","2019":"n #3"},"windows-mail":{"2019-10":"n #3"},"macos":{"2011":"y","2016":"y","16.80":"y"},"outlook-com":{"2019-10":"n #4","2024-01":"n #4"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"n"},"android":{"2019-10":"n"}},"thunderbird":{"macos":{"60.3":"y"}},"aol":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"yahoo":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"a #2"}},"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":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"a #5"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"a #5"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts.","2":"Buggy. The first `<head>` in the HTML is removed, so `<style>` elements need to be in a second `<head>` element.","3":"Buggy. Styles will be applied to the first selector of the chain.","4":"Buggy. Only the first selector of the chain is prefixed in the styles, but all classes are prefixed in the HTML.","5":"Partial. Only when used as class or ID selectors"}
"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":"Buggy. Styles will be applied to the first selector of the chain.","4":"Buggy. Only the first selector of the chain is prefixed in the styles, but all classes are prefixed in the HTML.","5":"Partial. Only when used as class or ID selectors"}
},
{
@@ -2128,7 +2160,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/kQbnzGXMSxMg2NDGmrcxIXOHrROwokTB29RcSssnkoPlS/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"13.1":"y"}},"gmail":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"a #1"},"android":{"2019-10":"a #1"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-10":"y","2021-03":"n"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"outlook":{"windows":{"2003":"y","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2023-06":"n"},"macos":{"2011":"y","2016":"y","16.80":"y"},"outlook-com":{"2019-10":"y","2024-01":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"n"},"android":{"2019-10":"n"}},"thunderbird":{"macos":{"60.3":"y"}},"aol":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"yahoo":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"a #2"}},"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":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts.","2":"Buggy. The first `<head>` in the HTML is removed, so `<style>` elements need to be in a second `<head>` element."}
"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."}
},
{
@@ -2144,7 +2176,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/kQbnzGXMSxMg2NDGmrcxIXOHrROwokTB29RcSssnkoPlS/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"13.1":"y"}},"gmail":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"a #1"},"android":{"2019-10":"a #1"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-10":"y","2021-03":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"outlook":{"windows":{"2003":"y","2007":"y","2010":"y","2013":"y","2016":"y","2019":"y"},"windows-mail":{"2019-10":"y"},"macos":{"2011":"y","2016":"y","16.80":"y"},"outlook-com":{"2019-10":"y","2024-01":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"n"},"android":{"2019-10":"n"}},"thunderbird":{"macos":{"60.3":"y"}},"aol":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"yahoo":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"a #2"}},"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":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"y"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"y"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts.","2":"Buggy. The first `<head>` in the HTML is removed, so `<style>` elements need to be in a second `<head>` element."}
"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."}
},
{
@@ -2160,7 +2192,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/kQbnzGXMSxMg2NDGmrcxIXOHrROwokTB29RcSssnkoPlS/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"13.1":"y"}},"gmail":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"a #1"},"android":{"2019-10":"a #1"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-10":"y","2021-03":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"outlook":{"windows":{"2003":"y","2007":"y","2010":"y","2013":"y","2016":"y","2019":"y"},"windows-mail":{"2019-10":"y"},"macos":{"2011":"y","2016":"y","16.80":"y"},"outlook-com":{"2019-10":"y","2024-01":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"n"},"android":{"2019-10":"n"}},"thunderbird":{"macos":{"60.3":"y"}},"aol":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"yahoo":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"a #2"}},"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":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"a #3"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"a #3"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts.","2":"Buggy. The first `<head>` in the HTML is removed, so `<style>` elements need to be in a second `<head>` element.","3":"Partial. Only when used as class or ID selectors"}
"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":"Partial. Only when used as class or ID selectors"}
},
{
@@ -2176,7 +2208,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/ZOGHmMjaZIUfa2M44xDIdv9lwqol3UQN00PDO7G5kK21Y/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"13.3":"y"}},"gmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"a #1"},"android":{"2020-03":"a #1"},"mobile-webmail":{"2020-03":"n"}},"orange":{"desktop-webmail":{"2020-03":"y","2021-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"outlook":{"windows":{"2003":"y","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-03":"n"},"macos":{"2011":"y","2016":"y","16.80":"y"},"outlook-com":{"2020-03":"y","2024-01":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"samsung-email":{"android":{"7.0":"y"}},"sfr":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"n"},"android":{"2020-03":"n"}},"thunderbird":{"macos":{"68.5":"y"}},"aol":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"yahoo":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"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":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts."}
},
{
@@ -2192,7 +2224,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/kQbnzGXMSxMg2NDGmrcxIXOHrROwokTB29RcSssnkoPlS/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"13.1":"y"}},"gmail":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"a #1"},"android":{"2019-10":"a #1"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-10":"y","2021-03":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"outlook":{"windows":{"2003":"y","2007":"y","2010":"y","2013":"y","2016":"y","2019":"y"},"windows-mail":{"2019-10":"y"},"macos":{"2011":"y","2016":"y","16.80":"y"},"outlook-com":{"2019-10":"y","2024-01":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"n"},"android":{"2019-10":"n"}},"thunderbird":{"macos":{"60.3":"y"}},"aol":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"yahoo":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"a #2"}},"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":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"a #3"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"a #3"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts.","2":"Buggy. The first `<head>` in the HTML is removed, so `<style>` elements need to be in a second `<head>` element.","3":"Partial. Only when used as class or ID selectors"}
"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":"Partial. Only when used as class or ID selectors"}
},
{
@@ -2208,7 +2240,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/kQbnzGXMSxMg2NDGmrcxIXOHrROwokTB29RcSssnkoPlS/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"13.1":"y"}},"gmail":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"a #1"},"android":{"2019-10":"a #1"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-10":"y","2021-03":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"outlook":{"windows":{"2003":"y","2007":"y","2010":"y","2013":"y","2016":"y","2019":"y"},"windows-mail":{"2019-10":"y"},"macos":{"2011":"y","2016":"y","16.80":"y"},"outlook-com":{"2019-10":"y","2024-01":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"n"},"android":{"2019-10":"n"}},"thunderbird":{"macos":{"60.3":"y"}},"aol":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"yahoo":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"a #2"}},"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":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"y"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"y"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"n"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts.","2":"Buggy. The first `<head>` in the HTML is removed, so `<style>` elements need to be in a second `<head>` element."}
"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."}
},
{
@@ -2224,7 +2256,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/kQbnzGXMSxMg2NDGmrcxIXOHrROwokTB29RcSssnkoPlS/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"13.1":"y"}},"gmail":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"a #1"},"android":{"2019-10":"a #1"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-10":"y","2021-03":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"outlook":{"windows":{"2003":"y","2007":"y","2010":"y","2013":"y","2016":"y","2019":"y"},"windows-mail":{"2019-10":"y"},"macos":{"2011":"y","2016":"y","16.80":"y"},"outlook-com":{"2019-10":"y","2024-01":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"n"},"android":{"2019-10":"n"}},"thunderbird":{"macos":{"60.3":"y"}},"aol":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"yahoo":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"a #2"}},"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":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"n"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts.","2":"Buggy. The first `<head>` in the HTML is removed, so `<style>` elements need to be in a second `<head>` element."}
"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."}
},
{
@@ -2240,7 +2272,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/kQbnzGXMSxMg2NDGmrcxIXOHrROwokTB29RcSssnkoPlS/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"13.1":"y"}},"gmail":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"a #1"},"android":{"2019-10":"a #1"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-10":"y","2021-03":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"outlook":{"windows":{"2003":"y","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2019-10":"n"},"macos":{"2011":"y","2016":"y","16.80":"y"},"outlook-com":{"2019-10":"y","2024-01":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"n"},"android":{"2019-10":"n"}},"thunderbird":{"macos":{"60.3":"y"}},"aol":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"yahoo":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"a #2"}},"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 #3"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"y"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"y"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail 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."}
"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."}
},
{
@@ -2336,7 +2368,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/VYmPi84Nw2pMoQLeljigICaH0QudjS2xc2CgpvPbEW7FZ/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12":"a #1","13.4":"y"}},"gmail":{"desktop-webmail":{"2020-04":"y"},"ios":{"2020-04":"a #3"},"android":{"2020-04":"a #3"},"mobile-webmail":{"2020-04":"n"}},"orange":{"desktop-webmail":{"2020-04":"y","2021-03":"n"},"ios":{"2020-04":"y"},"android":{"2020-04":"y"}},"outlook":{"windows":{"2003":"n","2007":"n #2","2010":"n #2","2013":"n #2","2016":"n #2","2019":"n #2"},"windows-mail":{"2020-04":"n #2"},"macos":{"2011":"a #1","2016":"a #1","16.80":"y"},"outlook-com":{"2020-04":"y","2024-01":"y"},"ios":{"2020-04":"y"},"android":{"2020-04":"y"}},"samsung-email":{"android":{"7.0":"y"}},"sfr":{"desktop-webmail":{"2020-04":"y"},"ios":{"2020-04":"y"},"android":{"2020-04":"y"}},"thunderbird":{"macos":{"68.7":"y"}},"aol":{"desktop-webmail":{"2020-04":"y"},"ios":{"2020-04":"y"},"android":{"2020-04":"y"}},"yahoo":{"desktop-webmail":{"2020-04":"y"},"ios":{"2020-04":"y"},"android":{"2020-04":"y"}},"protonmail":{"desktop-webmail":{"2020-04":"y"},"ios":{"2020-04":"y"},"android":{"2020-04":"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":"y"}},"gmx":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"y"},"android":{"2022-07":"y"}},"web-de":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"y"},"android":{"2022-07":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-07":"y"},"android":{"2022-07":"y"}}},
"notes":"`text-decoration-color` is not supported in Internet Explorer.",
"notes_by_num":{"1":"Supported with prefix `-webkit-`.","2":"Not supported, but the proprietary `text-underline-color` property can be used instead.","3":"Partial. Not supported with non Gmail accounts."}
"notes_by_num":{"1":"Supported with prefix `-webkit-`.","2":"Not supported, but the proprietary `text-underline-color` property can be used instead.","3":"Partial. Not supported with non Google accounts."}
},
{
@@ -2345,7 +2377,7 @@
"description":"Sets the kind of decoration that is used on text in an element, such as an underline or overline.",
"url":"https://www.caniemail.com/features/css-text-decoration-line/",
"category":"css",
"tags":[],
"tags":["i18n"],
"keywords":"underline, overline, line-through",
"last_test_date":"2023-12-06",
"test_url":"https://www.caniemail.com/tests/css-text-decoration-line.html",
@@ -2361,7 +2393,7 @@
"description":"Specifies how overlines and underlines are drawn when they pass over glyph ascenders and descenders.",
"url":"https://www.caniemail.com/features/css-text-decoration-skip-ink/",
"category":"css",
"tags":[],
"tags":["i18n"],
"keywords":"underline",
"last_test_date":"2023-12-08",
"test_url":"https://www.caniemail.com/tests/css-text-decoration-skip-ink.html",
@@ -2416,7 +2448,7 @@
"test_results_url":"https: //app.emailonacid.com/app/acidtest/DkqbHs69ek5UnK6uhZ7Uj0n5GVQNTP4Z1FvgXvnKyEoTM/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"a #1"},"android":{"2019-02":"a #1"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2020-01":"y","2021-03":"y"},"ios":{"2020-01":"n"},"android":{"2020-01":"y"}},"outlook":{"windows":{"2007":"a #2 #3","2010":"a #2 #3","2013":"a #2 #3","2016":"a #2 #3","2019":"a #2 #3"},"windows-mail":{"2019-02":"a #2 #3"},"macos":{"2019-02":"y","16.80":"y"},"outlook-com":{"2019-02":"y","2024-01":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"aol":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"samsung-email":{"android":{"5.0.10.2":"y"}},"sfr":{"desktop-webmail":{"2020-01":"y"},"ios":{"2020-01":"n"},"android":{"2020-01":"y"}},"thunderbird":{"macos":{"68.4":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"n"},"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":"y"}},"gmx":{"desktop-webmail":{"2022-07":"a #4"},"ios":{"2022-07":"a #5"},"android":{"2022-07":"y"}},"web-de":{"desktop-webmail":{"2022-07":"a #4"},"ios":{"2022-07":"a #5"},"android":{"2022-07":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-07":"y"},"android":{"2022-07":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts.","2":"Partial. Not supported with multiple values.","3":"Partial. `overline` is not supported.","4":"Partial. Only supports the line property, not style, color or thickness.","5":"Partial. Only supports style, color or thickness when written with long hadn selectors."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts.","2":"Partial. Not supported with multiple values.","3":"Partial. `overline` is not supported.","4":"Partial. Only supports the line property, not style, color or thickness.","5":"Partial. Only supports style, color or thickness when written with long hadn selectors."}
},
{
@@ -2496,7 +2528,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/DkqbHs69ek5UnK6uhZ7Uj0n5GVQNTP4Z1FvgXvnKyEoTM/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"a #1"},"android":{"2019-02":"a #1"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2020-01":"n #2","2021-03":"n"},"ios":{"2020-01":"y"},"android":{"2020-01":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2019-02":"n"},"macos":{"2019-02":"y","16.80":"y"},"outlook-com":{"2019-02":"y","2024-01":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"n"}},"aol":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"n"}},"samsung-email":{"android":{"5.0.10.2":"n"}},"sfr":{"desktop-webmail":{"2020-01":"y"},"ios":{"2020-01":"y"},"android":{"2020-01":"y"}},"thunderbird":{"macos":{"68.4":"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":"y"}},"gmx":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"y"},"android":{"2022-08":"y"}},"web-de":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"y"},"android":{"2022-08":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-08":"y"},"android":{"2022-08":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts.","2":"Not supported. `overflow` is replaced by `java-script`."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts.","2":"Not supported. `overflow` is replaced by `java-script`."}
},
{
@@ -2537,7 +2569,7 @@
"description":null,
"url":"https://www.caniemail.com/features/css-text-underline-offset/",
"category":"css",
"tags":[],
"tags":["i18n"],
"keywords":"underline",
"last_test_date":"2023-01-16",
"test_url":"https://www.caniemail.com/tests/css-text-decoration.html",
@@ -2553,7 +2585,7 @@
"description":"Specifies the position of the underline which is set using the `text-decoration property`'s underline value.",
"url":"https://www.caniemail.com/features/css-text-underline-position/",
"category":"css",
"tags":[],
"tags":["i18n"],
"keywords":"underline",
"last_test_date":"2023-12-08",
"test_url":"https://www.caniemail.com/tests/css-text-underline-position.html",
@@ -2592,7 +2624,7 @@
"test_results_url":"",
"stats":{"apple-mail":{"macos":{"13":"y"},"ios":{"13":"y"}},"gmail":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"a #1"},"android":{"2020-02":"a #1"},"mobile-webmail":{"2020-02":"y"}},"outlook":{"windows":{"2003":"n","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-02":"n"},"macos":{"2016":"y","16.80":"n"},"outlook-com":{"2020-02":"n","2024-01":"n"},"ios":{"2020-02":"n"},"android":{"2020-02":"n"}},"samsung-email":{"android":{"9.0":"y"}},"thunderbird":{"windows":{"2020-02":"y"},"macos":{"68.4":"y"}},"aol":{"desktop-webmail":{"2020-02":"n"},"ios":{"2020-02":"n"},"android":{"2020-02":"n"}},"yahoo":{"desktop-webmail":{"2020-02":"n"},"ios":{"2020-02":"n"},"android":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2020-02":"y","2021-03":"n"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"sfr":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"y"},"android":{"2022-08":"y"}},"web-de":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"y"},"android":{"2022-08":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-08":"y"},"android":{"2022-08":"y"}}},
"notes":"",
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts."}
},
{
@@ -2688,7 +2720,7 @@
"test_results_url":"",
"stats":{"apple-mail":{"macos":{"13":"y"},"ios":{"13":"y"}},"gmail":{"desktop-webmail":{"2020-02":"a #1"},"ios":{"2020-02":"a #2"},"android":{"2020-02":"a #2"},"mobile-webmail":{"2020-02":"y"}},"outlook":{"windows":{"2003":"n","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-02":"n"},"macos":{"2016":"y","16.80":"y"},"outlook-com":{"2020-02":"a #1","2024-01":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"samsung-email":{"android":{"9.0":"y"}},"thunderbird":{"windows":{"2020-02":"y"},"macos":{"68.4":"y"}},"aol":{"desktop-webmail":{"2020-02":"n"},"ios":{"2020-02":"n"},"android":{"2020-02":"n"}},"yahoo":{"desktop-webmail":{"2020-02":"n"},"ios":{"2020-02":"n"},"android":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2020-02":"y","2021-03":"n"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"sfr":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"y"},"android":{"2022-08":"y"}},"web-de":{"desktop-webmail":{"2022-08":"n"},"ios":{"2022-08":"y"},"android":{"2022-08":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-08":"y"},"android":{"2022-08":"y"}}},
"notes":"",
"notes_by_num":{"1":"Partial. Webmail rendering doesn't work in IE.","2":"Partial. Not supported with non Gmail accounts."}
"notes_by_num":{"1":"Partial. Webmail rendering doesn't work in IE.","2":"Partial. Not supported with non Google accounts."}
},
{
@@ -2851,6 +2883,22 @@
"notes_by_num":{}
},
{
"slug":"css-user-select",
"title":"user-select",
"description":"Controls whether the user can select text.",
"url":"https://www.caniemail.com/features/css-user-select/",
"category":"css",
"tags":[],
"keywords":"select, copy",
"last_test_date":"2024-02-14",
"test_url":"https://www.caniemail.com/tests/css-user-select.html",
"test_results_url":"https://testi.at/proj/9zjptajgcxyzc74ockp",
"stats":{"apple-mail":{"macos":{"2024-02":"y #1"},"ios":{"2024-02":"a #1 #2"}},"gmail":{"desktop-webmail":{"2024-02":"n #3"},"ios":{"2024-02":"n #3"},"android":{"2024-02":"n"},"mobile-webmail":{"2024-02":"u"}},"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":{"2024-02":"n #3"},"outlook-com":{"2024-02":"n #3"},"ios":{"2024-02":"n #3"},"android":{"2024-02":"n"}},"yahoo":{"desktop-webmail":{"2024-02":"n #3"},"ios":{"2024-02":"n"},"android":{"2024-02":"n"}},"aol":{"desktop-webmail":{"2024-02":"n #3"},"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":"u"}},"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":"n #3"},"ios":{"2024-02":"u"},"android":{"2024-02":"u"}},"web-de":{"desktop-webmail":{"2024-02":"n #3"},"ios":{"2024-02":"n"},"android":{"2024-02":"u"}},"ionos-1and1":{"desktop-webmail":{"2024-02":"u"},"android":{"2024-02":"u"}}},
"notes":null,
"notes_by_num":{"1":"Works with `-webkit` prefix","2":"`all` value does not work","3":"Property is stripped from style tag"}
},
{
"slug":"css-variables",
"title":"CSS Variables (Custom Properties)",
@@ -2864,7 +2912,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/3viCexhHsrjaP9YS8RwzNwikbf4C0akxPFhK8xgyltxpe/list",
"stats":{"apple-mail":{"macos":{"10.3":"y"},"ios":{"10.3":"y","12.2":"y"}},"gmail":{"desktop-webmail":{"2019-08":"n #1"},"ios":{"2019-08":"n #1 #2"},"android":{"2019-08":"n #1 #2"},"mobile-webmail":{"2020-02":"n #1"}},"orange":{"desktop-webmail":{"2019-08":"y","2021-03":"n"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"outlook":{"windows":{"2003":"n","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-01":"n"},"macos":{"2011":"y","2016":"y","16.80":"n"},"outlook-com":{"2019-08":"n","2020-02":"n","2024-01":"n"},"ios":{"2019-08":"n","2020-02":"n"},"android":{"2019-08":"n","2020-02":"n"}},"samsung-email":{"android":{"6.0":"y","9.0":"y"}},"sfr":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"thunderbird":{"windows":{"2020-02":"y"},"macos":{"60.3":"y","68.4":"y"}},"aol":{"desktop-webmail":{"2020-01":"n"},"ios":{"2020-01":"n"},"android":{"2020-01":"n"}},"yahoo":{"desktop-webmail":{"2019-08":"n","2020-02":"n"},"ios":{"2019-08":"n","2020-02":"n"},"android":{"2019-08":"n","2020-02":"n"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"n #1"}},"fastmail":{"desktop-webmail":{"2021-07":"n #1"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"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. The `var()` function is supported, but not the variable declaration.","2":"Partial. Not supported with non Gmail accounts."}
"notes_by_num":{"1":"Partial. The `var()` function is supported, but not the variable declaration.","2":"Partial. Not supported with non Google accounts."}
},
{
@@ -2912,7 +2960,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/DkqbHs69ek5UnK6uhZ7Uj0n5GVQNTP4Z1FvgXvnKyEoTM/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"a #1 #2"},"android":{"2019-02":"a #1 #2"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2020-01":"y","2021-03":"y"},"ios":{"2020-01":"y"},"android":{"2020-01":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2019-02":"n"},"macos":{"2019-02":"y #2","16.80":"y"},"outlook-com":{"2019-02":"y #2","2024-01":"y"},"ios":{"2019-02":"y #2"},"android":{"2019-02":"y #2"}},"yahoo":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"a #2"},"android":{"2019-02":"a #2"}},"aol":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"samsung-email":{"android":{"5.0.10.2":"n"}},"sfr":{"desktop-webmail":{"2020-01":"y"},"ios":{"2020-01":"y"},"android":{"2020-01":"a #2"}},"thunderbird":{"macos":{"68.4":"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":"y"}},"gmx":{"desktop-webmail":{"2022-09":"y"},"ios":{"2022-09":"y"},"android":{"2022-09":"a #2"}},"web-de":{"desktop-webmail":{"2022-09":"y"},"ios":{"2022-09":"y"},"android":{"2022-09":"a #2"}},"ionos-1and1":{"desktop-webmail":{"2022-09":"y"},"android":{"2022-09":"a #2"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts.","2":"Partial. `pre` value is not supported."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts.","2":"Partial. `pre` value is not supported."}
},
{
@@ -2960,7 +3008,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/Ingjv4scPnWSgh0u0Fr7EctmGksq4DyF7Pw9PQcENfZ37/list",
"stats":{"apple-mail":{"macos":{"13":"y","15":"y"},"ios":{"14":"y","15":"y"}},"gmail":{"desktop-webmail":{"2022-05":"y"},"ios":{"2022-05":"a #1"},"android":{"2022-05":"a #1"},"mobile-webmail":{"2022-05":"y"}},"orange":{"desktop-webmail":{"2022-05":"n"},"ios":{"2022-05":"n"},"android":{"2022-05":"n"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2022-05":"n"},"macos":{"16.62":"y","16.80":"y"},"outlook-com":{"2022-05":"y","2024-01":"y"},"ios":{"2022-05":"y"},"android":{"2022-05":"y"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2022-05":"y"},"ios":{"2022-05":"y"},"android":{"2022-05":"y"}},"thunderbird":{"macos":{"91.5.0":"y"}},"aol":{"desktop-webmail":{"2022-05":"n"},"ios":{"2022-05":"n"},"android":{"2022-05":"n"}},"yahoo":{"desktop-webmail":{"2022-05":"n"},"ios":{"2022-05":"n"},"android":{"2022-05":"n"}},"protonmail":{"desktop-webmail":{"2022-05":"y"},"ios":{"2022-05":"y"},"android":{"2022-05":"y"}},"hey":{"desktop-webmail":{"2022-05":"y"}},"mail-ru":{"desktop-webmail":{"2022-05":"y"}},"fastmail":{"desktop-webmail":{"2022-05":"n"}},"laposte":{"desktop-webmail":{"2022-05":"y"}},"gmx":{"desktop-webmail":{"2022-05":"n","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. Not supported with Non Gmail Accounts."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts."}
},
{
@@ -2995,6 +3043,22 @@
"notes_by_num":{"1":"Buggy. Element's content are still kept."}
},
{
"slug":"html-acronym",
"title":"<acronym> element",
"description":"Represents an abbreviation or acronym.",
"url":"https://www.caniemail.com/features/html-acronym/",
"category":"html",
"tags":[],
"keywords":null,
"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"}}},
"notes":null,
"notes_by_num":{"1":"Buggy. `title` attribute is removed but keeps `<acronym>` tag."}
},
{
"slug":"html-address",
"title":"address",
@@ -3022,9 +3086,9 @@
"last_test_date":"2021-11-10",
"test_url":"https://www.caniemail.com/tests/html-align.html",
"test_results_url":"https://app.emailonacid.com/app/acidtest/00FrYZOGg0JvRT2Q0A5jI1jJFZ1cOzDvhWFMdE5883gj3/list",
"stats":{"apple-mail":{"macos":{"15":"y"},"ios":{"15":"y"}},"gmail":{"desktop-webmail":{"2021-11":"y"},"ios":{"2021-11":"y"},"android":{"2021-11":"y"},"mobile-webmail":{"2021-11":"y"}},"orange":{"desktop-webmail":{"2021-11":"y"},"ios":{"2021-11":"y"},"android":{"2021-11":"y"}},"outlook":{"windows":{"2007":"y","2010":"y","2013":"y","2016":"y","2019":"y"},"windows-mail":{"2021-11":"y"},"macos":{"16.56":"y","16.80":"y"},"outlook-com":{"2021-11":"y","2024-01":"y"},"ios":{"2021-11":"y"},"android":{"2021-11":"y"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2021-11":"y"},"ios":{"2021-11":"y"},"android":{"2021-11":"y"}},"thunderbird":{"macos":{"78.14":"y"}},"aol":{"desktop-webmail":{"2021-11":"y"},"ios":{"2021-11":"y"},"android":{"2021-11":"y"}},"yahoo":{"desktop-webmail":{"2021-11":"y"},"ios":{"2021-11":"y"},"android":{"2021-11":"y"}},"protonmail":{"desktop-webmail":{"2021-11":"a #1"},"ios":{"2021-11":"y"},"android":{"2021-11":"y"}},"hey":{"desktop-webmail":{"2021-11":"a #2"}},"mail-ru":{"desktop-webmail":{"2021-11":"y"}},"fastmail":{"desktop-webmail":{"2021-11":"y"}},"laposte":{"desktop-webmail":{"2021-11":"y"}},"gmx":{"desktop-webmail":{"2022-11":"y"},"ios":{"2022-11":"y"},"android":{"2022-11":"y"}},"web-de":{"desktop-webmail":{"2022-11":"y"},"ios":{"2022-11":"y"},"android":{"2022-11":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"y"},"android":{"2022-11":"y"}}},
"stats":{"apple-mail":{"macos":{"15":"y"},"ios":{"15":"y"}},"gmail":{"desktop-webmail":{"2021-11":"y"},"ios":{"2021-11":"y"},"android":{"2021-11":"y"},"mobile-webmail":{"2021-11":"y"}},"orange":{"desktop-webmail":{"2021-11":"y"},"ios":{"2021-11":"y"},"android":{"2021-11":"y"}},"outlook":{"windows":{"2007":"y","2010":"y","2013":"y","2016":"y","2019":"y"},"windows-mail":{"2021-11":"y"},"macos":{"16.56":"y","16.80":"a #3"},"outlook-com":{"2021-11":"y","2024-01":"y"},"ios":{"2021-11":"y"},"android":{"2021-11":"y"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2021-11":"y"},"ios":{"2021-11":"y"},"android":{"2021-11":"y"}},"thunderbird":{"macos":{"78.14":"y"}},"aol":{"desktop-webmail":{"2021-11":"y"},"ios":{"2021-11":"y"},"android":{"2021-11":"y"}},"yahoo":{"desktop-webmail":{"2021-11":"y"},"ios":{"2021-11":"y"},"android":{"2021-11":"y"}},"protonmail":{"desktop-webmail":{"2021-11":"a #1"},"ios":{"2021-11":"y"},"android":{"2021-11":"y"}},"hey":{"desktop-webmail":{"2021-11":"a #2"}},"mail-ru":{"desktop-webmail":{"2021-11":"y"}},"fastmail":{"desktop-webmail":{"2021-11":"y"}},"laposte":{"desktop-webmail":{"2021-11":"y"}},"gmx":{"desktop-webmail":{"2022-11":"y"},"ios":{"2022-11":"y"},"android":{"2022-11":"y"}},"web-de":{"desktop-webmail":{"2022-11":"y"},"ios":{"2022-11":"y"},"android":{"2022-11":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"y"},"android":{"2022-11":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. `<img>` elements are wrapped in a `<span>` so `left` and `right` values have no effect.","2":"Partial. Not supported on `<img>` elements."}
"notes_by_num":{"1":"Partial. `<img>` elements are wrapped in a `<span>` so `left` and `right` values have no effect.","2":"Partial. Not supported on `<img>` elements.","3":"Partial. `left` and `right` do not work on `<table>` elements. Use `float:left` or `float:right` styles instead."}
},
{
@@ -3152,7 +3216,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/oxaaoE6R3ur4T9fAPzVsQ3G2R7p1c9axDm7LLgC3cKw0F/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"a #1"},"android":{"2019-02":"a #1"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2019-08":"y","2021-03":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"outlook":{"windows":{"2007":"a #2","2010":"a #2","2013":"a #2","2016":"a #2","2019":"a #2"},"windows-mail":{"2019-02":"n"},"macos":{"2019-02":"y","16.80":"y"},"outlook-com":{"2019-02":"y","2024-01":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"aol":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"samsung-email":{"android":{"5.0.10.2":"y","6.0.04.6":"y"}},"sfr":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"thunderbird":{"macos":{"60.5.0":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"n"}},"mail-ru":{"desktop-webmail":{"2020-10":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"t-online-de":{"desktop-webmail":{"2021-11":"y"}},"free-fr":{"desktop-webmail":{"2021-11":"y"}},"gmx":{"desktop-webmail":{"2022-11":"y"},"ios":{"2022-11":"y"},"android":{"2022-11":"y"}},"web-de":{"desktop-webmail":{"2022-11":"y"},"ios":{"2022-11":"y"},"android":{"2022-11":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"y"},"android":{"2022-11":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts.","2":"Partial. Only supported on the `<body>` element."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts.","2":"Partial. Only supported on the `<body>` element."}
},
{
@@ -3232,7 +3296,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/MOk8g8TWwCTL4vLGrdMIgu3Vncqdxif6KlK4g8HfUV1mB/list",
"stats":{"apple-mail":{"macos":{"10.3":"y","12.4":"y"},"ios":{"10.3":"y","12.4":"y"}},"gmail":{"desktop-webmail":{"2019-09":"y"},"ios":{"2019-09":"a #1"},"android":{"2019-09":"y"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2020-01":"y","2021-03":"y"},"ios":{"2020-01":"y"},"android":{"2020-01":"y"}},"outlook":{"windows":{"2003":"n","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-01":"n"},"macos":{"2011":"y","2016":"y","2019":"y","16.80":"y"},"outlook-com":{"2019-09":"y","2024-01":"y"},"ios":{"2019-06":"n"},"android":{"2019-06":"y"}},"samsung-email":{"android":{"6.0":"y","9.0":"y"}},"sfr":{"desktop-webmail":{"2020-01":"a #3"},"ios":{"2020-01":"y"},"android":{"2020-01":"y"}},"thunderbird":{"macos":{"68.4":"a #2"}},"yahoo":{"desktop-webmail":{"2019-09":"n"},"ios":{"2019-09":"n"},"android":{"2019-09":"n"}},"aol":{"desktop-webmail":{"2019-09":"n"},"ios":{"2019-09":"n"},"android":{"2019-09":"n"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"n"}},"hey":{"desktop-webmail":{"2020-06":"n"}},"mail-ru":{"desktop-webmail":{"2020-10":"a #2"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"web-de":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"a #3"},"android":{"2022-11":"n"}}},
"notes":"",
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts.","2":"Buggy. Interacting with the element submits the form it belongs in.","3":"Partial. The element is present but is not interactive."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts.","2":"Buggy. Interacting with the element submits the form it belongs in.","3":"Partial. The element is present but is not interactive."}
},
{
@@ -3248,7 +3312,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/MOk8g8TWwCTL4vLGrdMIgu3Vncqdxif6KlK4g8HfUV1mB/list",
"stats":{"apple-mail":{"macos":{"10.3":"y","12.4":"y"},"ios":{"10.3":"y","12.4":"y"}},"gmail":{"desktop-webmail":{"2019-09":"y"},"ios":{"2019-09":"a #1"},"android":{"2019-09":"y"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2020-01":"y","2021-03":"y"},"ios":{"2020-01":"y"},"android":{"2020-01":"y"}},"outlook":{"windows":{"2003":"n","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-01":"n"},"macos":{"2011":"n","2016":"n","2019":"n","16.80":"n"},"outlook-com":{"2019-09":"y","2024-01":"n"},"ios":{"2019-06":"n"},"android":{"2019-06":"y"}},"samsung-email":{"android":{"6.0":"y","9.0":"y"}},"sfr":{"desktop-webmail":{"2020-01":"a #2"},"ios":{"2020-01":"y"},"android":{"2020-01":"y"}},"thunderbird":{"macos":{"68.4":"y"}},"yahoo":{"desktop-webmail":{"2019-09":"y"},"ios":{"2019-09":"n"},"android":{"2019-09":"y"}},"aol":{"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":"n"}},"hey":{"desktop-webmail":{"2020-06":"n"}},"mail-ru":{"desktop-webmail":{"2020-10":"a #2"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"web-de":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"a #2"},"android":{"2022-11":"n"}}},
"notes":"",
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts.","2":"Partial. The element is present but is not interactive."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts.","2":"Partial. The element is present but is not interactive."}
},
{
@@ -3280,7 +3344,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/DkqbHs69ek5UnK6uhZ7Uj0n5GVQNTP4Z1FvgXvnKyEoTM/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"a #1","2023-03":"y"},"android":{"2019-02":"a #1","2023-03":"y"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2020-01":"y","2021-03":"y"},"ios":{"2020-01":"y"},"android":{"2020-01":"y"}},"outlook":{"windows":{"2007":"y","2010":"y","2013":"y","2016":"y","2019":"y"},"windows-mail":{"2019-02":"y"},"macos":{"2019-02":"y","16.80":"y"},"outlook-com":{"2019-02":"y","2024-01":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"aol":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"samsung-email":{"android":{"5.0.10.2":"y"}},"sfr":{"desktop-webmail":{"2020-01":"y"},"ios":{"2020-01":"y"},"android":{"2020-01":"y"}},"thunderbird":{"macos":{"68.4":"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":"y"}},"gmx":{"desktop-webmail":{"2022-11":"y"},"ios":{"2022-11":"y"},"android":{"2022-11":"y"}},"web-de":{"desktop-webmail":{"2022-11":"y"},"ios":{"2022-11":"y"},"android":{"2022-11":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"y"},"android":{"2022-11":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts."}
},
{
@@ -3376,7 +3440,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/MOk8g8TWwCTL4vLGrdMIgu3Vncqdxif6KlK4g8HfUV1mB/list",
"stats":{"apple-mail":{"macos":{"10.3":"y","12.4":"y"},"ios":{"10.3":"y","12.4":"y"}},"gmail":{"desktop-webmail":{"2019-09":"y"},"ios":{"2019-09":"a #1"},"android":{"2019-09":"y"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2020-01":"n #4","2021-03":"n"},"ios":{"2020-01":"n #4"},"android":{"2020-01":"n #4"}},"outlook":{"windows":{"2003":"n","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-01":"n"},"macos":{"2011":"n","2016":"n","2019":"n","16.80":"a #6"},"outlook-com":{"2019-02":"y #2","2019-10":"a #2 #3","2024-01":"a #6"},"ios":{"2019-06":"y"},"android":{"2019-06":"y"}},"samsung-email":{"android":{"6.0":"y","9.0":"y"}},"sfr":{"desktop-webmail":{"2020-01":"n #4"},"ios":{"2020-01":"y"},"android":{"2020-01":"y"}},"thunderbird":{"macos":{"68.4":"y"}},"yahoo":{"desktop-webmail":{"2019-09":"y"},"ios":{"2019-09":"n"},"android":{"2019-09":"y"}},"aol":{"desktop-webmail":{"2019-09":"y"},"ios":{"2019-09":"y"},"android":{"2019-09":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"n"},"ios":{"2020-03":"n"},"android":{"2020-03":"n #5"}},"hey":{"desktop-webmail":{"2020-06":"n"}},"mail-ru":{"desktop-webmail":{"2020-10":"n #6"}},"fastmail":{"desktop-webmail":{"2021-07":"n"}},"laposte":{"desktop-webmail":{"2021-08":"n #4"}},"gmx":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"web-de":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"n"},"android":{"2022-11":"n"}}},
"notes":"",
"notes_by_num":{"1":"Does not submit in iOS with non Gmail accounts.","2":"On submit name values are prefixed with `x_`.","3":"Does not submit in preview pane, but does when the email is opened in a new window.","4":"Not supported. `<form>` is transformed into `<noform>`.","5":"Not supported. The `<form>` and its entire content is removed.","6":"Not supported. The `<form>` is there but values are not submitted."}
"notes_by_num":{"1":"Does not submit in iOS with non Google accounts.","2":"On submit name values are prefixed with `x_`.","3":"Does not submit in preview pane, but does when the email is opened in a new window.","4":"Not supported. `<form>` is transformed into `<noform>`.","5":"Not supported. The `<form>` and its entire content is removed.","6":"Not supported. The `<form>` is there but values are not submitted."}
},
{
@@ -3411,6 +3475,22 @@
"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":"Buggy. Sizes set in attributes don't scale in 120 dpi mode."}
},
{
"slug":"html-hidden",
"title":"hidden attribute",
"description":"The global HTML `hidden` attribute",
"url":"https://www.caniemail.com/features/html-hidden/",
"category":"html",
"tags":["accessibility"],
"keywords":null,
"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"}}},
"notes":null,
"notes_by_num":{"1":"Does not support the unquoted attribute value syntax `<div hidden></div>`"}
},
{
"slug":"html-hr",
"title":"<hr> element",
@@ -3520,7 +3600,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/MOk8g8TWwCTL4vLGrdMIgu3Vncqdxif6KlK4g8HfUV1mB/list",
"stats":{"apple-mail":{"macos":{"10.3":"y","12.4":"y"},"ios":{"10.3":"y","12.4":"y"}},"gmail":{"desktop-webmail":{"2019-09":"y"},"ios":{"2019-09":"a #1"},"android":{"2019-09":"y"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2020-01":"n #2","2021-03":"n"},"ios":{"2020-01":"n #2"},"android":{"2020-01":"n #2"}},"outlook":{"windows":{"2003":"n","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-01":"n"},"macos":{"2011":"y","2016":"y","2019":"y","16.80":"y"},"outlook-com":{"2019-09":"y","2024-01":"y"},"ios":{"2019-06":"y"},"android":{"2019-06":"y"}},"samsung-email":{"android":{"6.0":"y","9.0":"y"}},"sfr":{"desktop-webmail":{"2020-01":"n #2"},"ios":{"2020-01":"y","2021-06":"n #1"},"android":{"2020-01":"y","2021-06":"n #1"}},"thunderbird":{"macos":{"68.4":"a #3"}},"yahoo":{"desktop-webmail":{"2019-09":"y"},"ios":{"2019-09":"y"},"android":{"2019-09":"y"}},"aol":{"desktop-webmail":{"2019-09":"y"},"ios":{"2019-09":"y"},"android":{"2019-09":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"n"},"ios":{"2020-03":"n"},"android":{"2020-03":"n"}},"hey":{"desktop-webmail":{"2020-06":"n"}},"mail-ru":{"desktop-webmail":{"2020-10":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"n #1"}},"gmx":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"web-de":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"n"},"android":{"2022-11":"n"}}},
"notes":"",
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts.","2":"Not supported. `<input>` is transformed into `<noinput>`.","3":"Buggy. Interacting with the element submits the form it belongs in."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts.","2":"Not supported. `<input>` is transformed into `<noinput>`.","3":"Buggy. Interacting with the element submits the form it belongs in."}
},
{
@@ -3536,7 +3616,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/MOk8g8TWwCTL4vLGrdMIgu3Vncqdxif6KlK4g8HfUV1mB/list",
"stats":{"apple-mail":{"macos":{"10.3":"y","12.4":"y"},"ios":{"10.3":"y","12.4":"y"}},"gmail":{"desktop-webmail":{"2019-09":"y"},"ios":{"2019-09":"a #1"},"android":{"2019-09":"y"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2020-01":"n #2","2021-03":"n"},"ios":{"2020-01":"n #2"},"android":{"2020-01":"n #2"}},"outlook":{"windows":{"2003":"n","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-01":"n"},"macos":{"2011":"n","2016":"n","2019":"n","16.80":"n"},"outlook-com":{"2019-09":"y","2024-01":"n"},"ios":{"2019-06":"y"},"android":{"2019-06":"y"}},"samsung-email":{"android":{"6.0":"y","9.0":"y"}},"sfr":{"desktop-webmail":{"2020-01":"n #2"},"ios":{"2020-01":"y","2021-06":"n #1"},"android":{"2020-01":"y","2021-06":"n #1"}},"thunderbird":{"macos":{"68.4":"y"}},"yahoo":{"desktop-webmail":{"2019-09":"y"},"ios":{"2019-09":"n"},"android":{"2019-09":"y"}},"aol":{"desktop-webmail":{"2019-09":"y"},"ios":{"2019-09":"y"},"android":{"2019-09":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"n"},"ios":{"2020-03":"n"},"android":{"2020-03":"n"}},"hey":{"desktop-webmail":{"2020-06":"n"}},"mail-ru":{"desktop-webmail":{"2020-10":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"n #2"}},"gmx":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"web-de":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"n"},"android":{"2022-11":"n"}}},
"notes":"",
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts.","2":"Not supported. `<input>` is transformed into `<noinput>`."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts.","2":"Not supported. `<input>` is transformed into `<noinput>`."}
},
{
@@ -3595,12 +3675,12 @@
"category":"html",
"tags":[],
"keywords":"ul, ol, li, dl, dt, dd",
"last_test_date":"2020-04-20",
"last_test_date":"2024-02-17",
"test_url":"https://www.caniemail.com/tests/css-list.html",
"test_results_url":"https://app.emailonacid.com/app/acidtest/ifwlqtEsBCU23xVI7NgjBqvJlcJ4c20Akv3aRW3ugRJsP/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"13.4":"y"}},"gmail":{"desktop-webmail":{"2020-04":"y"},"ios":{"2020-04":"y"},"android":{"2020-04":"y"},"mobile-webmail":{"2020-04":"y"}},"orange":{"desktop-webmail":{"2020-04":"y","2021-03":"y"},"ios":{"2020-04":"y"},"android":{"2020-04":"y"}},"outlook":{"windows":{"2003":"y","2007":"y","2010":"y","2013":"y","2016":"y","2019":"y"},"windows-mail":{"2020-04":"y"},"macos":{"2011":"y","2016":"y","16.80":"y"},"outlook-com":{"2020-04":"y","2024-01":"y"},"ios":{"2020-04":"y"},"android":{"2020-04":"y"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2020-04":"y"},"ios":{"2020-04":"y"},"android":{"2020-04":"y"}},"thunderbird":{"macos":{"68.7":"y"}},"aol":{"desktop-webmail":{"2020-04":"y"},"ios":{"2020-04":"y"},"android":{"2020-04":"y"}},"yahoo":{"desktop-webmail":{"2020-04":"y"},"ios":{"2020-04":"y"},"android":{"2020-04":"y"}},"protonmail":{"desktop-webmail":{"2020-04":"y"},"ios":{"2020-04":"y"},"android":{"2020-04":"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":"y"}},"gmx":{"desktop-webmail":{"2022-06":"y"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"y"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"test_results_url":"https://testi.at/proj/5jbpc3r2cknp15ayua",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"13.4":"y"}},"gmail":{"desktop-webmail":{"2020-04":"y","2024-02":"a #1"},"ios":{"2020-04":"y","2024-02":"a #1"},"android":{"2020-04":"y","2024-02":"a #1"},"mobile-webmail":{"2020-04":"y","2024-02":"a #1"}},"orange":{"desktop-webmail":{"2020-04":"y","2021-03":"y"},"ios":{"2020-04":"y"},"android":{"2020-04":"y"}},"outlook":{"windows":{"2003":"a #1 #2","2007":"a #1 #2","2010":"a #1 #2","2013":"a #1 #2","2016":"a #1 #2","2019":"a #1 #2"},"windows-mail":{"2020-04":"y","2024-02":"a #1 #2"},"macos":{"2011":"y","2016":"y","16.80":"y","2024-02":"a #1"},"outlook-com":{"2020-04":"y","2024-01":"y","2024-02":"a #1"},"ios":{"2020-04":"y","2024-02":"a #1"},"android":{"2020-04":"y","2024-02":"a #1"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2020-04":"y"},"ios":{"2020-04":"y"},"android":{"2020-04":"y"}},"thunderbird":{"macos":{"68.7":"y"}},"aol":{"desktop-webmail":{"2020-04":"y","2024-02":"a #1"},"ios":{"2020-04":"y","2024-02":"a #1"},"android":{"2020-04":"y","2024-02":"a #1"}},"yahoo":{"desktop-webmail":{"2020-04":"y","2024-02":"a #1"},"ios":{"2020-04":"y","2024-02":"a #1"},"android":{"2020-04":"y","2024-02":"a #1"}},"protonmail":{"desktop-webmail":{"2020-04":"y"},"ios":{"2020-04":"y"},"android":{"2020-04":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"y","2024-02":"a #1"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"y"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"y"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
"notes":null,
"notes_by_num":null
"notes_by_num":{"1":"Partial. The `reversed` attribute on `<ol>` is not supported.","2":"Partial. Setting the `value` attribute to an `<li>` within an `<ol>` results in a different behaviour in comparison to browsers. The `<ol>` tag is closed before the `<li value=\"\">`. A new `<ol>` is added with the `start` attribute on it set to the value of the `value` attribute of the `<li>`."}
},
{
@@ -3680,7 +3760,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/Bzyzx8Z5Kvlfib1Fw9Ted8xtPE26RcjPSdUobdUywgJVm/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"a #1"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-08":"y","2021-03":"n"},"ios":{"2019-08":"y"},"android":{"2020-01":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2019-02":"n"},"macos":{"2019-02":"y","16.80":"y"},"outlook-com":{"2019-02":"y","2024-01":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"thunderbird":{"macos":{"60.8":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"n"}},"aol":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"n"}},"samsung-email":{"android":{"5.0.10.2":"y"}},"sfr":{"desktop-webmail":{"2020-01":"y"},"ios":{"2020-01":"y"},"android":{"2020-01":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"n"}},"mail-ru":{"desktop-webmail":{"2020-10":"n"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"web-de":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"n"},"android":{"2022-11":"n"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Supported with Non Gmail Accounts only."}
"notes_by_num":{"1":"Partial. Supported with non Google accounts only."}
},
{
@@ -3776,7 +3856,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/Bzyzx8Z5Kvlfib1Fw9Ted8xtPE26RcjPSdUobdUywgJVm/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"a #1"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-08":"y","2021-03":"n"},"ios":{"2019-08":"y"},"android":{"2020-01":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2019-02":"n"},"macos":{"2019-02":"y","16.80":"n"},"outlook-com":{"2019-02":"n","2024-01":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"n"}},"thunderbird":{"macos":{"60.8":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"n"}},"aol":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"n"}},"samsung-email":{"android":{"5.0.10.2":"y"}},"sfr":{"desktop-webmail":{"2020-01":"y"},"ios":{"2020-01":"y"},"android":{"2020-01":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"n"}},"mail-ru":{"desktop-webmail":{"2020-10":"n"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"web-de":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"n"},"android":{"2022-11":"n"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Supported with Non Gmail Accounts only."}
"notes_by_num":{"1":"Partial. Supported with non Google accounts only."}
},
{
@@ -3792,7 +3872,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/MOk8g8TWwCTL4vLGrdMIgu3Vncqdxif6KlK4g8HfUV1mB/list",
"stats":{"apple-mail":{"macos":{"10.3":"y","12.4":"y"},"ios":{"10.3":"y","12.4":"y"}},"gmail":{"desktop-webmail":{"2019-09":"n"},"ios":{"2019-09":"n"},"android":{"2019-09":"a #1"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2020-01":"a #2","2021-03":"n"},"ios":{"2020-01":"a #2"},"android":{"2020-01":"a #2"}},"outlook":{"windows":{"2003":"n","2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-01":"n"},"macos":{"2011":"y","2016":"y","2019":"y","16.80":"n"},"outlook-com":{"2019-09":"n","2024-01":"n"},"ios":{"2019-06":"y"},"android":{"2019-06":"y"}},"samsung-email":{"android":{"6.0":"y","9.0":"y"}},"sfr":{"desktop-webmail":{"2020-01":"a #2"},"ios":{"2020-01":"n"},"android":{"2020-01":"y"}},"thunderbird":{"macos":{"68.4":"y"}},"yahoo":{"desktop-webmail":{"2019-09":"n"},"ios":{"2019-09":"n"},"android":{"2019-09":"n"}},"aol":{"desktop-webmail":{"2019-09":"n"},"ios":{"2019-09":"n"},"android":{"2019-09":"n"}},"protonmail":{"desktop-webmail":{"2020-03":"a #2"},"ios":{"2020-03":"n"},"android":{"2020-03":"n"}},"hey":{"desktop-webmail":{"2020-06":"n"}},"mail-ru":{"desktop-webmail":{"2020-10":"n"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"a #2"}},"gmx":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"web-de":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"n"},"android":{"2022-11":"n"}}},
"notes":"",
"notes_by_num":{"1":"Partial. Only supported with non Gmail accounts.","2":"Partial. The `required` attribute is supported, but form elements are not."}
"notes_by_num":{"1":"Partial. Only supported with non Google accounts.","2":"Partial. The `required` attribute is supported, but form elements are not."}
},
{
@@ -3824,7 +3904,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/Bzyzx8Z5Kvlfib1Fw9Ted8xtPE26RcjPSdUobdUywgJVm/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2020-02":"a #1"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-08":"y","2021-03":"n"},"ios":{"2019-08":"y"},"android":{"2020-01":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2019-02":"n"},"macos":{"2019-02":"y","16.80":"y"},"outlook-com":{"2019-02":"y","2024-01":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"thunderbird":{"macos":{"60.8":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"aol":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"samsung-email":{"android":{"5.0.10.2":"y"}},"sfr":{"desktop-webmail":{"2020-01":"y"},"ios":{"2020-01":"y"},"android":{"2020-01":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"n"}},"mail-ru":{"desktop-webmail":{"2020-10":"n"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"web-de":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"n"},"android":{"2022-11":"n"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Supported with Non Gmail Accounts only."}
"notes_by_num":{"1":"Partial. Supported with non Google accounts only."}
},
{
@@ -3840,7 +3920,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/Bzyzx8Z5Kvlfib1Fw9Ted8xtPE26RcjPSdUobdUywgJVm/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"a #1"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-08":"y","2021-03":"n"},"ios":{"2019-08":"y"},"android":{"2020-01":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2019-02":"n"},"macos":{"2019-02":"y","16.80":"y"},"outlook-com":{"2019-02":"y","2024-01":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"thunderbird":{"macos":{"60.8":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"aol":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"samsung-email":{"android":{"5.0.10.2":"y"}},"sfr":{"desktop-webmail":{"2020-01":"y"},"ios":{"2020-01":"y"},"android":{"2020-01":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"n"}},"mail-ru":{"desktop-webmail":{"2020-10":"n"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"web-de":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"n"},"android":{"2022-11":"n"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Supported with Non Gmail Accounts only."}
"notes_by_num":{"1":"Partial. Supported with non Google accounts only."}
},
{
@@ -3856,7 +3936,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/Bzyzx8Z5Kvlfib1Fw9Ted8xtPE26RcjPSdUobdUywgJVm/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"n"},"ios":{"2019-02":"n"},"android":{"2019-02":"a #1"},"mobile-webmail":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2019-08":"y","2021-03":"n"},"ios":{"2019-08":"y"},"android":{"2020-01":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2019-02":"n"},"macos":{"2019-02":"y","16.80":"y"},"outlook-com":{"2019-02":"y","2024-01":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"thunderbird":{"macos":{"60.8":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"aol":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"samsung-email":{"android":{"5.0.10.2":"y"}},"sfr":{"desktop-webmail":{"2020-01":"y"},"ios":{"2020-01":"y"},"android":{"2020-01":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"n"}},"mail-ru":{"desktop-webmail":{"2020-10":"n"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"web-de":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"n"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"n"},"android":{"2022-11":"n"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Supported with Non Gmail Accounts only."}
"notes_by_num":{"1":"Partial. Supported with non Google accounts only."}
},
{
@@ -3952,7 +4032,7 @@
"test_results_url":"https: //app.emailonacid.com/app/acidtest/DkqbHs69ek5UnK6uhZ7Uj0n5GVQNTP4Z1FvgXvnKyEoTM/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"a #1"},"android":{"2019-02":"a #1"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2020-01":"y","2021-03":"y"},"ios":{"2020-01":"y"},"android":{"2020-01":"y"}},"outlook":{"windows":{"2007":"y","2010":"y","2013":"y","2016":"y","2019":"y"},"windows-mail":{"2019-02":"y"},"macos":{"2019-02":"y","16.80":"y"},"outlook-com":{"2019-02":"y","2024-01":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"aol":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"samsung-email":{"android":{"5.0.10.2":"y"}},"sfr":{"desktop-webmail":{"2020-01":"y"},"ios":{"2020-01":"y"},"android":{"2020-01":"y"}},"thunderbird":{"macos":{"68.4":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"n"}},"mail-ru":{"desktop-webmail":{"2020-10":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-11":"y"},"ios":{"2022-11":"y"},"android":{"2022-11":"y"}},"web-de":{"desktop-webmail":{"2022-11":"y"},"ios":{"2022-11":"y"},"android":{"2022-11":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"y"},"android":{"2022-11":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts."}
"notes_by_num":{"1":"Partial. Not supported with non Google accounts."}
},
{
@@ -3984,7 +4064,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/od5IYQtx8yIbIUbeRyQXnP0yzFKEm2E9CKa3FU4BcEXFv/list",
"stats":{"apple-mail":{"macos":{"10.3":"y","16.0":"y"},"ios":{"10.3":"y","12.3":"y","16.2":"y"}},"gmail":{"desktop-webmail":{"2019-06":"a #1","2023-01":"a #1"},"ios":{"2019-06":"a #1 #2","2023-01":"a #1 #2"},"android":{"2019-06":"a #1 #2"},"mobile-webmail":{"2020-02":"n","2023-01":"n"}},"orange":{"desktop-webmail":{"2019-06":"y","2021-03":"y","2023-02":"y"},"ios":{"2019-06":"y","2023-02":"y"},"android":{"2019-06":"n","2019-08":"y","2023-02":"y"}},"outlook":{"windows":{"2003":"y","2007":"a #4","2010":"a #4","2013":"a #4","2016":"a #4","2019":"a #4"},"windows-mail":{"2020-01":"a #4","2023-01":"a #4"},"macos":{"2011":"y","2016":"y","2019":"y","2021":"y","16.80":"y"},"outlook-com":{"2019-06":"y","2023-01":"y","2024-01":"y"},"ios":{"2019-06":"y","2023-01":"y"},"android":{"2019-06":"y"}},"samsung-email":{"android":{"5.0.10.2":"y","6.0":"y"}},"sfr":{"desktop-webmail":{"2019-06":"y","2023-02":"y"},"ios":{"2019-06":"n","2023-02":"n"},"android":{"2019-06":"n","2023-02":"n"}},"thunderbird":{"macos":{"60.3":"y","102.7":"y"},"windows":{"102.7":"y"}},"aol":{"desktop-webmail":{"2020-01":"y","2023-01":"y"},"ios":{"2020-01":"y","2023-01":"y"},"android":{"2020-01":"y"}},"yahoo":{"desktop-webmail":{"2019-06":"y #5","2023-01":"y"},"ios":{"2019-06":"y","2023-01":"y"},"android":{"2019-06":"a #3","2023-01":"a #3"}},"protonmail":{"desktop-webmail":{"2020-03":"n","2022-02":"y","2023-01":"y"},"ios":{"2020-03":"n","2023-01":"a #1"},"android":{"2020-03":"a #1"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-09":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"},"ios":{"2023-02":"n"},"android":{"2023-02":"n"}},"gmx":{"desktop-webmail":{"2022-11":"y","2023-07":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"y"}},"web-de":{"desktop-webmail":{"2022-11":"y"},"ios":{"2022-11":"y"},"android":{"2022-11":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"y"},"android":{"2022-11":"y"}},"rainloop":{"desktop-webmail":{"2023-02":"n"}},"wp-pl":{"desktop-webmail":{"2023-12":"y"}}},
"notes":"",
"notes_by_num":{"1":"Partial. Not supported inside the `<body>`.","2":"Partial. Not supported with Non Gmail Accounts.","3":"Buggy. The first `<head>` in the HTML is removed, so `<style>` elements need to be in a second `<head>` element.","4":"Buggy. `<style>` elements need to be declared before their rules are used.","5":"A CSS rule following a CSS comment is ignored. (See [email-bugs#25](https://github.com/hteumeuleu/email-bugs/issues/25).)"}
"notes_by_num":{"1":"Partial. Not supported inside the `<body>`.","2":"Partial. Not supported with non Google accounts.","3":"Buggy. The first `<head>` in the HTML is removed, so `<style>` elements need to be in a second `<head>` element.","4":"Buggy. `<style>` elements need to be declared before their rules are used.","5":"A CSS rule following a CSS comment is ignored. (See [email-bugs#25](https://github.com/hteumeuleu/email-bugs/issues/25).)"}
},
{
@@ -4096,7 +4176,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/Bzyzx8Z5Kvlfib1Fw9Ted8xtPE26RcjPSdUobdUywgJVm/list",
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"n","2020-02":"y"},"ios":{"2019-02":"n"},"android":{"2019-02":"a #1"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2019-08":"y","2021-03":"n"},"ios":{"2019-08":"y"},"android":{"2020-01":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2019-02":"n"},"macos":{"2019-02":"y","16.80":"y"},"outlook-com":{"2019-02":"y","2024-01":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"thunderbird":{"macos":{"60.8":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"aol":{"desktop-webmail":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"samsung-email":{"android":{"5.0.10.2":"y"}},"sfr":{"desktop-webmail":{"2020-01":"y"},"ios":{"2020-01":"y"},"android":{"2020-01":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"n"}},"mail-ru":{"desktop-webmail":{"2020-10":"n"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"n"},"android":{"2022-11":"n"}},"web-de":{"desktop-webmail":{"2022-11":"n"},"ios":{"2022-11":"n"},"android":{"2022-11":"n"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"y"},"android":{"2022-11":"n"}}},
"notes":null,
"notes_by_num":{"1":"Partial. Supported with Non Gmail Accounts only."}
"notes_by_num":{"1":"Partial. Supported with non Google accounts only."}
},
{
@@ -4222,9 +4302,9 @@
"last_test_date":"2020-02-06",
"test_url":"https://www.caniemail.com/tests/images.html",
"test_results_url":"https://app.emailonacid.com/app/acidtest/xm1T5nQ1MKtHpVSJidhagmt3Z53CjqbkMhorlvuM0Gz57/list",
"stats":{"apple-mail":{"macos":{"13":"n","16":"y #2"},"ios":{"13":"n","17":"y"}},"gmail":{"desktop-webmail":{"2020-02":"a #1"},"ios":{"2020-02":"n"},"android":{"2020-02":"a #1"},"mobile-webmail":{"2020-02":"a #1"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-02":"n"},"macos":{"2016":"n","2019":"n","16.80":"y"},"outlook-com":{"2020-02":"n","2024-01":"n"},"ios":{"2020-02":"n"},"android":{"2020-02":"n"}},"samsung-email":{"android":{"9.0":"n"}},"thunderbird":{"windows":{"2020-02":"n"},"macos":{"68.4":"n"}},"aol":{"desktop-webmail":{"2020-02":"n"},"ios":{"2020-02":"n"},"android":{"2020-02":"n"}},"yahoo":{"desktop-webmail":{"2020-02":"n"},"ios":{"2020-02":"n"},"android":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2020-02":"n","2021-03":"n"},"ios":{"2020-02":"n"},"android":{"2020-02":"n"}},"sfr":{"desktop-webmail":{"2020-02":"n"},"ios":{"2020-02":"n"},"android":{"2020-02":"n"}},"protonmail":{"desktop-webmail":{"2020-03":"n"},"ios":{"2020-03":"n"},"android":{"2020-03":"n"}},"hey":{"desktop-webmail":{"2020-06":"n"}},"mail-ru":{"desktop-webmail":{"2020-10":"n"}},"fastmail":{"desktop-webmail":{"2021-07":"n"}},"laposte":{"desktop-webmail":{"2021-08":"n"}},"gmx":{"desktop-webmail":{"2022-09":"n"},"ios":{"2022-09":"n"},"android":{"2022-09":"n"}},"web-de":{"desktop-webmail":{"2022-09":"n"},"ios":{"2022-09":"n"},"android":{"2022-09":"n"}},"ionos-1and1":{"desktop-webmail":{"2022-09":"n"},"android":{"2022-09":"n"}}},
"stats":{"apple-mail":{"macos":{"13":"n","16":"y #2"},"ios":{"13":"n","17":"y"}},"gmail":{"desktop-webmail":{"2020-02":"a #1","2024-02":"a #1"},"ios":{"2020-02":"n","2024-02":"a #1"},"android":{"2020-02":"a #1","2024-02":"a #1"},"mobile-webmail":{"2020-02":"a #1","2024-02":"a #1"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-02":"n","2024-02":"n"},"macos":{"2016":"n","2019":"n","16.80":"y"},"outlook-com":{"2020-02":"n","2024-02":"n"},"ios":{"2020-02":"n","2024-02":"y"},"android":{"2020-02":"n","2024-02":"n"}},"samsung-email":{"android":{"9.0":"n"}},"thunderbird":{"windows":{"2020-02":"n","115.7":"n"},"macos":{"68.4":"n","115.7":"n"}},"aol":{"desktop-webmail":{"2020-02":"n","2024-02":"a #3"},"ios":{"2020-02":"n","2024-02":"y"},"android":{"2020-02":"n","2024-02":"n"}},"yahoo":{"desktop-webmail":{"2020-02":"n","2024-02":"a #3"},"ios":{"2020-02":"n","2024-02":"y"},"android":{"2020-02":"n","2024-02":"n"}},"orange":{"desktop-webmail":{"2020-02":"n","2021-03":"n"},"ios":{"2020-02":"n"},"android":{"2020-02":"n"}},"sfr":{"desktop-webmail":{"2020-02":"n"},"ios":{"2020-02":"n"},"android":{"2020-02":"n"}},"protonmail":{"desktop-webmail":{"2020-03":"n","2024-02":"a #3"},"ios":{"2020-03":"n","2024-02":"y"},"android":{"2020-03":"n","2024-02":"n"}},"hey":{"desktop-webmail":{"2020-06":"n"}},"mail-ru":{"desktop-webmail":{"2020-10":"n"}},"fastmail":{"desktop-webmail":{"2021-07":"n"}},"laposte":{"desktop-webmail":{"2021-08":"n"}},"gmx":{"desktop-webmail":{"2022-09":"n"},"ios":{"2022-09":"n"},"android":{"2022-09":"n"}},"web-de":{"desktop-webmail":{"2022-09":"n"},"ios":{"2022-09":"n"},"android":{"2022-09":"n"}},"ionos-1and1":{"desktop-webmail":{"2022-09":"n"},"android":{"2022-09":"n"}}},
"notes":null,
"notes_by_num":{"1":"Partial: Converts file to jpg.","2":"Supported on macOS 14 Sonoma."}
"notes_by_num":{"1":"Partial: Converts file to jpg.","2":"Supported on macOS 14 Sonoma.","3":"Partial. Webmail rendering depends on browser support."}
},
{
@@ -4304,7 +4384,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/xm1T5nQ1MKtHpVSJidhagmt3Z53CjqbkMhorlvuM0Gz57/list",
"stats":{"apple-mail":{"macos":{"13":"n","14":"y"},"ios":{"13":"n","14":"n","15":"y"}},"gmail":{"desktop-webmail":{"2020-02":"n","2023-01":"n"},"ios":{"2020-02":"a #1","2023-01":"a #1"},"android":{"2020-02":"a #1","2023-01":"a #1"},"mobile-webmail":{"2020-02":"n","2023-01":"n"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-02":"y"},"macos":{"2016":"n","13.1":"y","16.80":"y"},"outlook-com":{"2020-02":"y","2024-01":"y"},"ios":{"2020-02":"n","2023-01":"y"},"android":{"2020-02":"y"}},"samsung-email":{"android":{"9.0":"y"}},"thunderbird":{"windows":{"2020-02":"y"},"macos":{"68.4":"y"}},"aol":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"yahoo":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2020-02":"y","2021-03":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"sfr":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"n","2023-01":"n"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-09":"y"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"web-de":{"desktop-webmail":{"2022-09":"y"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-09":"y"},"android":{"2022-09":"y"}}},
"notes":null,
"notes_by_num":{"1":"Partially supported. Only works with non Gmail accounts."}
"notes_by_num":{"1":"Partially supported. Only works with non Google accounts."}
},
{
@@ -4320,7 +4400,7 @@
"test_results_url":"https://app.emailonacid.com/app/acidtest/xm1T5nQ1MKtHpVSJidhagmt3Z53CjqbkMhorlvuM0Gz57/list",
"stats":{"apple-mail":{"macos":{"13":"y"},"ios":{"13":"y"}},"gmail":{"desktop-webmail":{"2020-02":"a #1"},"ios":{"2020-02":"y"},"android":{"2020-02":"a #1 #2"},"mobile-webmail":{"2020-02":"a #1"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-02":"y"},"macos":{"2016":"y","16.80":"y"},"outlook-com":{"2020-02":"n","2024-01":"n"},"ios":{"2020-02":"y"},"android":{"2020-02":"n"}},"samsung-email":{"android":{"9.0":"n"}},"thunderbird":{"windows":{"2020-02":"n"},"macos":{"68.4":"n"}},"aol":{"desktop-webmail":{"2020-02":"n"},"ios":{"2020-02":"y"},"android":{"2020-02":"n"}},"yahoo":{"desktop-webmail":{"2020-02":"n"},"ios":{"2020-02":"y"},"android":{"2020-02":"n"}},"orange":{"desktop-webmail":{"2020-02":"y","2021-03":"n"},"ios":{"2020-02":"y"},"android":{"2020-02":"n"}},"sfr":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"n"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"n"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"n"}},"fastmail":{"desktop-webmail":{"2021-07":"n"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-09":"n"},"ios":{"2022-09":"y"},"android":{"2022-09":"n"}},"web-de":{"desktop-webmail":{"2022-09":"n"},"ios":{"2022-09":"y"},"android":{"2022-09":"n"}},"ionos-1and1":{"desktop-webmail":{"2022-09":"n"},"android":{"2022-09":"n"}}},
"notes":null,
"notes_by_num":{"1":"Buggy. An image renders but the colours are incorrect.","2":"Partial. Does not render with non Gmail accounts."}
"notes_by_num":{"1":"Buggy. An image renders but the colours are incorrect.","2":"Partial. Does not render with non Google accounts."}
},
{

View File

@@ -63,7 +63,7 @@ func doHead(link string, followRedirects bool) (int, error) {
tr := &http.Transport{}
if config.AllowUntrustedTLS {
tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} // #nosec
}
client := http.Client{

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"github.com/sirupsen/logrus"
@@ -39,7 +40,7 @@ func Log() *logrus.Logger {
}
if LogFile != "" {
file, err := os.OpenFile(LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0664)
file, err := os.OpenFile(filepath.Clean(LogFile), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0664) // #nosec
if err == nil {
log.Out = file
} else {

166
internal/storage/cron.go Normal file
View File

@@ -0,0 +1,166 @@
package storage
import (
"context"
"database/sql"
"math"
"strings"
"time"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/server/websockets"
"github.com/leporo/sqlf"
)
// Database cron runs every minute
func dbCron() {
for {
time.Sleep(60 * time.Second)
currentTime := time.Now()
sinceLastDbAction := currentTime.Sub(dbLastAction)
// only run the database has been idle for 5 minutes
if math.Floor(sinceLastDbAction.Minutes()) == 5 {
deletedSize := getDeletedSize()
if deletedSize > 0 {
total := totalMessagesSize()
var deletedPercent int64
if total == 0 {
deletedPercent = 100
} else {
deletedPercent = deletedSize * 100 / total
}
// 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)
vacuumDb()
}
}
}
pruneMessages()
}
}
// PruneMessages will auto-delete the oldest messages if messages > config.MaxMessages.
// Set config.MaxMessages to 0 to disable.
func pruneMessages() {
if config.MaxMessages < 1 {
return
}
start := time.Now()
q := sqlf.Select("ID, Size").
From("mailbox").
OrderBy("Created DESC").
Limit(5000).
Offset(config.MaxMessages)
ids := []string{}
var prunedSize int64
var size int
if err := q.Query(nil, db, func(row *sql.Rows) {
var id string
if err := row.Scan(&id, &size); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return
}
ids = append(ids, id)
prunedSize = prunedSize + int64(size)
}); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return
}
if len(ids) == 0 {
return
}
tx, err := db.BeginTx(context.Background(), nil)
if err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return
}
args := make([]interface{}, len(ids))
for i, id := range ids {
args[i] = id
}
_, err = tx.Query(`DELETE FROM mailbox 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
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
if err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return
}
err = tx.Commit()
if err != nil {
logger.Log().Errorf("[db] %s", err.Error())
if err := tx.Rollback(); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
}
}
if err := pruneUnusedTags(); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
}
addDeletedSize(prunedSize)
dbLastAction = time.Now()
elapsed := time.Since(start)
logger.Log().Debugf("[db] auto-pruned %d messages in %s", len(ids), elapsed)
logMessagesDeleted(len(ids))
websockets.Broadcast("prune", nil)
}
// Vacuum the database to reclaim space from deleted messages
func vacuumDb() {
start := time.Now()
// set WAL file checkpoint
if _, err := db.Exec("PRAGMA wal_checkpoint"); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return
}
// vacuum database
if _, err := db.Exec("VACUUM"); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return
}
// truncate WAL file
if _, err := db.Exec("PRAGMA wal_checkpoint(TRUNCATE)"); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return
}
if err := SettingPut("DeletedSize", "0"); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
}
elapsed := time.Since(start)
logger.Log().Debugf("[db] vacuumed database in %s", elapsed)
}

View File

@@ -2,29 +2,17 @@
package storage
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/mail"
"os"
"os/signal"
"path"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/tools"
"github.com/axllent/mailpit/server/webhook"
"github.com/axllent/mailpit/server/websockets"
"github.com/google/uuid"
"github.com/jhillyerd/enmime"
"github.com/klauspost/compress/zstd"
"github.com/leporo/sqlf"
@@ -33,12 +21,10 @@ import (
)
var (
db *sql.DB
dbFile string
dbIsTemp bool
dbLastAction time.Time
dbIsIdle bool
dbDataDeleted bool
db *sql.DB
dbFile string
dbIsTemp bool
dbLastAction time.Time
// zstd compression encoder & decoder
dbEncoder, _ = zstd.NewWriter(nil)
@@ -128,595 +114,6 @@ func Close() {
}
}
// Store will save an email to the database tables.
// Returns the database ID of the saved message.
func Store(body *[]byte) (string, error) {
// Parse message body with enmime
env, err := enmime.ReadEnvelope(bytes.NewReader(*body))
if err != nil {
logger.Log().Warnf("[message] %s", err.Error())
return "", nil
}
from := &mail.Address{}
fromJSON := addressToSlice(env, "From")
if len(fromJSON) > 0 {
from = fromJSON[0]
} else if env.GetHeader("From") != "" {
from = &mail.Address{Name: env.GetHeader("From")}
}
messageID := strings.Trim(env.Root.Header.Get("Message-ID"), "<>")
obj := DBMailSummary{
From: from,
To: addressToSlice(env, "To"),
Cc: addressToSlice(env, "Cc"),
Bcc: addressToSlice(env, "Bcc"),
}
created := time.Now()
// use message date instead of created date
if config.UseMessageDates {
if mDate, err := env.Date(); err == nil {
created = mDate
}
}
// generate the search text
searchText := createSearchText(env)
// generate unique ID
id := uuid.New().String()
summaryJSON, err := json.Marshal(obj)
if err != nil {
return "", err
}
// extract tags from body matches based on --tag
tagStr := findTagsInRawMessage(body)
// extract tags from X-Tags header
headerTags := strings.TrimSpace(env.Root.Header.Get("X-Tags"))
if headerTags != "" {
tagStr += "," + headerTags
}
tagData := uniqueTagsFromString(tagStr)
// begin a transaction to ensure both the message
// and data are stored successfully
ctx := context.Background()
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return "", err
}
// roll back if it fails
defer tx.Rollback()
subject := env.GetHeader("Subject")
size := len(*body)
inline := len(env.Inlines)
attachments := len(env.Attachments)
snippet := tools.CreateSnippet(env.Text, env.HTML)
// 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)
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))
if err != nil {
return "", err
}
if err := tx.Commit(); err != nil {
return "", err
}
if len(tagData) > 0 {
// set tags after tx.Commit()
if err := SetMessageTags(id, tagData); err != nil {
return "", err
}
}
c := &MessageSummary{}
if err := json.Unmarshal(summaryJSON, c); err != nil {
return "", err
}
c.Created = created
c.ID = id
c.MessageID = messageID
c.Attachments = attachments
c.Subject = subject
c.Size = size
c.Tags = tagData
c.Snippet = snippet
websockets.Broadcast("new", c)
webhook.Send(c)
dbLastAction = time.Now()
BroadcastMailboxStats()
return id, nil
}
// List returns a subset of messages from the mailbox,
// sorted latest to oldest
func List(start, limit int) ([]MessageSummary, error) {
results := []MessageSummary{}
tsStart := time.Now()
q := sqlf.From("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(nil, db, func(row *sql.Rows) {
var created int64
var id string
var messageID string
var subject string
var metadata string
var size int
var attachments int
var read int
var snippet string
em := MessageSummary{}
if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &snippet); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return
}
if err := json.Unmarshal([]byte(metadata), &em); err != nil {
logger.Log().Errorf("[json] %s", err.Error())
return
}
em.Created = time.UnixMilli(created)
em.ID = id
em.MessageID = messageID
em.Subject = subject
em.Size = size
em.Attachments = attachments
em.Read = read == 1
em.Snippet = snippet
results = append(results, em)
}); err != nil {
return results, err
}
// set tags for listed messages only
for i, m := range results {
results[i].Tags = getMessageTags(m.ID)
}
dbLastAction = time.Now()
elapsed := time.Since(tsStart)
logger.Log().Debugf("[db] list INBOX in %s", elapsed)
return results, nil
}
// GetMessage returns a Message generated from the mailbox_data collection.
// If the message lacks a date header, then the received datetime is used.
func GetMessage(id string) (*Message, error) {
raw, err := GetMessageRaw(id)
if err != nil {
return nil, err
}
r := bytes.NewReader(raw)
env, err := enmime.ReadEnvelope(r)
if err != nil {
return nil, err
}
var from *mail.Address
fromData := addressToSlice(env, "From")
if len(fromData) > 0 {
from = fromData[0]
} else if env.GetHeader("From") != "" {
from = &mail.Address{Name: env.GetHeader("From")}
}
messageID := strings.Trim(env.GetHeader("Message-ID"), "<>")
returnPath := strings.Trim(env.GetHeader("Return-Path"), "<>")
if returnPath == "" && from != nil {
returnPath = from.Address
}
date, err := env.Date()
if err != nil {
// return received datetime when message does not contain a date header
q := sqlf.From("mailbox").
Select(`Created`).
Where(`ID = ?`, id)
if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
var created int64
if err := row.Scan(&created); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return
}
logger.Log().Debugf("[db] %s does not contain a date header, using received datetime", id)
date = time.UnixMilli(created)
}); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
}
}
obj := Message{
ID: id,
MessageID: messageID,
From: from,
Date: date,
To: addressToSlice(env, "To"),
Cc: addressToSlice(env, "Cc"),
Bcc: addressToSlice(env, "Bcc"),
ReplyTo: addressToSlice(env, "Reply-To"),
ReturnPath: returnPath,
Subject: env.GetHeader("Subject"),
Tags: getMessageTags(id),
Size: len(raw),
Text: env.Text,
}
obj.HTML = env.HTML
obj.Inline = []Attachment{}
obj.Attachments = []Attachment{}
for _, i := range env.Inlines {
if i.FileName != "" || i.ContentID != "" {
obj.Inline = append(obj.Inline, AttachmentSummary(i))
}
}
for _, i := range env.OtherParts {
if i.FileName != "" || i.ContentID != "" {
obj.Inline = append(obj.Inline, AttachmentSummary(i))
}
}
for _, a := range env.Attachments {
if a.FileName != "" || a.ContentID != "" {
obj.Attachments = append(obj.Attachments, AttachmentSummary(a))
}
}
// get List-Unsubscribe links if set
obj.ListUnsubscribe = ListUnsubscribe{}
obj.ListUnsubscribe.Links = []string{}
if env.GetHeader("List-Unsubscribe") != "" {
l := env.GetHeader("List-Unsubscribe")
links, err := tools.ListUnsubscribeParser(l)
obj.ListUnsubscribe.Header = l
obj.ListUnsubscribe.Links = links
if err != nil {
obj.ListUnsubscribe.Errors = err.Error()
}
obj.ListUnsubscribe.HeaderPost = env.GetHeader("List-Unsubscribe-Post")
}
// mark message as read
if err := MarkRead(id); err != nil {
return &obj, err
}
dbLastAction = time.Now()
return &obj, nil
}
// GetMessageRaw returns an []byte of the full message
func GetMessageRaw(id string) ([]byte, error) {
var i string
var msg string
q := sqlf.From("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
}
if i == "" {
return nil, errors.New("message not found")
}
raw, err := dbDecoder.DecodeAll([]byte(msg), nil)
if err != nil {
return nil, fmt.Errorf("error decompressing message: %s", err.Error())
}
dbLastAction = time.Now()
return raw, err
}
// GetAttachmentPart returns an *enmime.Part (attachment or inline) from a message
func GetAttachmentPart(id, partID string) (*enmime.Part, error) {
raw, err := GetMessageRaw(id)
if err != nil {
return nil, err
}
r := bytes.NewReader(raw)
env, err := enmime.ReadEnvelope(r)
if err != nil {
return nil, err
}
for _, a := range env.Inlines {
if a.PartID == partID {
return a, nil
}
}
for _, a := range env.OtherParts {
if a.PartID == partID {
return a, nil
}
}
for _, a := range env.Attachments {
if a.PartID == partID {
return a, nil
}
}
dbLastAction = time.Now()
return nil, errors.New("attachment not found")
}
// LatestID returns the latest message ID
//
// If a query argument is set in the request the function will return the
// latest message matching the search
func LatestID(r *http.Request) (string, error) {
var messages []MessageSummary
var err error
search := strings.TrimSpace(r.URL.Query().Get("query"))
if search != "" {
messages, _, err = Search(search, 0, 1)
if err != nil {
return "", err
}
} else {
messages, err = List(0, 1)
if err != nil {
return "", err
}
}
if len(messages) == 0 {
return "", errors.New("Message not found")
}
return messages[0].ID, nil
}
// MarkRead will mark a message as read
func MarkRead(id string) error {
if !IsUnread(id) {
return nil
}
_, err := sqlf.Update("mailbox").
Set("Read", 1).
Where("ID = ?", id).
ExecAndClose(context.Background(), db)
if err == nil {
logger.Log().Debugf("[db] marked message %s as read", id)
}
BroadcastMailboxStats()
return err
}
// MarkAllRead will mark all messages as read
func MarkAllRead() error {
var (
start = time.Now()
total = CountUnread()
)
_, err := sqlf.Update("mailbox").
Set("Read", 1).
Where("Read = ?", 0).
ExecAndClose(context.Background(), db)
if err != nil {
return err
}
elapsed := time.Since(start)
logger.Log().Debugf("[db] marked %d messages as read in %s", total, elapsed)
BroadcastMailboxStats()
dbLastAction = time.Now()
return nil
}
// MarkAllUnread will mark all messages as unread
func MarkAllUnread() error {
var (
start = time.Now()
total = CountRead()
)
_, err := sqlf.Update("mailbox").
Set("Read", 0).
Where("Read = ?", 1).
ExecAndClose(context.Background(), db)
if err != nil {
return err
}
elapsed := time.Since(start)
logger.Log().Debugf("[db] marked %d messages as unread in %s", total, elapsed)
BroadcastMailboxStats()
dbLastAction = time.Now()
return nil
}
// MarkUnread will mark a message as unread
func MarkUnread(id string) error {
if IsUnread(id) {
return nil
}
_, err := sqlf.Update("mailbox").
Set("Read", 0).
Where("ID = ?", id).
ExecAndClose(context.Background(), db)
if err == nil {
logger.Log().Debugf("[db] marked message %s as unread", id)
}
dbLastAction = time.Now()
BroadcastMailboxStats()
return err
}
// DeleteOneMessage will delete a single message from a mailbox
func DeleteOneMessage(id string) error {
// 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
}
_, err = tx.Exec("DELETE FROM mailbox_data WHERE ID = ?", id)
if err != nil {
return err
}
err = tx.Commit()
if err == nil {
logger.Log().Debugf("[db] deleted message %s", id)
}
if err := DeleteAllMessageTags(id); err != nil {
return err
}
dbLastAction = time.Now()
dbDataDeleted = true
logMessagesDeleted(1)
BroadcastMailboxStats()
return err
}
// DeleteAllMessages will delete all messages from a mailbox
func DeleteAllMessages() error {
var (
start = time.Now()
total int
)
_ = sqlf.From("mailbox").
Select("COUNT(*)").To(&total).
QueryRowAndClose(nil, db)
// begin a transaction to ensure both the message
// summaries 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")
if err != nil {
return err
}
_, err = tx.Exec("DELETE FROM mailbox_data")
if err != nil {
return err
}
_, err = tx.Exec("DELETE FROM tags")
if err != nil {
return err
}
_, err = tx.Exec("DELETE FROM message_tags")
if err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
_, err = db.Exec("VACUUM")
if err == nil {
elapsed := time.Since(start)
logger.Log().Debugf("[db] deleted %d messages in %s", total, elapsed)
}
dbLastAction = time.Now()
dbDataDeleted = false
logMessagesDeleted(total)
websockets.Broadcast("prune", nil)
BroadcastMailboxStats()
return err
}
// StatsGet returns the total/unread statistics for a mailbox
func StatsGet() MailboxStats {
var (

View File

@@ -0,0 +1,619 @@
package storage
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/mail"
"strings"
"time"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/tools"
"github.com/axllent/mailpit/server/webhook"
"github.com/axllent/mailpit/server/websockets"
"github.com/jhillyerd/enmime"
"github.com/leporo/sqlf"
"github.com/lithammer/shortuuid/v4"
)
// Store will save an email to the database tables.
// Returns the database ID of the saved message.
func Store(body *[]byte) (string, error) {
// Parse message body with enmime
env, err := enmime.ReadEnvelope(bytes.NewReader(*body))
if err != nil {
logger.Log().Warnf("[message] %s", err.Error())
return "", nil
}
from := &mail.Address{}
fromJSON := addressToSlice(env, "From")
if len(fromJSON) > 0 {
from = fromJSON[0]
} else if env.GetHeader("From") != "" {
from = &mail.Address{Name: env.GetHeader("From")}
}
obj := DBMailSummary{
From: from,
To: addressToSlice(env, "To"),
Cc: addressToSlice(env, "Cc"),
Bcc: addressToSlice(env, "Bcc"),
ReplyTo: addressToSlice(env, "Reply-To"),
}
messageID := strings.Trim(env.Root.Header.Get("Message-ID"), "<>")
created := time.Now()
// use message date instead of created date
if config.UseMessageDates {
if mDate, err := env.Date(); err == nil {
created = mDate
}
}
// generate the search text
searchText := createSearchText(env)
// generate unique ID
id := shortuuid.New()
summaryJSON, err := json.Marshal(obj)
if err != nil {
return "", err
}
// extract tags from body matches based on --tag, plus addresses & X-Tags header
tagStr := findTagsInRawMessage(body) + "," +
obj.tagsFromPlusAddresses() + "," +
strings.TrimSpace(env.Root.Header.Get("X-Tags"))
tagData := uniqueTagsFromString(tagStr)
// begin a transaction to ensure both the message
// and data are stored successfully
ctx := context.Background()
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return "", err
}
// roll back if it fails
defer tx.Rollback()
subject := env.GetHeader("Subject")
size := len(*body)
inline := len(env.Inlines)
attachments := len(env.Attachments)
snippet := tools.CreateSnippet(env.Text, env.HTML)
// 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)
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))
if err != nil {
return "", err
}
if err := tx.Commit(); err != nil {
return "", err
}
if len(tagData) > 0 {
// set tags after tx.Commit()
if err := SetMessageTags(id, tagData); err != nil {
return "", err
}
}
c := &MessageSummary{}
if err := json.Unmarshal(summaryJSON, c); err != nil {
return "", err
}
c.Created = created
c.ID = id
c.MessageID = messageID
c.Attachments = attachments
c.Subject = subject
c.Size = size
c.Tags = tagData
c.Snippet = snippet
websockets.Broadcast("new", c)
webhook.Send(c)
dbLastAction = time.Now()
BroadcastMailboxStats()
return id, nil
}
// List returns a subset of messages from the mailbox,
// sorted latest to oldest
func List(start, limit int) ([]MessageSummary, error) {
results := []MessageSummary{}
tsStart := time.Now()
q := sqlf.From("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(nil, db, func(row *sql.Rows) {
var created int64
var id string
var messageID string
var subject string
var metadata string
var size int
var attachments int
var read int
var snippet string
em := MessageSummary{}
if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &snippet); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return
}
if err := json.Unmarshal([]byte(metadata), &em); err != nil {
logger.Log().Errorf("[json] %s", err.Error())
return
}
em.Created = time.UnixMilli(created)
em.ID = id
em.MessageID = messageID
em.Subject = subject
em.Size = size
em.Attachments = attachments
em.Read = read == 1
em.Snippet = snippet
// artificially generate ReplyTo if legacy data is missing Reply-To field
if em.ReplyTo == nil {
em.ReplyTo = []*mail.Address{}
}
results = append(results, em)
}); err != nil {
return results, err
}
// set tags for listed messages only
for i, m := range results {
results[i].Tags = getMessageTags(m.ID)
}
dbLastAction = time.Now()
elapsed := time.Since(tsStart)
logger.Log().Debugf("[db] list INBOX in %s", elapsed)
return results, nil
}
// GetMessage returns a Message generated from the mailbox_data collection.
// If the message lacks a date header, then the received datetime is used.
func GetMessage(id string) (*Message, error) {
raw, err := GetMessageRaw(id)
if err != nil {
return nil, err
}
r := bytes.NewReader(raw)
env, err := enmime.ReadEnvelope(r)
if err != nil {
return nil, err
}
var from *mail.Address
fromData := addressToSlice(env, "From")
if len(fromData) > 0 {
from = fromData[0]
} else if env.GetHeader("From") != "" {
from = &mail.Address{Name: env.GetHeader("From")}
}
messageID := strings.Trim(env.GetHeader("Message-ID"), "<>")
returnPath := strings.Trim(env.GetHeader("Return-Path"), "<>")
if returnPath == "" && from != nil {
returnPath = from.Address
}
date, err := env.Date()
if err != nil {
// return received datetime when message does not contain a date header
q := sqlf.From("mailbox").
Select(`Created`).
Where(`ID = ?`, id)
if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
var created int64
if err := row.Scan(&created); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return
}
logger.Log().Debugf("[db] %s does not contain a date header, using received datetime", id)
date = time.UnixMilli(created)
}); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
}
}
obj := Message{
ID: id,
MessageID: messageID,
From: from,
Date: date,
To: addressToSlice(env, "To"),
Cc: addressToSlice(env, "Cc"),
Bcc: addressToSlice(env, "Bcc"),
ReplyTo: addressToSlice(env, "Reply-To"),
ReturnPath: returnPath,
Subject: env.GetHeader("Subject"),
Tags: getMessageTags(id),
Size: len(raw),
Text: env.Text,
}
obj.HTML = env.HTML
obj.Inline = []Attachment{}
obj.Attachments = []Attachment{}
for _, i := range env.Inlines {
if i.FileName != "" || i.ContentID != "" {
obj.Inline = append(obj.Inline, AttachmentSummary(i))
}
}
for _, i := range env.OtherParts {
if i.FileName != "" || i.ContentID != "" {
obj.Inline = append(obj.Inline, AttachmentSummary(i))
}
}
for _, a := range env.Attachments {
if a.FileName != "" || a.ContentID != "" {
obj.Attachments = append(obj.Attachments, AttachmentSummary(a))
}
}
// get List-Unsubscribe links if set
obj.ListUnsubscribe = ListUnsubscribe{}
obj.ListUnsubscribe.Links = []string{}
if env.GetHeader("List-Unsubscribe") != "" {
l := env.GetHeader("List-Unsubscribe")
links, err := tools.ListUnsubscribeParser(l)
obj.ListUnsubscribe.Header = l
obj.ListUnsubscribe.Links = links
if err != nil {
obj.ListUnsubscribe.Errors = err.Error()
}
obj.ListUnsubscribe.HeaderPost = env.GetHeader("List-Unsubscribe-Post")
}
// mark message as read
if err := MarkRead(id); err != nil {
return &obj, err
}
dbLastAction = time.Now()
return &obj, nil
}
// GetMessageRaw returns an []byte of the full message
func GetMessageRaw(id string) ([]byte, error) {
var i string
var msg string
q := sqlf.From("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
}
if i == "" {
return nil, errors.New("message not found")
}
raw, err := dbDecoder.DecodeAll([]byte(msg), nil)
if err != nil {
return nil, fmt.Errorf("error decompressing message: %s", err.Error())
}
dbLastAction = time.Now()
return raw, err
}
// GetAttachmentPart returns an *enmime.Part (attachment or inline) from a message
func GetAttachmentPart(id, partID string) (*enmime.Part, error) {
raw, err := GetMessageRaw(id)
if err != nil {
return nil, err
}
r := bytes.NewReader(raw)
env, err := enmime.ReadEnvelope(r)
if err != nil {
return nil, err
}
for _, a := range env.Inlines {
if a.PartID == partID {
return a, nil
}
}
for _, a := range env.OtherParts {
if a.PartID == partID {
return a, nil
}
}
for _, a := range env.Attachments {
if a.PartID == partID {
return a, nil
}
}
dbLastAction = time.Now()
return nil, errors.New("attachment not found")
}
// LatestID returns the latest message ID
//
// If a query argument is set in the request the function will return the
// latest message matching the search
func LatestID(r *http.Request) (string, error) {
var messages []MessageSummary
var err error
search := strings.TrimSpace(r.URL.Query().Get("query"))
if search != "" {
messages, _, err = Search(search, 0, 1)
if err != nil {
return "", err
}
} else {
messages, err = List(0, 1)
if err != nil {
return "", err
}
}
if len(messages) == 0 {
return "", errors.New("Message not found")
}
return messages[0].ID, nil
}
// MarkRead will mark a message as read
func MarkRead(id string) error {
if !IsUnread(id) {
return nil
}
_, err := sqlf.Update("mailbox").
Set("Read", 1).
Where("ID = ?", id).
ExecAndClose(context.Background(), db)
if err == nil {
logger.Log().Debugf("[db] marked message %s as read", id)
}
BroadcastMailboxStats()
return err
}
// MarkAllRead will mark all messages as read
func MarkAllRead() error {
var (
start = time.Now()
total = CountUnread()
)
_, err := sqlf.Update("mailbox").
Set("Read", 1).
Where("Read = ?", 0).
ExecAndClose(context.Background(), db)
if err != nil {
return err
}
elapsed := time.Since(start)
logger.Log().Debugf("[db] marked %d messages as read in %s", total, elapsed)
BroadcastMailboxStats()
dbLastAction = time.Now()
return nil
}
// MarkAllUnread will mark all messages as unread
func MarkAllUnread() error {
var (
start = time.Now()
total = CountRead()
)
_, err := sqlf.Update("mailbox").
Set("Read", 0).
Where("Read = ?", 1).
ExecAndClose(context.Background(), db)
if err != nil {
return err
}
elapsed := time.Since(start)
logger.Log().Debugf("[db] marked %d messages as unread in %s", total, elapsed)
BroadcastMailboxStats()
dbLastAction = time.Now()
return nil
}
// MarkUnread will mark a message as unread
func MarkUnread(id string) error {
if IsUnread(id) {
return nil
}
_, err := sqlf.Update("mailbox").
Set("Read", 0).
Where("ID = ?", id).
ExecAndClose(context.Background(), db)
if err == nil {
logger.Log().Debugf("[db] marked message %s as unread", id)
}
dbLastAction = time.Now()
BroadcastMailboxStats()
return err
}
// DeleteOneMessage will delete a single message from a mailbox
func DeleteOneMessage(id string) error {
m, err := GetMessageRaw(id)
if err != nil {
return err
}
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
}
_, err = tx.Exec("DELETE FROM mailbox_data WHERE ID = ?", id)
if err != nil {
return err
}
err = tx.Commit()
if err == nil {
logger.Log().Debugf("[db] deleted message %s", id)
}
if err := DeleteAllMessageTags(id); err != nil {
return err
}
dbLastAction = time.Now()
addDeletedSize(int64(size))
logMessagesDeleted(1)
BroadcastMailboxStats()
return err
}
// DeleteAllMessages will delete all messages from a mailbox
func DeleteAllMessages() error {
var (
start = time.Now()
total int
)
_ = sqlf.From("mailbox").
Select("COUNT(*)").To(&total).
QueryRowAndClose(nil, db)
// begin a transaction to ensure both the message
// summaries 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")
if err != nil {
return err
}
_, err = tx.Exec("DELETE FROM mailbox_data")
if err != nil {
return err
}
_, err = tx.Exec("DELETE FROM tags")
if err != nil {
return err
}
_, err = tx.Exec("DELETE FROM message_tags")
if err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
elapsed := time.Since(start)
logger.Log().Debugf("[db] deleted %d messages in %s", total, elapsed)
vacuumDb()
dbLastAction = time.Now()
if err := SettingPut("DeletedSize", "0"); err != nil {
logger.Log().Warnf("[db] %s", err.Error())
}
logMessagesDeleted(total)
websockets.Broadcast("prune", nil)
BroadcastMailboxStats()
return err
}

View File

@@ -1,73 +0,0 @@
package storage
// These functions are used to migrate data formats/structure on startup.
import (
"database/sql"
"encoding/json"
"github.com/axllent/mailpit/internal/logger"
"github.com/leporo/sqlf"
)
func dataMigrations() {
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(nil, 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(nil, 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(nil, db); err != nil {
logger.Log().Errorf("[migration] %s", err.Error())
}
}

View File

@@ -1,6 +1,13 @@
package storage
import "github.com/GuiaBolso/darwin"
import (
"database/sql"
"encoding/json"
"github.com/GuiaBolso/darwin"
"github.com/axllent/mailpit/internal/logger"
"github.com/leporo/sqlf"
)
var (
dbMigrations = []darwin.Migration{
@@ -88,6 +95,18 @@ var (
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));`,
},
}
)
@@ -99,3 +118,71 @@ func dbApplyMigrations() error {
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(nil, 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(nil, 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(nil, db); err != nil {
logger.Log().Errorf("[migration] %s", err.Error())
}
}

View File

@@ -4,6 +4,8 @@ import (
"bytes"
"context"
"database/sql"
"encoding/json"
"net/mail"
"os"
"github.com/axllent/mailpit/internal/logger"
@@ -43,6 +45,7 @@ func ReindexAll() {
ID string
SearchText string
Snippet string
Metadata string
}
for _, ids := range chunks {
@@ -63,6 +66,28 @@ func ReindexAll() {
continue
}
from := &mail.Address{}
fromJSON := addressToSlice(env, "From")
if len(fromJSON) > 0 {
from = fromJSON[0]
} else if env.GetHeader("From") != "" {
from = &mail.Address{Name: env.GetHeader("From")}
}
obj := DBMailSummary{
From: from,
To: addressToSlice(env, "To"),
Cc: addressToSlice(env, "Cc"),
Bcc: addressToSlice(env, "Bcc"),
ReplyTo: addressToSlice(env, "Reply-To"),
}
MetadataJSON, err := json.Marshal(obj)
if err != nil {
logger.Log().Errorf("[message] %s", err.Error())
continue
}
searchText := createSearchText(env)
snippet := tools.CreateSnippet(env.Text, env.HTML)
@@ -70,6 +95,7 @@ func ReindexAll() {
u.ID = id
u.SearchText = searchText
u.Snippet = snippet
u.Metadata = string(MetadataJSON)
updates = append(updates, u)
}
@@ -86,7 +112,7 @@ func ReindexAll() {
// insert mail summary data
for _, u := range updates {
_, err = tx.Exec("UPDATE mailbox SET SearchText = ?, Snippet = ? WHERE ID = ?", u.SearchText, u.Snippet, u.ID)
_, err = tx.Exec("UPDATE mailbox SET SearchText = ?, Snippet = ?, Metadata = ? WHERE ID = ?", u.SearchText, u.Snippet, u.Metadata, u.ID)
if err != nil {
logger.Log().Errorf("[db] %s", err.Error())
continue

View File

@@ -42,7 +42,7 @@ func Search(search string, start, limit int) ([]MessageSummary, int, error) {
var ignore string
em := MessageSummary{}
if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &snippet, &ignore, &ignore, &ignore, &ignore); err != nil {
if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &snippet, &ignore, &ignore, &ignore, &ignore, &ignore); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return
}
@@ -99,6 +99,7 @@ func DeleteSearch(search string) error {
q := searchQueryBuilder(search)
ids := []string{}
deleteSize := 0
if err := q.QueryAndClose(nil, db, func(row *sql.Rows) {
var created int64
@@ -113,12 +114,13 @@ func DeleteSearch(search string) error {
var snippet string
var ignore string
if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &snippet, &ignore, &ignore, &ignore, &ignore); err != nil {
if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &snippet, &ignore, &ignore, &ignore, &ignore, &ignore); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return
}
ids = append(ids, id)
deleteSize = deleteSize + size
}); err != nil {
return err
}
@@ -158,21 +160,21 @@ func DeleteSearch(search string) error {
delIDs[i] = id
}
sqlDelete1 := `DELETE FROM mailbox WHERE ID IN (?` + strings.Repeat(",?", len(ids)-1) + `)`
sqlDelete1 := `DELETE FROM 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) + `)`
sqlDelete2 := `DELETE FROM 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) + `)`
sqlDelete3 := `DELETE FROM message_tags WHERE ID IN (?` + strings.Repeat(",?", len(ids)-1) + `)` // #nosec
_, err = tx.Exec(sqlDelete3, delIDs...)
if err != nil {
@@ -191,7 +193,7 @@ func DeleteSearch(search string) error {
}
dbLastAction = time.Now()
dbDataDeleted = true
addDeletedSize(int64(deleteSize))
logMessagesDeleted(total)
@@ -212,7 +214,8 @@ func searchQueryBuilder(searchString string) *sqlf.Stmt {
IFNULL(json_extract(Metadata, '$.To'), '{}') as ToJSON,
IFNULL(json_extract(Metadata, '$.From'), '{}') as FromJSON,
IFNULL(json_extract(Metadata, '$.Cc'), '{}') as CcJSON,
IFNULL(json_extract(Metadata, '$.Bcc'), '{}') as BccJSON
IFNULL(json_extract(Metadata, '$.Bcc'), '{}') as BccJSON,
IFNULL(json_extract(Metadata, '$.ReplyTo'), '{}') as ReplyToJSON
`).
OrderBy("m.Created DESC")
@@ -273,6 +276,15 @@ func searchQueryBuilder(searchString string) *sqlf.Stmt {
q.Where("BccJSON LIKE ?", "%"+escPercentChar(w)+"%")
}
}
} else if strings.HasPrefix(lw, "reply-to:") {
w = cleanString(w[9:])
if w != "" {
if exclude {
q.Where("ReplyToJSON NOT LIKE ?", "%"+escPercentChar(w)+"%")
} else {
q.Where("ReplyToJSON LIKE ?", "%"+escPercentChar(w)+"%")
}
}
} else if strings.HasPrefix(lw, "subject:") {
w = w[8:]
if w != "" {

View File

@@ -17,9 +17,13 @@ func TestSearch(t *testing.T) {
for i := 0; i < testRuns; i++ {
msg := enmime.Builder().
From(fmt.Sprintf("From %d", i), fmt.Sprintf("from-%d@example.com", i)).
CC(fmt.Sprintf("CC %d", i), fmt.Sprintf("cc-%d@example.com", i)).
CC(fmt.Sprintf("CC2 %d", i), fmt.Sprintf("cc2-%d@example.com", i)).
Subject(fmt.Sprintf("Subject line %d end", i)).
Text([]byte(fmt.Sprintf("This is the email body %d <jdsauk;dwqmdqw;>.", i))).
To(fmt.Sprintf("To %d", i), fmt.Sprintf("to-%d@example.com", i))
To(fmt.Sprintf("To %d", i), fmt.Sprintf("to-%d@example.com", i)).
To(fmt.Sprintf("To2 %d", i), fmt.Sprintf("to2-%d@example.com", i)).
ReplyTo(fmt.Sprintf("Reply To %d", i), fmt.Sprintf("reply-to-%d@example.com", i))
env, err := msg.Build()
if err != nil {
@@ -44,18 +48,26 @@ func TestSearch(t *testing.T) {
for i := 1; i < 51; i++ {
// search a random something that will return a single result
searchIdx := rand.Intn(4) + 1
var search string
switch searchIdx {
case 1:
search = fmt.Sprintf("from-%d@example.com", i)
case 2:
search = fmt.Sprintf("to-%d@example.com", i)
case 3:
search = fmt.Sprintf("\"Subject line %d end\"", i)
default:
search = fmt.Sprintf("\"the email body %d jdsauk dwqmdqw\"", i)
uniqueSearches := []string{
fmt.Sprintf("from-%d@example.com", i),
fmt.Sprintf("from:from-%d@example.com", i),
fmt.Sprintf("to-%d@example.com", i),
fmt.Sprintf("to:to-%d@example.com", i),
fmt.Sprintf("to2-%d@example.com", i),
fmt.Sprintf("to:to2-%d@example.com", i),
fmt.Sprintf("cc-%d@example.com", i),
fmt.Sprintf("cc:cc-%d@example.com", i),
fmt.Sprintf("cc2-%d@example.com", i),
fmt.Sprintf("cc:cc2-%d@example.com", i),
fmt.Sprintf("reply-to-%d@example.com", i),
fmt.Sprintf("reply-to:\"reply-to-%d@example.com\"", i),
fmt.Sprintf("\"Subject line %d end\"", i),
fmt.Sprintf("subject:\"Subject line %d end\"", i),
fmt.Sprintf("\"the email body %d jdsauk dwqmdqw\"", i),
}
searchIdx := rand.Intn(len(uniqueSearches))
search := uniqueSearches[searchIdx]
summaries, _, err := Search(search, 0, 100)
if err != nil {
@@ -63,7 +75,7 @@ func TestSearch(t *testing.T) {
t.Fail()
}
assertEqual(t, len(summaries), 1, "1 search result expected")
assertEqual(t, len(summaries), 1, "search result expected")
assertEqual(t, summaries[0].From.Name, fmt.Sprintf("From %d", i), "\"From\" name does not match")
assertEqual(t, summaries[0].From.Address, fmt.Sprintf("from-%d@example.com", i), "\"From\" address does not match")

View File

@@ -0,0 +1,75 @@
package storage
import (
"database/sql"
"github.com/axllent/mailpit/internal/logger"
"github.com/leporo/sqlf"
)
// 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").
Select("Value").To(&result).
Where("Key = ?", k).
Limit(1).
QueryAndClose(nil, db, func(row *sql.Rows) {})
if err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return ""
}
return result.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)
if err != nil {
logger.Log().Errorf("[db] %s", err.Error())
}
return err
}
// The total deleted message size as an int64 value
func getDeletedSize() int64 {
var result sql.NullInt64
err := sqlf.From("settings").
Select("Value").To(&result).
Where("Key = ?", "DeletedSize").
Limit(1).
QueryAndClose(nil, db, func(row *sql.Rows) {})
if err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return 0
}
return result.Int64
}
// 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").
Select("SUM(Size)").To(&result).
QueryAndClose(nil, db, func(row *sql.Rows) {})
if err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return 0
}
return result.Int64
}
// 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 {
logger.Log().Errorf("[db] %s", err.Error())
}
if _, err := db.Exec("UPDATE settings SET Value = Value + ? WHERE Key = ?", v, "DeletedSize"); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
}
}

View File

@@ -82,6 +82,8 @@ type MessageSummary struct {
Cc []*mail.Address
// Bcc addresses
Bcc []*mail.Address
// Reply-To address
ReplyTo []*mail.Address
// Email subject
Subject string
// Created time
@@ -105,10 +107,11 @@ type MailboxStats struct {
// DBMailSummary struct for storing mail summary
type DBMailSummary struct {
From *mail.Address
To []*mail.Address
Cc []*mail.Address
Bcc []*mail.Address
From *mail.Address
To []*mail.Address
Cc []*mail.Address
Bcc []*mail.Address
ReplyTo []*mail.Address
}
// AttachmentSummary returns a summary of the attachment without any binary data

View File

@@ -2,6 +2,7 @@ package storage
import (
"database/sql"
"regexp"
"sort"
"strings"
@@ -11,6 +12,10 @@ import (
"github.com/leporo/sqlf"
)
var (
addressPlusRe = regexp.MustCompile(`(?U)^(.*){1,}\+(.*)@`)
)
// SetMessageTags will set the tags for a given database ID
func SetMessageTags(id string, tags []string) error {
applyTags := []string{}
@@ -236,6 +241,35 @@ func findTagsInRawMessage(message *[]byte) string {
return tagStr
}
// Returns tags found in email plus addresses (eg: test+tagname@example.com)
func (d DBMailSummary) tagsFromPlusAddresses() string {
tags := []string{}
for _, c := range d.To {
matches := addressPlusRe.FindAllStringSubmatch(c.String(), 1)
if len(matches) == 1 {
tags = append(tags, strings.Split(matches[0][2], "+")...)
}
}
for _, c := range d.Cc {
matches := addressPlusRe.FindAllStringSubmatch(c.String(), 1)
if len(matches) == 1 {
tags = append(tags, strings.Split(matches[0][2], "+")...)
}
}
for _, c := range d.Bcc {
matches := addressPlusRe.FindAllStringSubmatch(c.String(), 1)
if len(matches) == 1 {
tags = append(tags, strings.Split(matches[0][2], "+")...)
}
}
matches := addressPlusRe.FindAllStringSubmatch(d.From.String(), 1)
if len(matches) == 1 {
tags = append(tags, strings.Split(matches[0][2], "+")...)
}
return strings.Join(tags, ",")
}
// Get message tags from the database for a given database ID
// Used when parsing a raw email.
func getMessageTags(id string) []string {

View File

@@ -107,5 +107,24 @@ func TestTags(t *testing.T) {
// Check deleted message tags also prune the tags database
allTags := GetAllTags()
assertEqual(t, "", strings.Join(allTags, "|"), "Dirty message tag did not clean as expected")
assertEqual(t, "", strings.Join(allTags, "|"), "Tags did not delete as expected")
if err := DeleteAllMessages(); err != nil {
t.Log("error ", err)
t.Fail()
}
// test 20 tags
id, err = Store(&testTagEmail)
if err != nil {
t.Log("error ", err)
t.Fail()
}
returnedTags = getMessageTags(id)
assertEqual(t, "BccTag|CcTag|FromFag|ToTag|X-tag1|X-tag2", strings.Join(returnedTags, "|"), "Tags not detected correctly")
if err := DeleteAllMessageTags(id); err != nil {
t.Log("error ", err)
t.Fail()
}
}

49
internal/storage/testdata/tags.eml vendored Normal file
View File

@@ -0,0 +1,49 @@
Date: Wed, 27 Jul 2022 15:44:41 +1200
From: Sender Smith <sender+FromFag@example.com>
To: Recipient Ross <recipient+ToTag@example.com>
Cc: Recipient Ross <cc+CcTag@example.com>
Bcc: <bcc+BccTag@example.com>
Subject: Plain text message
X-Tags: X-tag1, X-tag2
Message-ID: <20220727034441.7za34h6ljuzfpmj3@localhost.localhost>
MIME-Version: 1.0
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras non massa lacinia,
fringilla ex vel, ornare nulla. Suspendisse dapibus commodo sapien, non
hendrerit diam feugiat sit amet. Nulla lorem quam, laoreet vitae nisl volutpat,
mollis bibendum felis. In eget ultricies justo. Donec vitae hendrerit tortor, at
posuere libero. Fusce a gravida nibh. Nulla ac odio ex.
Aliquam sem turpis, cursus vitae condimentum at, scelerisque pulvinar lectus.
Cras tempor nisl ut arcu interdum, et luctus arcu cursus. Maecenas mollis
sagittis commodo. Mauris ac lorem nec ex interdum consequat. Morbi congue
ultrices ullamcorper. Aenean ex tortor, dapibus quis dapibus iaculis, iaculis
eget felis. Vestibulum purus ante, efficitur in turpis ac, tristique laoreet
orci. Nulla facilisi. Praesent mollis orci posuere elementum laoreet.
Pellentesque enim nibh, varius at ante id, consequat posuere ante.
Cras maximus venenatis nulla nec cursus. Morbi convallis, enim eget viverra
vulputate, ipsum arcu tincidunt tortor, ut cursus dui enim commodo quam. Donec
et vulputate quam. Vivamus non posuere erat. Nam commodo pellentesque
condimentum. Vivamus condimentum eros at odio dictum feugiat. Ut imperdiet
tempor luctus. Aenean varius libero ac faucibus dictum. Aliquam sed finibus
massa. Morbi dolor lorem, feugiat quis neque et, suscipit posuere ex. Sed auctor
et augue at finibus. Vestibulum interdum mi ac justo porta aliquam. Curabitur
nec enim sit amet enim aliquet accumsan. Etiam accumsan tellus tortor, interdum
sodales odio finibus eu. Integer eget ante eu nisi lobortis pulvinar et vel
ipsum. Cras condimentum posuere vulputate.
Cras nulla felis, blandit vitae egestas quis, fringilla ut dolor. Phasellus est
augue, feugiat eu risus quis, posuere ultrices libero. Phasellus non nunc eget
justo sollicitudin tincidunt. Praesent pretium dui id felis bibendum sodales.
Phasellus eget dictum libero, auctor tempor nibh. Suspendisse posuere libero
venenatis elit imperdiet porttitor. In condimentum dictum luctus. Nullam in
nulla vitae augue blandit posuere. Vestibulum consectetur ultricies tincidunt.
Vivamus dolor quam, pharetra sed eros sed, hendrerit ultrices diam. Vestibulum
vulputate tellus eget tellus lacinia, a pulvinar velit vulputate. Suspendisse
mauris odio, scelerisque eget turpis sed, tincidunt ultrices magna. Nunc arcu
arcu, commodo et porttitor quis, accumsan viverra purus. Fusce id libero iaculis
lorem tristique commodo porttitor id ipsum. Vestibulum odio dui, tincidunt eget
lectus vel, tristique lacinia libero. Aliquam dapibus ac felis vitae cursus.

View File

@@ -11,6 +11,7 @@ import (
var (
testTextEmail []byte
testTagEmail []byte
testMimeEmail []byte
testRuns = 100
)
@@ -31,6 +32,11 @@ func setup() {
panic(err)
}
testTagEmail, err = os.ReadFile("testdata/tags.eml")
if err != nil {
panic(err)
}
testMimeEmail, err = os.ReadFile("testdata/mime-attachment.eml")
if err != nil {
panic(err)

View File

@@ -1,21 +1,14 @@
package storage
import (
"context"
"database/sql"
"net/mail"
"os"
"regexp"
"strings"
"sync"
"time"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/html2text"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/server/websockets"
"github.com/jhillyerd/enmime"
"github.com/leporo/sqlf"
)
var (
@@ -77,107 +70,6 @@ func cleanString(str string) string {
return strings.ToLower(strings.Join(strings.Fields(strings.TrimSpace(str)), " "))
}
// Auto-prune runs every minute to automatically delete oldest messages
// if total is greater than the threshold
func dbCron() {
for {
time.Sleep(60 * time.Second)
start := time.Now()
// check if database contains deleted data and has not been in use
// for 5 minutes, if so VACUUM
currentTime := time.Now()
diff := currentTime.Sub(dbLastAction)
if dbDataDeleted && diff.Minutes() > 5 {
dbDataDeleted = false
_, err := db.Exec("VACUUM")
if err == nil {
elapsed := time.Since(start)
logger.Log().Debugf("[db] compressed idle database in %s", elapsed)
}
continue
}
if config.MaxMessages > 0 {
q := sqlf.Select("ID").
From("mailbox").
OrderBy("Created DESC").
Limit(5000).
Offset(config.MaxMessages)
ids := []string{}
if err := q.Query(nil, db, func(row *sql.Rows) {
var id string
if err := row.Scan(&id); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
return
}
ids = append(ids, id)
}); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
continue
}
if len(ids) == 0 {
continue
}
tx, err := db.BeginTx(context.Background(), nil)
if err != nil {
logger.Log().Errorf("[db] %s", err.Error())
continue
}
args := make([]interface{}, len(ids))
for i, id := range ids {
args[i] = id
}
_, err = tx.Query(`DELETE FROM mailbox WHERE ID IN (?`+strings.Repeat(",?", len(ids)-1)+`)`, args...) // #nosec
if err != nil {
logger.Log().Errorf("[db] %s", err.Error())
continue
}
_, err = tx.Query(`DELETE FROM mailbox_data WHERE ID IN (?`+strings.Repeat(",?", len(ids)-1)+`)`, args...) // #nosec
if err != nil {
logger.Log().Errorf("[db] %s", err.Error())
continue
}
_, err = tx.Query(`DELETE FROM message_tags WHERE ID IN (?`+strings.Repeat(",?", len(ids)-1)+`)`, args...) // #nosec
if err != nil {
logger.Log().Errorf("[db] %s", err.Error())
continue
}
err = tx.Commit()
if err != nil {
logger.Log().Errorf("[db] %s", err.Error())
if err := tx.Rollback(); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
}
}
if err := pruneUnusedTags(); err != nil {
logger.Log().Errorf("[db] %s", err.Error())
}
dbDataDeleted = true
elapsed := time.Since(start)
logger.Log().Debugf("[db] auto-pruned %d messages in %s", len(ids), elapsed)
logMessagesDeleted(len(ids))
websockets.Broadcast("prune", nil)
}
}
}
// LogMessagesDeleted logs the number of messages deleted
func logMessagesDeleted(n int) {
mu.Lock()
@@ -195,7 +87,7 @@ func isFile(path string) bool {
return true
}
// InArray tests if a string in within an array. It is not case sensitive.
// Tests if a string is within an array. It is not case sensitive.
func inArray(k string, arr []string) bool {
k = strings.ToLower(k)
for _, v := range arr {
@@ -207,7 +99,7 @@ func inArray(k string, arr []string) bool {
return false
}
// escPercentChar replaces `%` with `%%` for SQL searches
// Convert `%` to `%%` for SQL searches
func escPercentChar(s string) string {
return strings.ReplaceAll(s, "%", "%%")
}

View File

@@ -3,14 +3,20 @@ package tools
import (
"regexp"
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
var (
// Invalid tag characters regex
tagsInvalidChars = regexp.MustCompile(`[^a-zA-Z0-9\-\ \_]`)
tagsInvalidChars = regexp.MustCompile(`[^a-zA-Z0-9\-\ \_\.]`)
// Regex to catch multiple spaces
multiSpaceRe = regexp.MustCompile(`(\s+)`)
// TagsTitleCase enforces TitleCase on all tags
TagsTitleCase bool
)
// CleanTag returns a clean tag, removing whitespace and invalid characters
@@ -21,5 +27,10 @@ func CleanTag(s string) string {
" ",
),
)
if TagsTitleCase {
return cases.Title(language.Und, cases.NoLower).String(s)
}
return s
}

View File

@@ -178,8 +178,8 @@ func GithubUpdate(repo, appName, currentVersion string) (string, error) {
}
if runtime.GOOS != "windows" {
/* #nosec G302 */
if err := os.Chmod(newExec, 0755); err != nil {
err := os.Chmod(newExec, 0755) // #nosec
if err != nil {
return "", err
}
}

1291
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,8 +19,8 @@ import (
"github.com/axllent/mailpit/internal/storage"
"github.com/axllent/mailpit/internal/tools"
"github.com/axllent/mailpit/server/smtpd"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/lithammer/shortuuid/v4"
)
// GetMessages returns a paginated list of messages as JSON
@@ -642,7 +642,7 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
return
}
if config.SMTPRelayConfig.RecipientAllowlistRegexp != nil && !config.SMTPRelayConfig.RecipientAllowlistRegexp.MatchString(address.Address) {
if config.SMTPRelayConfig.AllowedRecipientsRegexp != nil && !config.SMTPRelayConfig.AllowedRecipientsRegexp.MatchString(address.Address) {
httpError(w, "Mail address does not match allowlist: "+to)
return
}
@@ -696,7 +696,7 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
}
// generate unique ID
uid := uuid.New().String() + "@mailpit"
uid := shortuuid.New() + "@mailpit"
// update Message-Id with unique ID
msg, err = tools.UpdateMessageHeader(msg, "Message-Id", "<"+uid+">")
if err != nil {

View File

@@ -20,7 +20,10 @@ type webUIConfiguration struct {
SMTPServer string
// Enforced Return-Path (if set) for relay bounces
ReturnPath string
// Allowlist of accepted recipients
// Only allow relaying to these recipients (regex)
AllowedRecipients string
// DEPRECATED 2024/03/12
// swagger:ignore
RecipientAllowlist string
}
@@ -57,7 +60,9 @@ func WebUIConfig(w http.ResponseWriter, _ *http.Request) {
if config.ReleaseEnabled {
conf.MessageRelay.SMTPServer = fmt.Sprintf("%s:%d", config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port)
conf.MessageRelay.ReturnPath = config.SMTPRelayConfig.ReturnPath
conf.MessageRelay.RecipientAllowlist = config.SMTPRelayConfig.RecipientAllowlist
conf.MessageRelay.AllowedRecipients = config.SMTPRelayConfig.AllowedRecipients
// DEPRECATED 2024/03/12
conf.MessageRelay.RecipientAllowlist = config.SMTPRelayConfig.AllowedRecipients
}
conf.DisableHTMLCheck = config.DisableHTMLCheck

View File

@@ -35,7 +35,7 @@ func ProxyHandler(w http.ResponseWriter, r *http.Request) {
tr := &http.Transport{}
if config.AllowUntrustedTLS {
tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} // #nosec
}
client := &http.Client{
@@ -108,7 +108,9 @@ func ProxyHandler(w http.ResponseWriter, r *http.Request) {
// relay status code - WriteHeader must come after Header.Set()
w.WriteHeader(resp.StatusCode)
w.Write(body)
if _, err := w.Write(body); err != nil {
logger.Log().Warnf("[proxy] %s", err.Error())
}
}
// AbsoluteURL will return a full URL regardless whether it is relative or absolute

76
server/pop3/functions.go Normal file
View File

@@ -0,0 +1,76 @@
package pop3
import (
"errors"
"fmt"
"net"
"strings"
"github.com/axllent/mailpit/internal/auth"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/storage"
)
func authUser(username, password string) bool {
return auth.POP3Credentials.Match(username, password)
}
// Send a response with debug logging
func sendResponse(c net.Conn, m string) {
fmt.Fprintf(c, "%s\r\n", m)
logger.Log().Debugf("[pop3] response: %s", m)
}
// Send a response without debug logging (for data)
func sendData(c net.Conn, m string) {
fmt.Fprintf(c, "%s\r\n", m)
}
func getMessages() ([]message, error) {
messages := []message{}
list, err := storage.List(0, 100)
if err != nil {
return messages, err
}
for _, m := range list {
msg := message{}
msg.ID = m.ID
msg.Size = m.Size
messages = append(messages, msg)
}
return messages, nil
}
// POP3 TOP command returns the headers, followed by the next x lines
func getTop(id string, nr int) (string, string, error) {
var header, body string
raw, err := storage.GetMessageRaw(id)
if err != nil {
return header, body, errors.New("-ERR no such message")
}
parts := strings.SplitN(string(raw), "\r\n\r\n", 2)
header = parts[0]
lines := []string{}
if nr > 0 && len(parts) == 2 {
lines = strings.SplitN(parts[1], "\r\n", nr)
}
return header, strings.Join(lines, "\r\n"), nil
}
// cuts the line into command and arguments
func getCommand(line string) (string, []string) {
line = strings.Trim(line, "\r \n")
cmd := strings.Split(line, " ")
return cmd[0], cmd[1:]
}
func getSafeArg(args []string, nr int) (string, error) {
if nr < len(args) {
return args[nr], nil
}
return "", errors.New("Out of range")
}

314
server/pop3/pop3.go Normal file
View File

@@ -0,0 +1,314 @@
// Package pop3 is a simple POP3 server for Mailpit.
// By default it is disabled unless password credentials have been loaded.
//
// References: https://github.com/r0stig/golang-pop3 | https://github.com/inbucket/inbucket
// See RFC: https://datatracker.ietf.org/doc/html/rfc1939
package pop3
import (
"bufio"
"crypto/tls"
"fmt"
"net"
"strconv"
"strings"
"time"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/auth"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/storage"
"github.com/axllent/mailpit/server/websockets"
)
const (
// UNAUTHORIZED state
UNAUTHORIZED = 1
// TRANSACTION state
TRANSACTION = 2
// UPDATE state
UPDATE = 3
)
// Run will start the pop3 server if enabled
func Run() {
if auth.POP3Credentials == nil || config.POP3Listen == "" {
// POP3 server is disabled without authentication
return
}
var listener net.Listener
var err error
if config.POP3TLSCert != "" {
cer, err := tls.LoadX509KeyPair(config.POP3TLSCert, config.POP3TLSKey)
if err != nil {
logger.Log().Errorf("[pop3] %s", err.Error())
return
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cer},
MinVersion: tls.VersionTLS12,
}
listener, err = tls.Listen("tcp", config.POP3Listen, tlsConfig)
} else {
// unencrypted
listener, err = net.Listen("tcp", config.POP3Listen)
}
if err != nil {
logger.Log().Errorf("[pop3] %s", err.Error())
return
}
logger.Log().Infof("[pop3] starting on %s", config.POP3Listen)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
// run as goroutine
go handleClient(conn)
}
}
type message struct {
ID string
Size int
}
func handleClient(conn net.Conn) {
var (
user = ""
state = 1
toDelete = []string{}
)
defer func() {
if state == UPDATE {
for _, id := range toDelete {
_ = storage.DeleteOneMessage(id)
}
if len(toDelete) > 0 {
// update web UI to remove deleted messages
websockets.Broadcast("prune", nil)
}
}
if err := conn.Close(); err != nil {
logger.Log().Errorf("[pop3] %s", err.Error())
}
}()
reader := bufio.NewReader(conn)
messages := []message{}
// State
// 1 = Unauthorized
// 2 = Transaction mode
// 3 = update mode
logger.Log().Debugf("[pop3] connection opened by %s", conn.RemoteAddr().String())
// First welcome the new connection
sendResponse(conn, "+OK Mailpit POP3 server")
timeoutDuration := 30 * time.Second
for {
// POP3 server enforced a timeout of 30 seconds
if err := conn.SetDeadline(time.Now().Add(timeoutDuration)); err != nil {
logger.Log().Errorf("[pop3] %s", err.Error())
return
}
// Reads a line from the client
rawLine, err := reader.ReadString('\n')
if err != nil {
logger.Log().Errorf("[pop3] %s", err.Error())
return
}
// Parses the command
cmd, args := getCommand(rawLine)
logger.Log().Debugf("[pop3] received: %s (%s)", strings.TrimSpace(rawLine), conn.RemoteAddr().String())
if cmd == "CAPA" {
// List our capabilities per RFC2449
sendResponse(conn, "+OK Capability list follows")
sendResponse(conn, "TOP")
sendResponse(conn, "USER")
sendResponse(conn, "UIDL")
sendResponse(conn, "IMPLEMENTATION Mailpit")
sendResponse(conn, ".")
continue
} else if cmd == "USER" && state == UNAUTHORIZED {
if len(args) != 1 {
sendResponse(conn, "-ERR must supply a user")
return
}
// always true - stash for PASS
sendResponse(conn, "+OK")
user = args[0]
} else if cmd == "PASS" && state == UNAUTHORIZED {
if len(args) != 1 {
sendResponse(conn, "-ERR must supply a password")
return
}
pass := args[0]
if authUser(user, pass) {
sendResponse(conn, "+OK signed in")
messages, err = getMessages()
if err != nil {
logger.Log().Errorf("[pop3] %s", err.Error())
}
state = 2
} else {
sendResponse(conn, "-ERR invalid password")
logger.Log().Warnf("[pop3] failed login: %s", user)
}
} else if cmd == "STAT" && state == TRANSACTION {
totalSize := 0
for _, m := range messages {
totalSize = totalSize + m.Size
}
sendResponse(conn, fmt.Sprintf("+OK %d %d", len(messages), totalSize))
} else if cmd == "LIST" && state == TRANSACTION {
totalSize := 0
for _, m := range messages {
totalSize = totalSize + m.Size
}
sendData(conn, fmt.Sprintf("+OK %d messages (%d octets)", len(messages), totalSize))
// print all sizes
for row, m := range messages {
sendData(conn, fmt.Sprintf("%d %d", row+1, m.Size))
}
// end
sendData(conn, ".")
} else if cmd == "UIDL" && state == TRANSACTION {
totalSize := 0
for _, m := range messages {
totalSize = totalSize + m.Size
}
sendData(conn, "+OK unique-id listing follows")
// print all message IDS
for row, m := range messages {
sendData(conn, fmt.Sprintf("%d %s", row+1, m.ID))
}
// end
sendData(conn, ".")
} else if cmd == "RETR" && state == TRANSACTION {
if len(args) != 1 {
sendResponse(conn, "-ERR no such message")
return
}
nr, err := strconv.Atoi(args[0])
if err != nil {
sendResponse(conn, "-ERR no such message")
return
}
if nr < 1 || nr > len(messages) {
sendResponse(conn, "-ERR no such message")
return
}
m := messages[nr-1]
raw, err := storage.GetMessageRaw(m.ID)
if err != nil {
sendResponse(conn, "-ERR no such message")
return
}
size := len(raw)
sendData(conn, fmt.Sprintf("+OK %d octets", size))
sendData(conn, string(raw))
sendData(conn, ".")
} else if cmd == "TOP" && state == TRANSACTION {
arg, err := getSafeArg(args, 0)
if err != nil {
sendResponse(conn, "-ERR TOP requires two arguments")
return
}
nr, err := strconv.Atoi(arg)
if err != nil {
sendResponse(conn, "-ERR TOP requires two arguments")
return
}
if nr < 1 || nr > len(messages) {
sendResponse(conn, "-ERR no such message")
return
}
arg2, err := getSafeArg(args, 1)
if err != nil {
sendResponse(conn, "-ERR TOP requires two arguments")
return
}
lines, err := strconv.Atoi(arg2)
if err != nil {
sendResponse(conn, "-ERR TOP requires two arguments")
return
}
m := messages[nr-1]
headers, body, err := getTop(m.ID, lines)
sendData(conn, "+OK Top of message follows")
sendData(conn, headers+"\r\n")
sendData(conn, body)
sendData(conn, ".")
} else if cmd == "NOOP" && state == TRANSACTION {
sendData(conn, "+OK")
} else if cmd == "DELE" && state == TRANSACTION {
arg, _ := getSafeArg(args, 0)
nr, err := strconv.Atoi(arg)
if err != nil {
logger.Log().Warnf("[pop3] -ERR invalid DELETE integer: %s", arg)
sendResponse(conn, "-ERR invalid integer")
return
}
if nr < 1 || nr > len(messages) {
logger.Log().Warnf("[pop3] -ERR no such message")
sendResponse(conn, "-ERR no such message")
return
}
toDelete = append(toDelete, messages[nr-1].ID)
sendResponse(conn, "+OK")
} else if cmd == "RSET" && state == TRANSACTION {
toDelete = []string{}
sendData(conn, "+OK")
} else if cmd == "QUIT" {
state = UPDATE
return
} else {
logger.Log().Warnf("[pop3] -ERR %s not implemented", cmd)
sendResponse(conn, fmt.Sprintf("-ERR %s not implemented", cmd))
}
}
}

View File

@@ -13,6 +13,7 @@ import (
"strings"
"sync/atomic"
"text/template"
"time"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/auth"
@@ -21,6 +22,7 @@ import (
"github.com/axllent/mailpit/internal/storage"
"github.com/axllent/mailpit/server/apiv1"
"github.com/axllent/mailpit/server/handlers"
"github.com/axllent/mailpit/server/pop3"
"github.com/axllent/mailpit/server/websockets"
"github.com/gorilla/mux"
)
@@ -47,6 +49,8 @@ func Listen() {
go websockets.MessageHub.Run()
go pop3.Run()
r := apiRoutes()
// kubernetes probes
@@ -94,12 +98,18 @@ func Listen() {
logger.Log().Infof("[http] starting on %s", config.HTTPListen)
server := &http.Server{
Addr: config.HTTPListen,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
}
if config.UITLSCert != "" && config.UITLSKey != "" {
logger.Log().Infof("[http] accessible via https://%s%s", logger.CleanHTTPIP(config.HTTPListen), config.Webroot)
logger.Log().Fatal(http.ListenAndServeTLS(config.HTTPListen, config.UITLSCert, config.UITLSKey, nil))
logger.Log().Fatal(server.ListenAndServeTLS(config.UITLSCert, config.UITLSKey))
} else {
logger.Log().Infof("[http] accessible via http://%s%s", logger.CleanHTTPIP(config.HTTPListen), config.Webroot)
logger.Log().Fatal(http.ListenAndServe(config.HTTPListen, nil))
logger.Log().Fatal(server.ListenAndServe())
}
}

View File

@@ -12,7 +12,7 @@ import (
)
func allowedRecipients(to []string) []string {
if config.SMTPRelayConfig.RecipientAllowlistRegexp == nil {
if config.SMTPRelayConfig.AllowedRecipientsRegexp == nil {
return to
}
@@ -26,8 +26,8 @@ func allowedRecipients(to []string) []string {
continue
}
if !config.SMTPRelayConfig.RecipientAllowlistRegexp.MatchString(address.Address) {
logger.Log().Debugf("[smtp] not allowed to relay to %s: does not match the allowlist %s", recipient, config.SMTPRelayConfig.RecipientAllowlist)
if !config.SMTPRelayConfig.AllowedRecipientsRegexp.MatchString(address.Address) {
logger.Log().Debugf("[smtp] not allowed to relay to %s: does not match the allowlist %s", recipient, config.SMTPRelayConfig.AllowedRecipients)
} else {
ar = append(ar, recipient)
}
@@ -54,7 +54,7 @@ func Send(from string, to []string, msg []byte) error {
defer c.Close()
if config.SMTPRelayConfig.STARTTLS {
conf := &tls.Config{ServerName: config.SMTPRelayConfig.Host}
conf := &tls.Config{ServerName: config.SMTPRelayConfig.Host} // #nosec
conf.InsecureSkipVerify = config.SMTPRelayConfig.AllowInsecure

View File

@@ -14,7 +14,7 @@ import (
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/stats"
"github.com/axllent/mailpit/internal/storage"
"github.com/google/uuid"
"github.com/lithammer/shortuuid/v4"
"github.com/mhale/smtpd"
)
@@ -63,7 +63,7 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
// add a message ID if not set
if messageID == "" {
// generate unique ID
messageID = uuid.New().String() + "@mailpit"
messageID = shortuuid.New() + "@mailpit"
// add unique ID
data = append([]byte("Message-Id: <"+messageID+">\r\n"), data...)
} else if config.IgnoreDuplicateIDs {

View File

@@ -160,18 +160,19 @@ export default {
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning mb-3" v-if="mailbox.appInfo.LatestVersion == ''">
There might be a newer version available. The check failed.
</div>
<a class="btn btn-warning d-block mb-3"
v-else-if="mailbox.appInfo.Version != mailbox.appInfo.LatestVersion"
:href="'https://github.com/axllent/mailpit/releases/tag/' + mailbox.appInfo.LatestVersion">
A new version of Mailpit ({{ mailbox.appInfo.LatestVersion }}) is available.
</a>
<div class="row g-3">
<div class="col-xl-6">
<div class="row g-3" v-if="mailbox.appInfo.LatestVersion == ''">
<div class="alert alert-warning mb-3">
There might be a newer version available. The check failed.
</div>
</div>
<div class="row g-3" v-else-if="mailbox.appInfo.Version != mailbox.appInfo.LatestVersion">
<a class="btn btn-warning d-block mb-3"
:href="'https://github.com/axllent/mailpit/releases/tag/' + mailbox.appInfo.LatestVersion">
A new version of Mailpit ({{ mailbox.appInfo.LatestVersion }}) is available.
</a>
</div>
<div class="row g-3">
<div class="col-12">
<RouterLink to="/api/v1/" class="btn btn-primary w-100" target="_blank">
@@ -224,7 +225,7 @@ export default {
<tbody>
<tr>
<td>
Mailpit uptime
Mailpit up since
</td>
<td>
{{ secondsToRelative(mailbox.appInfo.RuntimeStats.Uptime) }}

View File

@@ -301,7 +301,7 @@ export default {
<tr v-if="message.ReplyTo && message.ReplyTo.length" class="small">
<th class="text-nowrap">Reply-To</th>
<td class="privacy text-body-secondary text-break">
<span v-for="( t, i ) in message.ReplyTo ">
<span v-for="(t, i) in message.ReplyTo">
<template v-if="i > 0">,</template>
<span class="text-spaces">{{ t.Name }}</span>
&lt;<a :href="searchURI(t.Address)" class="text-body-secondary">
@@ -337,11 +337,11 @@ export default {
<select class="form-select small tag-selector" v-model="messageTags" multiple
data-full-width="false" data-suggestions-threshold="1" data-allow-new="true"
data-clear-end="true" data-allow-clear="true" data-placeholder="Add tags..."
data-badge-style="secondary" data-regex="^([a-zA-Z0-9\-\ \_]){3,}$"
data-badge-style="secondary" data-regex="^([a-zA-Z0-9\-\ \_\.]){1,}$"
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 mailbox.tags" :value="t">{{ t }}</option>
</select>
<div class="invalid-feedback">Invalid tag name</div>
</td>
@@ -491,7 +491,7 @@ export default {
</button>
<div class="d-none d-lg-block ms-auto me-3" v-if="showMobileButtons">
<template v-for=" vals, key in responsiveSizes ">
<template v-for="_, key in responsiveSizes">
<button class="btn" :disabled="scaleHTMLPreview == key" :title="'Switch to ' + key + ' view'"
v-on:click="scaleHTMLPreview = key">
<i class="bi" :class="'bi-' + key"></i>

View File

@@ -126,10 +126,10 @@ export default {
</div>
</div>
<div class="form-text text-center" v-if="mailbox.uiConfig.MessageRelay.RecipientAllowlist != ''">
<div class="form-text text-center" v-if="mailbox.uiConfig.MessageRelay.AllowedRecipients != ''">
Note: A recipient allowlist has been configured. Any mail address not matching it will be rejected.
<br class="d-none d-md-inline">
Configured allowlist: <b>{{ mailbox.uiConfig.MessageRelay.RecipientAllowlist }}</b>
Allowed recipients: <b>{{ mailbox.uiConfig.MessageRelay.AllowedRecipients }}</b>
</div>
<div class="form-text text-center">
Note: For testing purposes, a unique Message-Id will be generated on send.

View File

@@ -1259,6 +1259,13 @@
"description": "Read status",
"type": "boolean"
},
"ReplyTo": {
"description": "Reply-To address",
"type": "array",
"items": {
"$ref": "#/definitions/Address"
}
},
"Size": {
"description": "Message size in bytes (total)",
"type": "integer",
@@ -1400,14 +1407,14 @@
"description": "Message Relay information",
"type": "object",
"properties": {
"AllowedRecipients": {
"description": "Only allow relaying to these recipients (regex)",
"type": "string"
},
"Enabled": {
"description": "Whether message relaying (release) is enabled",
"type": "boolean"
},
"RecipientAllowlist": {
"description": "Allowlist of accepted recipients",
"type": "string"
},
"ReturnPath": {
"description": "Enforced Return-Path (if set) for relay bounces",
"type": "string"