mirror of
https://github.com/axllent/mailpit.git
synced 2026-03-10 19:17:02 +00:00
Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
585ea1dc30 | ||
|
|
c66a06379a | ||
|
|
c5c9292863 | ||
|
|
6f1f4f34c9 | ||
|
|
877a9159ce | ||
|
|
c4582889ad | ||
|
|
cd1cf695b9 | ||
|
|
392904fd23 | ||
|
|
f0160c0e29 | ||
|
|
f9024d1f77 | ||
|
|
061f159293 | ||
|
|
e69a0d75c9 | ||
|
|
0847167694 | ||
|
|
6dd3587ec6 | ||
|
|
2d1e38d4fd | ||
|
|
153174f928 | ||
|
|
3b9b470c09 | ||
|
|
dd99a4bcf0 | ||
|
|
5bf2f2796b | ||
|
|
a469655f65 | ||
|
|
432fedeafa | ||
|
|
37e4ff4139 | ||
|
|
2808316dd2 | ||
|
|
43d759b0fc | ||
|
|
264222d599 | ||
|
|
5e4bdb78b8 | ||
|
|
fc9572156b | ||
|
|
d52a0d550f | ||
|
|
fcce621f18 | ||
|
|
f4cd19aac2 | ||
|
|
46ccf866b2 | ||
|
|
266611fda0 | ||
|
|
fe3920e3c6 | ||
|
|
ac02802d62 | ||
|
|
7d6aab4e01 | ||
|
|
36d8525557 | ||
|
|
0f0a5d942f | ||
|
|
b987006897 | ||
|
|
c8e0bee8bb | ||
|
|
3b0ae24c2a | ||
|
|
aca491f10c | ||
|
|
6724f0ccdd | ||
|
|
93088f3361 | ||
|
|
e817bf5f7d | ||
|
|
4d100a9ec3 | ||
|
|
958fa6cf1a | ||
|
|
27e12474f5 | ||
|
|
302b269fb6 | ||
|
|
2d9157ffd3 | ||
|
|
242c96244a | ||
|
|
d308e7f30b | ||
|
|
85a9cc3c2b | ||
|
|
f94ce556e5 | ||
|
|
5ad8619893 | ||
|
|
8d692b6bd9 | ||
|
|
99ab38fbba | ||
|
|
2cf040e787 | ||
|
|
cde80bf0fd | ||
|
|
1a41d433c6 | ||
|
|
49557e8e59 | ||
|
|
339f6ef31d | ||
|
|
2e187cfcef | ||
|
|
39ecefa108 | ||
|
|
ae65312d02 | ||
|
|
0770bd8d19 | ||
|
|
e2314fb3b9 | ||
|
|
1dd0bf3d29 | ||
|
|
568ad5da62 | ||
|
|
d3063ea248 | ||
|
|
6ccc294a1e | ||
|
|
5629f39d40 | ||
|
|
314f30bee5 | ||
|
|
5c8931c04d | ||
|
|
ca6357f262 | ||
|
|
3645219615 | ||
|
|
7d74516270 | ||
|
|
fb1a06bc86 | ||
|
|
7809a26374 | ||
|
|
120f078a32 | ||
|
|
776912d38a | ||
|
|
e3b37943a8 | ||
|
|
5054d98701 | ||
|
|
8ce6fc0db5 | ||
|
|
933d064a51 |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1,3 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [axllent]
|
||||
thanks_dev: u/gh/axllent
|
||||
|
||||
1
.github/cliff.toml
vendored
1
.github/cliff.toml
vendored
@@ -28,6 +28,7 @@ trim = true
|
||||
# HTML comments added for grouping order, stripped on generation
|
||||
commit_parsers = [
|
||||
{body = ".*security", group = "<!-- 1 -->Security"},
|
||||
{message = "(?i)^security", group = "<!-- 1 -->Security"},
|
||||
{message = "(?i)^feat", group = "<!-- 2 -->Feature"},
|
||||
{message = "(?i)^chore", group = "<!-- 3 -->Chore"},
|
||||
{message = "(?i)^libs", group = "<!-- 3 -->Chore"},
|
||||
|
||||
2
.github/workflows/build-docker-edge.yml
vendored
2
.github/workflows/build-docker-edge.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
2
.github/workflows/build-docker.yml
vendored
2
.github/workflows/build-docker.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
3
.github/workflows/close-stale-issues.yml
vendored
3
.github/workflows/close-stale-issues.yml
vendored
@@ -10,12 +10,13 @@ jobs:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v9.1.0
|
||||
- uses: actions/stale@v10.1.1
|
||||
with:
|
||||
days-before-issue-stale: 7
|
||||
days-before-issue-close: 3
|
||||
exempt-issue-labels: "enhancement,bug,awaiting feedback"
|
||||
stale-issue-label: "stale"
|
||||
close-issue-reason: "completed"
|
||||
stale-issue-message: "This issue has been marked as stale because it has been open for 7 days with no activity."
|
||||
close-issue-message: "This issue was closed because there has been no activity since being marked as stale."
|
||||
days-before-pr-stale: -1
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
4
.github/workflows/release-build.yml
vendored
4
.github/workflows/release-build.yml
vendored
@@ -21,10 +21,10 @@ jobs:
|
||||
- goarch: arm
|
||||
goos: windows
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
# build the assets
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
|
||||
4
.github/workflows/tests-rqlite.yml
vendored
4
.github/workflows/tests-rqlite.yml
vendored
@@ -17,9 +17,9 @@ jobs:
|
||||
# the HTTP address the rqlite node should advertise
|
||||
HTTP_ADV_ADDR: "localhost:4001"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 'stable'
|
||||
cache-dependency-path: "**/*.sum"
|
||||
|
||||
21
.github/workflows/tests.yml
vendored
21
.github/workflows/tests.yml
vendored
@@ -12,13 +12,13 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: false
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Go environment
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
# build the assets
|
||||
- name: Set up node environment
|
||||
if: startsWith(matrix.os, 'ubuntu') == true
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
@@ -52,9 +52,10 @@ jobs:
|
||||
if: startsWith(matrix.os, 'ubuntu') == true
|
||||
run: npm run package
|
||||
|
||||
# validate the swagger file
|
||||
- name: Validate OpenAPI definition
|
||||
if: startsWith(matrix.os, 'ubuntu') == true
|
||||
uses: swaggerexpert/swagger-editor-validate@v1
|
||||
with:
|
||||
definition-file: server/ui/api/v1/swagger.json
|
||||
# # validate the swagger file
|
||||
# - name: Validate OpenAPI definition
|
||||
# if: startsWith(matrix.os, 'ubuntu') == true
|
||||
# uses: swaggerexpert/swagger-editor-validate@v1
|
||||
# with:
|
||||
# definition-file: server/ui/api/v1/swagger.json
|
||||
# default-timeout: 20000
|
||||
|
||||
120
CHANGELOG.md
120
CHANGELOG.md
@@ -2,6 +2,106 @@
|
||||
|
||||
Notable changes to Mailpit will be documented in this file.
|
||||
|
||||
## [v1.28.2]
|
||||
|
||||
### Security
|
||||
- Prevent Cross-Site WebSocket Hijacking (CSWSH) allowing unauthenticated access to message data [CVE-2026-22689](https://github.com/axllent/mailpit/security/advisories/GHSA-524m-q5m7-79mm)
|
||||
|
||||
### Feature
|
||||
- Allow default mail addresses to be set when releasing message ([#594](https://github.com/axllent/mailpit/issues/594))
|
||||
|
||||
### Chore
|
||||
- Remove webkit warnings about missing template / render functions
|
||||
- Avoid empty URL query parameter when returning to inbox from message view
|
||||
|
||||
|
||||
## [v1.28.1]
|
||||
|
||||
### Security
|
||||
- Restrict screenshot proxy to only support asset links contained in messages [CVE-2026-21859](https://github.com/axllent/mailpit/security/advisories/GHSA-8v65-47jx-7mfr)
|
||||
|
||||
### Chore
|
||||
- Bump actions/checkout from 5 to 6 ([#610](https://github.com/axllent/mailpit/issues/610))
|
||||
- Bump actions/cache from 4 to 5 ([#607](https://github.com/axllent/mailpit/issues/607))
|
||||
- Bump actions/stale from 10.0.0 to 10.1.1 ([#604](https://github.com/axllent/mailpit/issues/604))
|
||||
- Bump actions/setup-node from 5 to 6 ([#598](https://github.com/axllent/mailpit/issues/598))
|
||||
- Bump esbuild from 0.25.12 to 0.27.2 ([#611](https://github.com/axllent/mailpit/issues/611))
|
||||
- Update Go dependencies
|
||||
- Update node dependencies
|
||||
|
||||
### Test
|
||||
- Add inline message tests
|
||||
- Increase swagger test timeout
|
||||
|
||||
|
||||
## [v1.28.0]
|
||||
|
||||
### Feature
|
||||
- Optionally propagate SMTP errors ([#588](https://github.com/axllent/mailpit/issues/588))
|
||||
|
||||
### Chore
|
||||
- Update Go dependencies
|
||||
- Update node dependencies
|
||||
- Update caniemail test database
|
||||
|
||||
|
||||
## [v1.27.11]
|
||||
|
||||
### Chore
|
||||
- Update Go dependencies
|
||||
- Update node dependencies
|
||||
- Add type assertion for value in imaging assignment
|
||||
|
||||
|
||||
## [v1.27.10]
|
||||
|
||||
### Security
|
||||
- Prevent potential information disclosure via indirect expvar library (Prometheus)
|
||||
|
||||
### Chore
|
||||
- Add tooltip to messages nav dropdown
|
||||
- Update GitHub Actions
|
||||
- Add tooltip to messages nav dropdown
|
||||
- Update GitHub Actions
|
||||
- Update Go dependencies
|
||||
- Update node dependencies
|
||||
|
||||
|
||||
## [v1.27.9]
|
||||
|
||||
### Chore
|
||||
- UI tweaks to pagination layout for clearer navigation ([#568](https://github.com/axllent/mailpit/issues/568))
|
||||
- Add margin to icons in release and delete buttons for consistent spacing
|
||||
- Update navbar theme to use data-bs-theme attribute for consistency
|
||||
- Update Go dependencies
|
||||
- Update node dependencies
|
||||
|
||||
|
||||
## [v1.27.8]
|
||||
|
||||
### Chore
|
||||
- Update Go dependencies
|
||||
- Update node dependencies
|
||||
- Update caniemail test database
|
||||
|
||||
|
||||
## [v1.27.7]
|
||||
|
||||
### Fix
|
||||
- Move HELO/EHLO hostname setting to the correct position in SMTP client creation ([#558](https://github.com/axllent/mailpit/issues/558))
|
||||
|
||||
|
||||
## [v1.27.6]
|
||||
|
||||
### Feature
|
||||
- Add optional --no-release-check to version subcommand ([#557](https://github.com/axllent/mailpit/issues/557))
|
||||
|
||||
### Chore
|
||||
- Set HELO/EHLO hostname when connecting to external SMTP server ([#556](https://github.com/axllent/mailpit/issues/556))
|
||||
- Update Go dependencies
|
||||
- Update node dependencies
|
||||
|
||||
|
||||
## [v1.27.5]
|
||||
|
||||
### Chore
|
||||
@@ -32,6 +132,10 @@ Notable changes to Mailpit will be documented in this file.
|
||||
|
||||
## [v1.27.2]
|
||||
|
||||
### Security
|
||||
- Prevent integer overflow conversion to uint64
|
||||
- Add ReadHeaderTimeout to Prometheus metrics server
|
||||
|
||||
### Feature
|
||||
- Add ability to generate self-signed (snakeoil) certificates for UI, SMTP and POP3 ([#539](https://github.com/axllent/mailpit/issues/539))
|
||||
|
||||
@@ -47,10 +151,6 @@ Notable changes to Mailpit will be documented in this file.
|
||||
- Support angle brackets for text/plain URLs with spaces ([#535](https://github.com/axllent/mailpit/issues/535))
|
||||
- Do not check latest release for Prometheus statistics ([#522](https://github.com/axllent/mailpit/issues/522))
|
||||
|
||||
### Security
|
||||
- Prevent integer overflow conversion to uint64
|
||||
- Add ReadHeaderTimeout to Prometheus metrics server
|
||||
|
||||
|
||||
## [v1.27.1]
|
||||
|
||||
@@ -1680,6 +1780,9 @@ Notable changes to Mailpit will be documented in this file.
|
||||
|
||||
## [1.1.4]
|
||||
|
||||
### Security
|
||||
- Add restrictive HTTP Content-Security-Policy
|
||||
|
||||
### Feature
|
||||
- Add --quiet flag to display only errors
|
||||
|
||||
@@ -1688,9 +1791,6 @@ Notable changes to Mailpit will be documented in this file.
|
||||
- Add favicon unread message counter
|
||||
- Minor UI color change & unread count position adjustment
|
||||
|
||||
### Security
|
||||
- Add restrictive HTTP Content-Security-Policy
|
||||
|
||||
|
||||
## [1.1.3]
|
||||
|
||||
@@ -1777,14 +1877,14 @@ Notable changes to Mailpit will be documented in this file.
|
||||
|
||||
## [0.1.2]
|
||||
|
||||
### Feature
|
||||
- Optional browser notifications (HTTPS only)
|
||||
|
||||
### Security
|
||||
- Use strconv.Atoi() for safe string to int conversions
|
||||
- Sanitize mailbox names
|
||||
- Don't allow tar files containing a ".."
|
||||
|
||||
### Feature
|
||||
- Optional browser notifications (HTTPS only)
|
||||
|
||||
|
||||
## [0.1.1]
|
||||
|
||||
|
||||
@@ -115,3 +115,10 @@ Please refer to [the documentation](https://mailpit.axllent.org/docs/install/tes
|
||||
Mailpit's SMTP server (default on port 1025), so you will likely need to configure your sending application to deliver mail via that port.
|
||||
A common MTA (Mail Transfer Agent) that delivers system emails to an SMTP server is `sendmail`, used by many applications, including PHP.
|
||||
Mailpit can also act as substitute for sendmail. For instructions on how to set this up, please refer to the [sendmail documentation](https://mailpit.axllent.org/docs/install/sendmail/).
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
For team features, multiple inboxes, and a hosted setup, try
|
||||
<a href="https://mailtrap.io/?ref=mailpit">Mailtrap</a>, our friendly companion.
|
||||
</p>
|
||||
|
||||
@@ -17,7 +17,7 @@ The database can either be the database file (eg: --database /var/lib/mailpit/ma
|
||||
URL of a running Mailpit instance (eg: --http http://127.0.0.1/). If dumping over HTTP, the URL
|
||||
should be the base URL of your running Mailpit instance, not the link to the API itself.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Run: func(_ *cobra.Command, args []string) {
|
||||
if err := dump.Sync(args[0]); err != nil {
|
||||
logger.Log().Fatal(err)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ Mailpit server. Each email must be a separate file (eg: Maildir format, not mbox
|
||||
The --recent flag will only consider files with a modification date within the last X days.`,
|
||||
// Hidden: true,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Run: func(_ *cobra.Command, args []string) {
|
||||
var count int
|
||||
var total int
|
||||
var per100start = time.Now()
|
||||
|
||||
@@ -28,7 +28,7 @@ status 1 if unhealthy.
|
||||
If running within Docker, it should automatically detect environment
|
||||
settings to determine the HTTP bind interface & port.
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
webroot := strings.TrimRight(path.Join("/", config.Webroot, "/"), "/") + "/"
|
||||
proto := "http"
|
||||
if useHTTPS {
|
||||
|
||||
@@ -18,7 +18,7 @@ var reindexCmd = &cobra.Command{
|
||||
If you have several thousand messages in your mailbox, then it is advised to shut down
|
||||
Mailpit while you reindex as this process will likely result in database locking issues.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Run: func(_ *cobra.Command, args []string) {
|
||||
config.Database = args[0]
|
||||
config.MaxMessages = 0
|
||||
|
||||
|
||||
@@ -332,6 +332,7 @@ func initConfigFromEnv() {
|
||||
config.SMTPRelayConfig.AllowedRecipients = os.Getenv("MP_SMTP_RELAY_ALLOWED_RECIPIENTS")
|
||||
config.SMTPRelayConfig.BlockedRecipients = os.Getenv("MP_SMTP_RELAY_BLOCKED_RECIPIENTS")
|
||||
config.SMTPRelayConfig.PreserveMessageIDs = getEnabledFromEnv("MP_SMTP_RELAY_PRESERVE_MESSAGE_IDS")
|
||||
config.SMTPRelayConfig.ForwardSMTPErrors = getEnabledFromEnv("MP_SMTP_RELAY_FWD_SMTP_ERRORS")
|
||||
|
||||
// SMTP forwarding
|
||||
config.SMTPForwardConfigFile = os.Getenv("MP_SMTP_FORWARD_CONFIG")
|
||||
@@ -350,6 +351,7 @@ func initConfigFromEnv() {
|
||||
config.SMTPForwardConfig.ReturnPath = os.Getenv("MP_SMTP_FORWARD_RETURN_PATH")
|
||||
config.SMTPForwardConfig.OverrideFrom = os.Getenv("MP_SMTP_FORWARD_OVERRIDE_FROM")
|
||||
config.SMTPForwardConfig.To = os.Getenv("MP_SMTP_FORWARD_TO")
|
||||
config.SMTPForwardConfig.ForwardSMTPErrors = getEnabledFromEnv("MP_SMTP_FORWARD_FWD_SMTP_ERRORS")
|
||||
|
||||
// Chaos
|
||||
chaos.Enabled = getEnabledFromEnv("MP_ENABLE_CHAOS")
|
||||
|
||||
@@ -14,8 +14,9 @@ var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Display the current version & update information",
|
||||
Long: `Display the current version & update information (if available).`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
update, _ := cmd.Flags().GetBool("update")
|
||||
noReleaseCheck, _ := cmd.Flags().GetBool("no-release-check")
|
||||
|
||||
if update {
|
||||
// Update the application
|
||||
@@ -32,23 +33,25 @@ var versionCmd = &cobra.Command{
|
||||
fmt.Printf("%s %s compiled with %s on %s/%s\n",
|
||||
os.Args[0], config.Version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
release, err := config.GHRUConfig.Latest()
|
||||
if err != nil {
|
||||
fmt.Printf("Error checking for latest release: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if !noReleaseCheck {
|
||||
release, err := config.GHRUConfig.Latest()
|
||||
if err != nil {
|
||||
fmt.Printf("Error checking for latest release: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// The latest version is the same version
|
||||
if release.Tag == config.Version {
|
||||
os.Exit(0)
|
||||
}
|
||||
// The latest version is the same version
|
||||
if release.Tag == config.Version {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// A newer release is available
|
||||
fmt.Printf(
|
||||
"\nUpdate available: %s\nRun `%s version -u` to update (requires read/write access to install directory).\n",
|
||||
release.Tag,
|
||||
os.Args[0],
|
||||
)
|
||||
// A newer release is available
|
||||
fmt.Printf(
|
||||
"\nUpdate available: %s\nRun `%s version -u` to update (requires read/write access to install directory).\n",
|
||||
release.Tag,
|
||||
os.Args[0],
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -57,4 +60,6 @@ func init() {
|
||||
|
||||
versionCmd.Flags().
|
||||
BoolP("update", "u", false, "update to latest version")
|
||||
versionCmd.Flags().
|
||||
Bool("no-release-check", false, "do not check online for the latest release version")
|
||||
}
|
||||
|
||||
@@ -252,6 +252,7 @@ type SMTPRelayConfigStruct struct {
|
||||
BlockedRecipients string `yaml:"blocked-recipients"` // regex, if set prevents relating to these addresses
|
||||
BlockedRecipientsRegexp *regexp.Regexp // compiled regexp using BlockedRecipients
|
||||
PreserveMessageIDs bool `yaml:"preserve-message-ids"` // preserve the original Message-ID when relaying
|
||||
ForwardSMTPErrors bool `yaml:"forward-smtp-errors"` // whether to log smtp-errors or forward them to upstream-client
|
||||
|
||||
// DEPRECATED 2024/03/12
|
||||
RecipientAllowlist string `yaml:"recipient-allowlist"`
|
||||
@@ -259,18 +260,19 @@ type SMTPRelayConfigStruct struct {
|
||||
|
||||
// SMTPForwardConfigStruct struct for parsing yaml & storing variables
|
||||
type SMTPForwardConfigStruct struct {
|
||||
To string `yaml:"to"` // comma-separated list of email addresses
|
||||
Host string `yaml:"host"` // SMTP host
|
||||
Port int `yaml:"port"` // SMTP port
|
||||
STARTTLS bool `yaml:"starttls"` // whether to use STARTTLS
|
||||
TLS bool `yaml:"tls"` // whether to use TLS
|
||||
AllowInsecure bool `yaml:"allow-insecure"` // allow insecure authentication, ignore TLS validation
|
||||
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
|
||||
OverrideFrom string `yaml:"override-from"` // allow overriding of the from address
|
||||
To string `yaml:"to"` // comma-separated list of email addresses
|
||||
Host string `yaml:"host"` // SMTP host
|
||||
Port int `yaml:"port"` // SMTP port
|
||||
STARTTLS bool `yaml:"starttls"` // whether to use STARTTLS
|
||||
TLS bool `yaml:"tls"` // whether to use TLS
|
||||
AllowInsecure bool `yaml:"allow-insecure"` // allow insecure authentication, ignore TLS validation
|
||||
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
|
||||
OverrideFrom string `yaml:"override-from"` // allow overriding of the from address
|
||||
ForwardSMTPErrors bool `yaml:"forward-smtp-errors"` // whether to log smtp-errors or forward them to upstream-client
|
||||
}
|
||||
|
||||
// VerifyConfig wil do some basic checking
|
||||
@@ -283,7 +285,8 @@ func VerifyConfig() error {
|
||||
// The default Content Security Policy is updates on every application page load to replace script-src 'self'
|
||||
// with a random nonce ID to prevent XSS. This applies to the Mailpit app & API.
|
||||
// See server.middleWareFunc()
|
||||
ContentSecurityPolicy = fmt.Sprintf("default-src 'self'; script-src 'self'; style-src %s 'unsafe-inline'; frame-src 'self'; img-src * data: blob:; font-src %s data:; media-src 'self'; connect-src 'self' ws: wss:; object-src 'none'; base-uri 'self';",
|
||||
ContentSecurityPolicy = fmt.Sprintf(
|
||||
"default-src 'self'; script-src 'self'; style-src %s 'unsafe-inline'; frame-src 'self'; img-src * data: blob:; font-src %s data:; media-src 'self'; connect-src 'self' ws: wss:; object-src 'none'; base-uri 'self';",
|
||||
cssFontRestriction, cssFontRestriction,
|
||||
)
|
||||
|
||||
@@ -615,8 +618,10 @@ func VerifyConfig() error {
|
||||
}
|
||||
|
||||
SMTPRelayMatchingRegexp = re
|
||||
logger.Log().Infof("[relay] auto-relaying new messages to recipients matching \"%s\" via %s:%d",
|
||||
SMTPRelayMatching, SMTPRelayConfig.Host, SMTPRelayConfig.Port)
|
||||
logger.Log().Infof(
|
||||
"[relay] auto-relaying new messages to recipients matching \"%s\" via %s:%d",
|
||||
SMTPRelayMatching, SMTPRelayConfig.Host, SMTPRelayConfig.Port,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
62
go.mod
62
go.mod
@@ -3,32 +3,33 @@ module github.com/axllent/mailpit
|
||||
go 1.24.3
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.3
|
||||
github.com/PuerkitoBio/goquery v1.11.0
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
||||
github.com/axllent/ghru/v2 v2.0.2
|
||||
github.com/axllent/semver v1.0.0
|
||||
github.com/goccy/go-yaml v1.18.0
|
||||
github.com/goccy/go-yaml v1.19.1
|
||||
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/jhillyerd/enmime/v2 v2.2.0
|
||||
github.com/klauspost/compress v1.18.0
|
||||
github.com/kovidgoyal/imaging v1.6.4
|
||||
github.com/klauspost/compress v1.18.2
|
||||
github.com/kovidgoyal/imaging v1.8.18
|
||||
github.com/leporo/sqlf v1.4.0
|
||||
github.com/lithammer/shortuuid/v4 v4.2.0
|
||||
github.com/mneis/go-telnet v0.0.0-20221017141824-6f643e477c62
|
||||
github.com/prometheus/client_golang v1.23.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/rqlite/gorqlite v0.0.0-20250609141355-ac86a4a1c9a8
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/pflag v1.0.7
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/tg123/go-htpasswd v1.2.4
|
||||
github.com/vanng822/go-premailer v1.25.0
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/text v0.28.0
|
||||
golang.org/x/time v0.12.0
|
||||
modernc.org/sqlite v1.38.2
|
||||
github.com/vanng822/go-premailer v1.30.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/net v0.48.0
|
||||
golang.org/x/text v0.32.0
|
||||
golang.org/x/time v0.14.0
|
||||
modernc.org/sqlite v1.42.2
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -37,6 +38,9 @@ require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.6.2 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
|
||||
@@ -44,30 +48,34 @@ require (
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/inbucket/html2text v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/kovidgoyal/go-parallel v1.1.1 // indirect
|
||||
github.com/kovidgoyal/go-shm v1.0.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
github.com/olekukonko/errors v1.1.0 // indirect
|
||||
github.com/olekukonko/ll v0.0.9 // indirect
|
||||
github.com/olekukonko/tablewriter v1.0.9 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/olekukonko/ll v0.1.3 // indirect
|
||||
github.com/olekukonko/tablewriter v1.1.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // 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.7 // indirect
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // 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/exp v0.0.0-20250813145105-42675adae3e6 // indirect
|
||||
golang.org/x/image v0.30.0 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
google.golang.org/protobuf v1.36.7 // indirect
|
||||
modernc.org/libc v1.66.7 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
|
||||
golang.org/x/image v0.34.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
modernc.org/libc v1.67.4 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
159
go.sum
159
go.sum
@@ -1,7 +1,7 @@
|
||||
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI=
|
||||
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec=
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
|
||||
@@ -16,7 +16,14 @@ github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
|
||||
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -26,8 +33,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
|
||||
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
||||
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A=
|
||||
@@ -45,16 +52,26 @@ 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.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
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/inbucket/html2text v1.0.0 h1:N5kza++4uBBDJ2Z3KUnTRyPNoBcW+YfOgNiNmNB+sgs=
|
||||
github.com/inbucket/html2text v1.0.0/go.mod h1:5TrhXQKGU+LXurODaSm55Y9eXoPBRnYiOz4x2XfUoJU=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jhillyerd/enmime/v2 v2.2.0 h1:Pe35MB96eZK5Q0XjlvPftOgWypQpd1gcbfJKAt7rsB8=
|
||||
github.com/jhillyerd/enmime/v2 v2.2.0/go.mod h1:SOBXlCemjhiV2DvHhAKnJiWrtJGS/Ffuw4Iy7NjBTaI=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kovidgoyal/imaging v1.6.4 h1:K0idhRPXnRrJBKnBYcTfI1HTWSNDeAn7hYDvf9I0dCk=
|
||||
github.com/kovidgoyal/imaging v1.6.4/go.mod h1:bEIgsaZmXlvFfkv/CUxr9rJook6AQkJnpB5EPosRfRY=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kovidgoyal/go-parallel v1.1.1 h1:1OzpNjtrUkBPq3UaqrnvOoB2F9RttSt811uiUXyI7ok=
|
||||
github.com/kovidgoyal/go-parallel v1.1.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw=
|
||||
github.com/kovidgoyal/go-shm v1.0.0 h1:HJEel9D1F9YhULvClEHJLawoRSj/1u/EDV7MJbBPgQo=
|
||||
github.com/kovidgoyal/go-shm v1.0.0/go.mod h1:Yzb80Xf9L3kaoB2RGok9hHwMIt7Oif61kT6t3+VnZds=
|
||||
github.com/kovidgoyal/imaging v1.8.18 h1:42JCqJnQBzBo0hGllLEJVYDARWXPP9MT3HgiTno9Chc=
|
||||
github.com/kovidgoyal/imaging v1.8.18/go.mod h1:bqjHpeAxSuTLvKob6HuqAr9td2wP9G54Snbgd+1QLoU=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leporo/sqlf v1.4.0 h1:SyWnX/8GSGOzVmanG0Ub1c04mR9nNl6Tq3IeFKX2/4c=
|
||||
@@ -66,53 +83,56 @@ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stg
|
||||
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.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
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/mneis/go-telnet v0.0.0-20221017141824-6f643e477c62 h1:XMG5DklHoioVYysfYglOB7vRBg/LOUJZy2mq2QyedLg=
|
||||
github.com/mneis/go-telnet v0.0.0-20221017141824-6f643e477c62/go.mod h1:niAM5cni0I/47IFA995xQfeK58Mkbb7FHJjacY4OGQg=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
|
||||
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
|
||||
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
|
||||
github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
|
||||
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
|
||||
github.com/olekukonko/ll v0.1.3 h1:sV2jrhQGq5B3W0nENUISCR6azIPf7UBUpVq0x/y70Fg=
|
||||
github.com/olekukonko/ll v0.1.3/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
|
||||
github.com/olekukonko/tablewriter v1.1.2 h1:L2kI1Y5tZBct/O/TyZK1zIE9GlBj/TVs+AY5tZDCDSc=
|
||||
github.com/olekukonko/tablewriter v1.1.2/go.mod h1:z7SYPugVqGVavWoA2sGsFIoOVNmEHxUAAMrhXONtfkg=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/reiver/go-oi v1.0.0 h1:nvECWD7LF+vOs8leNGV/ww+F2iZKf3EYjYZ527turzM=
|
||||
github.com/reiver/go-oi v1.0.0/go.mod h1:RrDBct90BAhoDTxB1fenZwfykqeGvhI6LsNfStJoEkI=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rqlite/gorqlite v0.0.0-20250609141355-ac86a4a1c9a8 h1:BoxiqWvhprOB2isgM59s8wkgKwAoyQH66Twfmof41oE=
|
||||
github.com/rqlite/gorqlite v0.0.0-20250609141355-ac86a4a1c9a8/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -122,38 +142,41 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tg123/go-htpasswd v1.2.4 h1:HgH8KKCjdmo7jjXWN9k1nefPBd7Be3tFCTjc2jPraPU=
|
||||
github.com/tg123/go-htpasswd v1.2.4/go.mod h1:EKThQok9xHkun6NBMynNv6Jmu24A33XdZzzl4Q7H1+0=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/vanng822/css v1.0.1 h1:10yiXc4e8NI8ldU6mSrWmSWMuyWgPr9DZ63RSlsgDw8=
|
||||
github.com/vanng822/css v1.0.1/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w=
|
||||
github.com/vanng822/go-premailer v1.25.0 h1:hGHKfroCXrCDTyGVR8o4HCON5/HWvc7C1uocS+VnaZs=
|
||||
github.com/vanng822/go-premailer v1.25.0/go.mod h1:8WJKIPZtegxqSOA8+eDFx7QNesKmMYfGEIodLTJqrtM=
|
||||
github.com/vanng822/go-premailer v1.30.0 h1:9oAp2PrJm4rvPnBgP57J/K1sJ1fQvSrU8TxamFvvwGU=
|
||||
github.com/vanng822/go-premailer v1.30.0/go.mod h1:1okMIRBIcWIK1g5vJKaXi2ytD1ulsIc9wUGwK7UD3/I=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
|
||||
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||
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.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
@@ -163,8 +186,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
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=
|
||||
@@ -172,8 +195,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -187,8 +210,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -207,37 +230,41 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
|
||||
modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY=
|
||||
modernc.org/fileutil v1.3.15/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY=
|
||||
modernc.org/libc v1.66.7/go.mod h1:ln6tbWX0NH+mzApEoDRvilBvAWFt1HX7AUA4VDdVDPM=
|
||||
modernc.org/libc v1.67.4 h1:zZGmCMUVPORtKv95c2ReQN5VDjvkoRm9GWPTEPuvlWg=
|
||||
modernc.org/libc v1.67.4/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -246,8 +273,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/sqlite v1.42.2 h1:7hkZUNJvJFN2PgfUdjni9Kbvd4ef4mNLOu0B9FGxM74=
|
||||
modernc.org/sqlite v1.42.2/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"api_version":"1.0.4",
|
||||
"last_update_date":"2025-08-13 15:26:12 +0000",
|
||||
"last_update_date":"2025-11-10 14:54:35 +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":[
|
||||
{
|
||||
@@ -238,7 +238,7 @@
|
||||
"last_test_date":"2023-12-13",
|
||||
"test_url":"https://www.caniemail.com/tests/css-media.html",
|
||||
"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","2025-07":"n"},"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","2025-07":"n"}},"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"}}},
|
||||
"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","2025-07":"n"},"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","2025-06":"a #2"}},"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","2025-07":"n"}},"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 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."}
|
||||
},
|
||||
@@ -446,7 +446,7 @@
|
||||
"last_test_date":"2023-12-20",
|
||||
"test_url":"https://www.caniemail.com/tests/css-border-collapse.html",
|
||||
"test_results_url":"https://testi.at/proj/4zk4fe7tv86fn4bc6",
|
||||
"stats":{"apple-mail":{"macos":{"2023-12":"y"},"ios":{"2023-12":"y"}},"gmail":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"},"mobile-webmail":{"2023-12":"y"}},"orange":{"desktop-webmail":{"2023-12":"u"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"outlook":{"windows":{"2023-12":"y"},"windows-mail":{"2023-12":"y"},"macos":{"2023-12":"y"},"outlook-com":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"yahoo":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"aol":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"samsung-email":{"android":{"2023-12":"y"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"2023-12":"y"}},"protonmail":{"desktop-webmail":{"2023-12":"u"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"hey":{"desktop-webmail":{"2023-12":"u"}},"mail-ru":{"desktop-webmail":{"2023-12":"y"}},"fastmail":{"desktop-webmail":{"2023-12":"u"}},"laposte":{"desktop-webmail":{"2023-12":"u"}},"gmx":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"web-de":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"ionos-1and1":{"desktop-webmail":{"2023-12":"u"},"android":{"2023-12":"u"}}},
|
||||
"stats":{"apple-mail":{"macos":{"2023-12":"y"},"ios":{"2023-12":"y"}},"gmail":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"},"mobile-webmail":{"2023-12":"y"}},"orange":{"desktop-webmail":{"2023-12":"u"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"outlook":{"windows":{"2023-12":"y"},"windows-mail":{"2023-12":"y"},"macos":{"2023-12":"y"},"outlook-com":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"yahoo":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"aol":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"samsung-email":{"android":{"2023-12":"y"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"2023-12":"y"}},"protonmail":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"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
|
||||
},
|
||||
@@ -558,7 +558,7 @@
|
||||
"last_test_date":"2023-12-20",
|
||||
"test_url":"https://www.caniemail.com/tests/css-border-spacing.html",
|
||||
"test_results_url":"https://testi.at/proj/dyodfk8c5dhjanflz",
|
||||
"stats":{"apple-mail":{"macos":{"2023-12":"y"},"ios":{"2023-12":"y"}},"gmail":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"},"mobile-webmail":{"2023-12":"y"}},"orange":{"desktop-webmail":{"2023-12":"u"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2023-12":"n"},"macos":{"2023-12":"n"},"outlook-com":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"yahoo":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"aol":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"samsung-email":{"android":{"2023-12":"y"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"2023-12":"y"}},"protonmail":{"desktop-webmail":{"2023-12":"u"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"hey":{"desktop-webmail":{"2023-12":"u"}},"mail-ru":{"desktop-webmail":{"2023-12":"y"}},"fastmail":{"desktop-webmail":{"2023-12":"u"}},"laposte":{"desktop-webmail":{"2023-12":"u"}},"gmx":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"web-de":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"ionos-1and1":{"desktop-webmail":{"2023-12":"u"},"android":{"2023-12":"u"}}},
|
||||
"stats":{"apple-mail":{"macos":{"2023-12":"y"},"ios":{"2023-12":"y"}},"gmail":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"},"mobile-webmail":{"2023-12":"y"}},"orange":{"desktop-webmail":{"2023-12":"u"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2023-12":"n"},"macos":{"2023-12":"n"},"outlook-com":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"yahoo":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"aol":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"samsung-email":{"android":{"2023-12":"y"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"2023-12":"y"}},"protonmail":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"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
|
||||
},
|
||||
@@ -846,7 +846,7 @@
|
||||
"last_test_date":"2024-08-23",
|
||||
"test_url":"https://www.caniemail.com/tests/css-empty-cells.html",
|
||||
"test_results_url":"https://testi.at/proj/kgl7t57xs8jxueze0v8",
|
||||
"stats":{"apple-mail":{"macos":{"2024-08":"y"},"ios":{"2024-08":"y"}},"gmail":{"desktop-webmail":{"2024-08":"y"},"ios":{"2024-08":"y"},"android":{"2024-08":"y"},"mobile-webmail":{"2024-08":"u"}},"orange":{"desktop-webmail":{"2024-08":"u"},"ios":{"2024-08":"u"},"android":{"2024-08":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2024-08":"n"},"macos":{"2024-08":"y"},"outlook-com":{"2024-08":"y"},"ios":{"2024-08":"y"},"android":{"2024-08":"y"}},"yahoo":{"desktop-webmail":{"2024-08":"y"},"ios":{"2024-08":"n"},"android":{"2024-08":"n"}},"aol":{"desktop-webmail":{"2024-08":"y"},"ios":{"2024-08":"n"},"android":{"2024-08":"n"}},"samsung-email":{"android":{"2024-08":"y"}},"sfr":{"desktop-webmail":{"2024-03":"u"},"ios":{"2024-03":"u"},"android":{"2024-03":"u"}},"protonmail":{"desktop-webmail":{"2024-08":"u"},"ios":{"2024-08":"u"},"android":{"2024-08":"u"}},"hey":{"desktop-webmail":{"2024-08":"u"}},"mail-ru":{"desktop-webmail":{"2024-08":"n"}},"fastmail":{"desktop-webmail":{"2024-08":"u"}}},
|
||||
"stats":{"apple-mail":{"macos":{"2024-08":"y"},"ios":{"2024-08":"y"}},"gmail":{"desktop-webmail":{"2024-08":"y"},"ios":{"2024-08":"y"},"android":{"2024-08":"y"},"mobile-webmail":{"2024-08":"u"}},"orange":{"desktop-webmail":{"2024-08":"u"},"ios":{"2024-08":"u"},"android":{"2024-08":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2024-08":"n"},"macos":{"2024-08":"y"},"outlook-com":{"2024-08":"y"},"ios":{"2024-08":"y"},"android":{"2024-08":"y"}},"yahoo":{"desktop-webmail":{"2024-08":"y"},"ios":{"2024-08":"n"},"android":{"2024-08":"n"}},"aol":{"desktop-webmail":{"2024-08":"y"},"ios":{"2024-08":"n"},"android":{"2024-08":"n"}},"samsung-email":{"android":{"2024-08":"y"}},"sfr":{"desktop-webmail":{"2024-03":"u"},"ios":{"2024-03":"u"},"android":{"2024-03":"u"}},"protonmail":{"desktop-webmail":{"2024-08":"y"},"ios":{"2024-08":"y"},"android":{"2024-08":"y"}},"hey":{"desktop-webmail":{"2024-08":"u"}},"mail-ru":{"desktop-webmail":{"2024-08":"n"}},"fastmail":{"desktop-webmail":{"2024-08":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":null
|
||||
},
|
||||
@@ -926,7 +926,7 @@
|
||||
"last_test_date":"2022-08-01",
|
||||
"test_url":"https://www.caniemail.com/tests/css-font-kerning.html",
|
||||
"test_results_url":"https://app.emailonacid.com/app/acidtest/RlRYNGDjVNBhofxCNxloUcRbUVWGDhJ2kZ4fy6HXpEatH/list",
|
||||
"stats":{"apple-mail":{"macos":{"11":"y","12":"y","13":"y","14":"y","15":"y"},"ios":{"11":"y","12":"y","13":"y","14":"y","15":"y"}},"gmail":{"desktop-webmail":{"2022-08":"y"},"ios":{"2022-08":"y"},"android":{"2022-08":"y"},"mobile-webmail":{"2022-08":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"macos":{"2022-08":"y","16.80":"y"},"outlook-com":{"2022-08":"y"},"ios":{"2022-08":"y"},"android":{"4.2101.1":"y"}},"yahoo":{"desktop-webmail":{"2022-08":"n","2024-03":"n"},"ios":{"2024-03":"n"},"android":{"2024-03":"n"}},"aol":{"desktop-webmail":{"2022-08":"n"}},"samsung-email":{"android":{"6.1.31.2":"y"}},"mail-ru":{"desktop-webmail":{"2022-08":"n"}},"free-fr":{"desktop-webmail":{"2022-08":"y"}},"gmx":{"desktop-webmail":{"2022-08":"n"}},"t-online-de":{"desktop-webmail":{"2022-08":"n"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"u"},"android":{"2024-03":"u"}},"thunderbird":{"macos":{"128.9.0":"y"}}},
|
||||
"stats":{"apple-mail":{"macos":{"11":"y","12":"y","13":"y","14":"y","15":"y"},"ios":{"11":"y","12":"y","13":"y","14":"y","15":"y"}},"gmail":{"desktop-webmail":{"2022-08":"y"},"ios":{"2022-08":"y"},"android":{"2022-08":"y"},"mobile-webmail":{"2022-08":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"macos":{"2022-08":"y","16.80":"y"},"outlook-com":{"2022-08":"y"},"ios":{"2022-08":"y"},"android":{"4.2101.1":"y"}},"yahoo":{"desktop-webmail":{"2022-08":"n","2024-03":"n"},"ios":{"2024-03":"n"},"android":{"2024-03":"n"}},"aol":{"desktop-webmail":{"2022-08":"n"}},"samsung-email":{"android":{"6.1.31.2":"y"}},"mail-ru":{"desktop-webmail":{"2022-08":"n"}},"free-fr":{"desktop-webmail":{"2022-08":"y"}},"gmx":{"desktop-webmail":{"2022-08":"n"}},"t-online-de":{"desktop-webmail":{"2022-08":"n"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"u"},"android":{"2024-03":"u"}},"thunderbird":{"macos":{"128.9.0":"y"}},"protonmail":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":null
|
||||
},
|
||||
@@ -1166,9 +1166,9 @@
|
||||
"last_test_date":"2021-11-30",
|
||||
"test_url":"https://www.caniemail.com/tests/css-important.html",
|
||||
"test_results_url":"https://app.emailonacid.com/app/acidtest/JEyxyPfKHFZCPKxlgiOugpH4lyNrXX39cd9M8xaW1DojH/list",
|
||||
"stats":{"apple-mail":{"macos":{"13":"y","15":"y"},"ios":{"15":"y"}},"gmail":{"desktop-webmail":{"2021-11":"a #1"},"ios":{"2021-11":"a #1 #2"},"android":{"2021-11":"a #1 #2"},"mobile-webmail":{"2021-11":"a #1 #2"}},"orange":{"desktop-webmail":{"2021-11":"n #3"},"ios":{"2021-11":"n #3"},"android":{"2021-11":"n #3"}},"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-11":"a #4"},"macos":{"16.56":"y","16.80":"y"},"outlook-com":{"2021-11":"y"},"ios":{"2021-11":"y"},"android":{"2021-11":"y"}},"samsung-email":{"android":{"7.0":"y"}},"sfr":{"desktop-webmail":{"2021-11":"y"},"ios":{"2021-11":"a #6"},"android":{"2021-11":"a #6"}},"thunderbird":{"macos":{"91.2.1":"y"}},"aol":{"desktop-webmail":{"2021-11":"a #7"},"ios":{"2021-11":"a #7"},"android":{"2021-11":"a #7"}},"yahoo":{"desktop-webmail":{"2021-11":"a #7"},"ios":{"2021-11":"a #7"},"android":{"2021-11":"a #7"}},"protonmail":{"desktop-webmail":{"2021-11":"a #8"},"ios":{"2021-11":"a #6"},"android":{"2021-11":"a #6"}},"hey":{"desktop-webmail":{"2021-11":"y"}},"mail-ru":{"desktop-webmail":{"2021-11":"y"}},"fastmail":{"desktop-webmail":{"2021-11":"y"}},"laposte":{"desktop-webmail":{"2021-11":"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"}},"wp-pl":{"desktop-webmail":{"2023-12":"n"}}},
|
||||
"stats":{"apple-mail":{"macos":{"13":"y","15":"y"},"ios":{"15":"y"}},"gmail":{"desktop-webmail":{"2021-11":"a #1"},"ios":{"2021-11":"a #1 #2"},"android":{"2021-11":"a #1 #2"},"mobile-webmail":{"2021-11":"a #1 #2"}},"orange":{"desktop-webmail":{"2021-11":"n #3"},"ios":{"2021-11":"n #3"},"android":{"2021-11":"n #3"}},"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-11":"a #4"},"macos":{"16.56":"y","16.80":"y"},"outlook-com":{"2021-11":"y"},"ios":{"2021-11":"y"},"android":{"2021-11":"y"}},"samsung-email":{"android":{"7.0":"y"}},"sfr":{"desktop-webmail":{"2021-11":"y"},"ios":{"2021-11":"a #6"},"android":{"2021-11":"a #6"}},"thunderbird":{"macos":{"91.2.1":"y"}},"aol":{"desktop-webmail":{"2021-11":"a #7"},"ios":{"2021-11":"a #7"},"android":{"2021-11":"a #7"}},"yahoo":{"desktop-webmail":{"2021-11":"a #7"},"ios":{"2021-11":"a #7"},"android":{"2021-11":"a #7"}},"protonmail":{"desktop-webmail":{"2021-11":"a #8"},"ios":{"2021-11":"a #6","2025-08":"a #9"},"android":{"2021-11":"a #6"}},"hey":{"desktop-webmail":{"2021-11":"y"}},"mail-ru":{"desktop-webmail":{"2021-11":"y"}},"fastmail":{"desktop-webmail":{"2021-11":"y"}},"laposte":{"desktop-webmail":{"2021-11":"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"}},"wp-pl":{"desktop-webmail":{"2023-12":"n"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":{"1":"Partial. Only supported in lowercase. (See [email-bugs#13](https://github.com/hteumeuleu/email-bugs/issues/13))","2":"Partial. Only supported inline when using a Non Gmail Account due to the lack of `<style>` support.","3":"Not supported. The entire declaration is removed if there is no space before `!important`.","4":"Partial. Only supported with a space before.","5":"Partial. Not supported inline. (See [email-bugs#31](https://github.com/hteumeuleu/email-bugs/issues/31))","6":"Partial. Only supported inline due to the lack of `<style>` support.","7":"Partial. Removed if there is no space before when used with a `background-image` property. (See [email-bugs#16](https://github.com/hteumeuleu/email-bugs/issues/16))","8":"Partial. Only supported in lowercase."}
|
||||
"notes_by_num":{"1":"Partial. Only supported in lowercase. (See [email-bugs#13](https://github.com/hteumeuleu/email-bugs/issues/13))","2":"Partial. Only supported inline when using a Non Gmail Account due to the lack of `<style>` support.","3":"Not supported. The entire declaration is removed if there is no space before `!important`.","4":"Partial. Only supported with a space before.","5":"Partial. Not supported inline. (See [email-bugs#31](https://github.com/hteumeuleu/email-bugs/issues/31))","6":"Partial. Only supported inline due to the lack of `<style>` support.","7":"Partial. Removed if there is no space before when used with a `background-image` property. (See [email-bugs#16](https://github.com/hteumeuleu/email-bugs/issues/16))","8":"Partial. Only supported in lowercase.","9":"Partial. Only supported if not written in lowercase."}
|
||||
},
|
||||
|
||||
{
|
||||
@@ -1502,7 +1502,7 @@
|
||||
"last_test_date":"2019-08-02",
|
||||
"test_url":"https://www.caniemail.com/tests/css-width-height.html",
|
||||
"test_results_url":"https://app.emailonacid.com/app/acidtest/dP8XNPcCLZGrogYGvFgCRRjJJO2nTWxchQ0WZSu0Pxcyb/list",
|
||||
"stats":{"apple-mail":{"macos":{"10.3":"y"},"ios":{"5.1":"a #2","6.1":"a #2","10.3":"y","12.2":"y"}},"gmail":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"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":{"2003":"a #2","2007":"n","2010":"n","2013":"n","2016":"n","2019":"a #1"},"windows-mail":{"2020-01":"a #1"},"macos":{"2011":"y","2016":"y","16.80":"y"},"outlook-com":{"2019-08":"y","2024-01":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"thunderbird":{"macos":{"60.7":"y"}},"aol":{"desktop-webmail":{"2020-01":"y"},"ios":{"2020-01":"a #2"},"android":{"2020-01":"y"}},"yahoo":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"a #2"},"android":{"2019-08":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"a #2"},"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"}}},
|
||||
"stats":{"apple-mail":{"macos":{"10.3":"y"},"ios":{"5.1":"a #2","6.1":"a #2","10.3":"y","12.2":"y"}},"gmail":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"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":{"2003":"a #2","2007":"n","2010":"n","2013":"n","2016":"n","2019":"a #1"},"windows-mail":{"2020-01":"a #1"},"macos":{"2011":"y","2016":"y","16.80":"y"},"outlook-com":{"2019-08":"y","2024-01":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"thunderbird":{"macos":{"60.7":"y"}},"aol":{"desktop-webmail":{"2020-01":"y"},"ios":{"2020-01":"a #2","2025-11":"y"},"android":{"2020-01":"y"}},"yahoo":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"a #2","2025-11":"y"},"android":{"2019-08":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"a #2","2025-11":"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. Only works on `<table>` elements.","2":"Partial. Doesn't work on `<table>` elements, as per [CSS 2.1 specification](https://www.w3.org/TR/CSS2/visudet.html#min-max-widths)."}
|
||||
},
|
||||
@@ -2574,7 +2574,7 @@
|
||||
"last_test_date":"2022-07-20",
|
||||
"test_url":"https://www.caniemail.com/tests/css-table-layout.html",
|
||||
"test_results_url":"https://testi.at/proj/G4buV6sBBxUr6quykrtVA3sk",
|
||||
"stats":{"apple-mail":{"macos":{"16":"y","17":"y","18":"y","19":"y","20":"y","21":"y"},"ios":{"11":"y","12":"y","13":"y","14":"y","15":"y"}},"gmail":{"desktop-webmail":{"2022-07":"y"},"ios":{"2022-07":"y"},"android":{"2022-07":"y"},"mobile-webmail":{"2022-07":"u"}},"orange":{"desktop-webmail":{"2022-07":"u"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2022-07":"n"},"macos":{"2022-07":"y","16.80":"n #1"},"outlook-com":{"2022-07":"y","2024-01":"y"},"ios":{"2022-07":"y"},"android":{"2022-07":"y"}},"yahoo":{"desktop-webmail":{"2022-07":"y"},"ios":{"2022-07":"y"},"android":{"2022-07":"y"}},"aol":{"desktop-webmail":{"2022-07":"y"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"samsung-email":{"android":{"2022-07":"y"}},"sfr":{"desktop-webmail":{"2022-07":"u"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"protonmail":{"desktop-webmail":{"2022-07":"u"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"hey":{"desktop-webmail":{"2022-07":"u"}},"mail-ru":{"desktop-webmail":{"2022-07":"y"}},"fastmail":{"desktop-webmail":{"2022-07":"u"}}},
|
||||
"stats":{"apple-mail":{"macos":{"16":"y","17":"y","18":"y","19":"y","20":"y","21":"y"},"ios":{"11":"y","12":"y","13":"y","14":"y","15":"y"}},"gmail":{"desktop-webmail":{"2022-07":"y"},"ios":{"2022-07":"y"},"android":{"2022-07":"y"},"mobile-webmail":{"2022-07":"u"}},"orange":{"desktop-webmail":{"2022-07":"u"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2022-07":"n"},"macos":{"2022-07":"y","16.80":"n #1"},"outlook-com":{"2022-07":"y","2024-01":"y"},"ios":{"2022-07":"y"},"android":{"2022-07":"y"}},"yahoo":{"desktop-webmail":{"2022-07":"y"},"ios":{"2022-07":"y"},"android":{"2022-07":"y"}},"aol":{"desktop-webmail":{"2022-07":"y"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"samsung-email":{"android":{"2022-07":"y"}},"sfr":{"desktop-webmail":{"2022-07":"u"},"ios":{"2022-07":"u"},"android":{"2022-07":"u"}},"protonmail":{"desktop-webmail":{"2022-07":"y"},"ios":{"2022-07":"y"},"android":{"2022-07":"y"}},"hey":{"desktop-webmail":{"2022-07":"u"}},"mail-ru":{"desktop-webmail":{"2022-07":"y"}},"fastmail":{"desktop-webmail":{"2022-07":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":{"1":"Not supported. All tables are forced to `table-layout:fixed`."}
|
||||
},
|
||||
@@ -2590,7 +2590,7 @@
|
||||
"last_test_date":"2022-08-31",
|
||||
"test_url":"https://www.caniemail.com/tests/css-text-align-last.html",
|
||||
"test_results_url":"https://app.emailonacid.com/app/acidtest/LxplTmJT9Ilq9GUyn8Aq8MVK6EO427qmx1Ic4A7jc7bOJ/list",
|
||||
"stats":{"apple-mail":{"macos":{"2022-10":"y"},"ios":{"11":"n","12":"n","13":"n","14":"n","15":"n","16.0":"y"}},"gmail":{"desktop-webmail":{"2022-08":"y"},"ios":{"2022-08":"y"},"android":{"2022-08":"y"},"mobile-webmail":{"2021-08":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"macos":{"2022-08":"n","16.80":"n"},"outlook-com":{"2022-08":"y","2024-01":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"}},"yahoo":{"desktop-webmail":{"2022-08":"n","2024-03":"n"},"ios":{"2024-03":"n"},"android":{"2024-03":"n"}},"aol":{"desktop-webmail":{"2022-08":"n"}},"samsung-email":{"android":{"6.1.51.1":"y"}},"free-fr":{"desktop-webmail":{"2022-08":"y"}},"t-online-de":{"desktop-webmail":{"2022-08":"y"}},"mail-ru":{"desktop-webmail":{"2022-08":"y"}}},
|
||||
"stats":{"apple-mail":{"macos":{"2022-10":"y"},"ios":{"11":"n","12":"n","13":"n","14":"n","15":"n","16.0":"y"}},"gmail":{"desktop-webmail":{"2022-08":"y"},"ios":{"2022-08":"y"},"android":{"2022-08":"y"},"mobile-webmail":{"2021-08":"y"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"macos":{"2022-08":"n","16.80":"n"},"outlook-com":{"2022-08":"y","2024-01":"n"},"ios":{"2022-08":"n"},"android":{"2022-08":"n"}},"yahoo":{"desktop-webmail":{"2022-08":"n","2024-03":"n"},"ios":{"2024-03":"n"},"android":{"2024-03":"n"}},"aol":{"desktop-webmail":{"2022-08":"n"}},"samsung-email":{"android":{"6.1.51.1":"y"}},"protonmail":{"desktop-webmail":{"2022-07":"y"},"ios":{"2022-07":"y"},"android":{"2022-07":"y"}},"free-fr":{"desktop-webmail":{"2022-08":"y"}},"t-online-de":{"desktop-webmail":{"2022-08":"y"}},"mail-ru":{"desktop-webmail":{"2022-08":"y"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":null
|
||||
},
|
||||
@@ -2670,7 +2670,7 @@
|
||||
"last_test_date":"2023-12-06",
|
||||
"test_url":"https://www.caniemail.com/tests/css-text-decoration-style.html",
|
||||
"test_results_url":"https://testi.at/proj/jalr04oy0yrxfd7kuo",
|
||||
"stats":{"apple-mail":{"macos":{"2023-12":"y"},"ios":{"11":"n","12":"y","13":"y","14":"y"}},"gmail":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"6":"n","7":"n","8":"y","9":"y","10":"y","11":"y","12":"y","13":"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":"y","16.80":"y"},"outlook-com":{"2023-12":"y","2024-01":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"yahoo":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"n","2025-06":"y"}},"aol":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"n"}},"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":"n"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"web-de":{"desktop-webmail":{"2023-12":"n"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"ionos-1and1":{"desktop-webmail":{"2023-12":"u"},"android":{"2023-12":"u"}}},
|
||||
"stats":{"apple-mail":{"macos":{"2023-12":"y"},"ios":{"11":"n","12":"y","13":"y","14":"y"}},"gmail":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"6":"n","7":"n","8":"y","9":"y","10":"y","11":"y","12":"y","13":"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":"y","16.80":"y"},"outlook-com":{"2023-12":"y","2024-01":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"yahoo":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"n","2025-06":"y"}},"aol":{"desktop-webmail":{"2023-12":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"n"}},"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":"y"},"ios":{"2023-12":"y"},"android":{"2023-12":"y"}},"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":"n"},"ios":{"2023-12":"u"},"android":{"2023-12":"u"}},"web-de":{"desktop-webmail":{"2023-12":"n"},"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
|
||||
},
|
||||
@@ -3422,7 +3422,7 @@
|
||||
"last_test_date":"2024-01-03",
|
||||
"test_url":"https://www.caniemail.com/tests/html-acronym.html",
|
||||
"test_results_url":"https://testi.at/proj/ayebhgpxu58yce2bhd",
|
||||
"stats":{"apple-mail":{"macos":{"2024-01":"y"},"ios":{"2024-01":"y"}},"gmail":{"desktop-webmail":{"2024-01":"y"},"ios":{"2024-01":"y"},"android":{"2024-01":"y"},"mobile-webmail":{"2024-01":"y"}},"orange":{"desktop-webmail":{"2024-01":"u"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"outlook":{"windows":{"2013":"n #1","2016":"n #1","2019":"n #1","2021":"n #1"},"windows-mail":{"2024-01":"n #1"},"macos":{"2024-01":"y"},"outlook-com":{"2024-01":"y"},"ios":{"2024-01":"y"},"android":{"2024-01":"y"}},"samsung-email":{"android":{"2024-01":"y"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"2024-01":"y"}},"aol":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"n"},"android":{"2024-01":"n"}},"yahoo":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"n"},"android":{"2024-01":"n"}},"protonmail":{"desktop-webmail":{"2024-01":"u"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"hey":{"desktop-webmail":{"2024-01":"u"}},"mail-ru":{"desktop-webmail":{"2024-01":"n"}},"fastmail":{"desktop-webmail":{"2024-01":"u"}},"laposte":{"desktop-webmail":{"2024-01":"u"}},"free-fr":{"desktop-webmail":{"2024-01":"u"}},"t-online-de":{"desktop-webmail":{"2024-01":"y"}},"gmx":{"desktop-webmail":{"2024-01":"y"}},"web-de":{"desktop-webmail":{"2024-01":"y"}}},
|
||||
"stats":{"apple-mail":{"macos":{"2024-01":"y"},"ios":{"2024-01":"y"}},"gmail":{"desktop-webmail":{"2024-01":"y"},"ios":{"2024-01":"y"},"android":{"2024-01":"y"},"mobile-webmail":{"2024-01":"y"}},"orange":{"desktop-webmail":{"2024-01":"u"},"ios":{"2024-01":"u"},"android":{"2024-01":"u"}},"outlook":{"windows":{"2013":"n #1","2016":"n #1","2019":"n #1","2021":"n #1"},"windows-mail":{"2024-01":"n #1"},"macos":{"2024-01":"y"},"outlook-com":{"2024-01":"y"},"ios":{"2024-01":"y"},"android":{"2024-01":"y"}},"samsung-email":{"android":{"2024-01":"y"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"2024-01":"y"}},"aol":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"n"},"android":{"2024-01":"n"}},"yahoo":{"desktop-webmail":{"2024-01":"n"},"ios":{"2024-01":"n"},"android":{"2024-01":"n"}},"protonmail":{"desktop-webmail":{"2024-01":"y"},"ios":{"2024-01":"y"},"android":{"2024-01":"y"}},"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."}
|
||||
},
|
||||
@@ -3774,7 +3774,7 @@
|
||||
"last_test_date":"2023-09-11",
|
||||
"test_url":"https://www.caniemail.com/tests/html-dfn.html",
|
||||
"test_results_url":"https://testi.at/proj/gy2dfo4j19d4176d08y",
|
||||
"stats":{"apple-mail":{"macos":{"2023-09":"y"},"ios":{"2023-09":"y"}},"gmail":{"desktop-webmail":{"2023-09":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"},"mobile-webmail":{"2023-09":"y"}},"orange":{"desktop-webmail":{"2023-09":"u"},"ios":{"2023-09":"u"},"android":{"2023-09":"u"}},"outlook":{"windows":{"2013":"a #1","2016":"a #1","2019":"a #1","2021":"a #1"},"windows-mail":{"2023-09":"a #1"},"macos":{"16.56":"y","16.80":"y"},"outlook-com":{"2023-09":"y","2024-01":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"}},"samsung-email":{"android":{"2023-09":"y"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"2023-09":"y"}},"aol":{"desktop-webmail":{"2023-09":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"}},"yahoo":{"desktop-webmail":{"2023-09":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"}},"protonmail":{"desktop-webmail":{"2023-09":"u"},"ios":{"2023-09":"u"},"android":{"2023-09":"u"}},"hey":{"desktop-webmail":{"2023-09":"u"}},"mail-ru":{"desktop-webmail":{"2023-09":"n"}},"fastmail":{"desktop-webmail":{"2023-09":"u"}},"laposte":{"desktop-webmail":{"2023-09":"u"}},"free-fr":{"desktop-webmail":{"2023-09":"u"}},"t-online-de":{"desktop-webmail":{"2023-09":"n"}},"gmx":{"desktop-webmail":{"2023-09":"y"}},"web-de":{"desktop-webmail":{"2023-09":"y"}}},
|
||||
"stats":{"apple-mail":{"macos":{"2023-09":"y"},"ios":{"2023-09":"y"}},"gmail":{"desktop-webmail":{"2023-09":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"},"mobile-webmail":{"2023-09":"y"}},"orange":{"desktop-webmail":{"2023-09":"u"},"ios":{"2023-09":"u"},"android":{"2023-09":"u"}},"outlook":{"windows":{"2013":"a #1","2016":"a #1","2019":"a #1","2021":"a #1"},"windows-mail":{"2023-09":"a #1"},"macos":{"16.56":"y","16.80":"y"},"outlook-com":{"2023-09":"y","2024-01":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"}},"samsung-email":{"android":{"2023-09":"y"}},"sfr":{"desktop-webmail":{"2024-03":"y"},"ios":{"2024-03":"y"},"android":{"2024-03":"y"}},"thunderbird":{"macos":{"2023-09":"y"}},"aol":{"desktop-webmail":{"2023-09":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"}},"yahoo":{"desktop-webmail":{"2023-09":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"}},"protonmail":{"desktop-webmail":{"2023-09":"y"},"ios":{"2023-09":"y"},"android":{"2023-09":"y"}},"hey":{"desktop-webmail":{"2023-09":"u"}},"mail-ru":{"desktop-webmail":{"2023-09":"n"}},"fastmail":{"desktop-webmail":{"2023-09":"u"}},"laposte":{"desktop-webmail":{"2023-09":"u"}},"free-fr":{"desktop-webmail":{"2023-09":"u"}},"t-online-de":{"desktop-webmail":{"2023-09":"n"}},"gmx":{"desktop-webmail":{"2023-09":"y"}},"web-de":{"desktop-webmail":{"2023-09":"y"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":{"1":"Partial. The `title` attribute is removed."}
|
||||
},
|
||||
@@ -4478,9 +4478,9 @@
|
||||
"last_test_date":"2023-07-27",
|
||||
"test_url":"https://www.caniemail.com/tests/html-style.html",
|
||||
"test_results_url":"https://app.emailonacid.com/app/acidtest/CAMb612bxbVwRWPhM4wZKNhhdcdkNxj0Rj6dtRRw6LQUO/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","2025-07":"n"},"ios":{"2019-06":"n","2023-02":"n","2025-07":"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","2025-06":"y"}},"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","2025-07":"n"},"ios":{"2023-02":"n","2025-07":"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","2025-04":"n"},"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"}}},
|
||||
"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 #6"},"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","2025-07":"n"},"ios":{"2019-06":"n","2023-02":"n","2025-07":"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","2025-06":"y"}},"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","2025-07":"n"},"ios":{"2023-02":"n","2025-07":"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","2025-04":"n"},"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 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).)"}
|
||||
"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).)","6":"The size of the `<style>` tag [is limited to 16 KB](https://github.com/hteumeuleu/email-bugs/issues/90)"}
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@@ -4,25 +4,31 @@ import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/axllent/mailpit/internal/tools"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Wrapper to forward messages if configured
|
||||
func autoForwardMessage(from string, data *[]byte) {
|
||||
func autoForwardMessage(from string, data *[]byte) error {
|
||||
if config.SMTPForwardConfig.Host == "" {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := forward(from, *data); err != nil {
|
||||
logger.Log().Errorf("[forward] error: %s", err.Error())
|
||||
} else {
|
||||
logger.Log().Debugf("[forward] message from %s to %s via %s:%d",
|
||||
from, config.SMTPForwardConfig.To, config.SMTPForwardConfig.Host, config.SMTPForwardConfig.Port)
|
||||
return errors.WithMessage(err, "[forward] error: %s")
|
||||
}
|
||||
|
||||
logger.Log().Debugf(
|
||||
"[forward] message from %s to %s via %s:%d",
|
||||
from, config.SMTPForwardConfig.To, config.SMTPForwardConfig.Host, config.SMTPForwardConfig.Port,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createForwardingSMTPClient(config config.SMTPForwardConfigStruct, addr string) (*smtp.Client, error) {
|
||||
@@ -50,6 +56,13 @@ func createForwardingSMTPClient(config config.SMTPForwardConfigStruct, addr stri
|
||||
return nil, fmt.Errorf("error connecting to %s: %v", addr, err)
|
||||
}
|
||||
|
||||
// Set the hostname for HELO/EHLO
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
if err := client.Hello(hostname); err != nil {
|
||||
return nil, fmt.Errorf("error saying HELO/EHLO to %s: %v", addr, err)
|
||||
}
|
||||
}
|
||||
|
||||
if config.STARTTLS {
|
||||
tlsConf := &tls.Config{ServerName: config.Host} // #nosec
|
||||
tlsConf.InsecureSkipVerify = config.AllowInsecure
|
||||
@@ -100,6 +113,9 @@ func forward(from string, msg []byte) error {
|
||||
for _, addr := range to {
|
||||
if err = c.Rcpt(addr); err != nil {
|
||||
logger.Log().Warnf("error response to RCPT command for %s: %s", addr, err.Error())
|
||||
if config.SMTPForwardConfig.ForwardSMTPErrors {
|
||||
return errors.WithMessagef(err, "error response to RCPT command for %s", addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/axllent/mailpit/internal/tools"
|
||||
"github.com/axllent/mailpit/server/websockets"
|
||||
"github.com/lithammer/shortuuid/v4"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -73,10 +74,36 @@ func SaveToDatabase(origin net.Addr, from string, to []string, data []byte, smtp
|
||||
}
|
||||
|
||||
// if enabled, this may conditionally relay the email through to the preconfigured smtp server
|
||||
autoRelayMessage(from, to, &data)
|
||||
if relayErr := autoRelayMessage(from, to, &data); relayErr != nil {
|
||||
logger.Log().Error(relayErr.Error())
|
||||
|
||||
if config.SMTPRelayConfig.ForwardSMTPErrors {
|
||||
for {
|
||||
unwrappedErr := errors.Unwrap(relayErr)
|
||||
if unwrappedErr == nil {
|
||||
break
|
||||
}
|
||||
relayErr = unwrappedErr
|
||||
}
|
||||
return "", relayErr
|
||||
}
|
||||
}
|
||||
|
||||
// if enabled, this will forward a copy to preconfigured addresses
|
||||
autoForwardMessage(from, &data)
|
||||
if forwardErr := autoForwardMessage(from, &data); forwardErr != nil {
|
||||
logger.Log().Error(forwardErr.Error())
|
||||
|
||||
if config.SMTPForwardConfig.ForwardSMTPErrors {
|
||||
for {
|
||||
unwrappedErr := errors.Unwrap(forwardErr)
|
||||
if unwrappedErr == nil {
|
||||
break
|
||||
}
|
||||
forwardErr = unwrappedErr
|
||||
}
|
||||
return "", forwardErr
|
||||
}
|
||||
}
|
||||
|
||||
// build array of all addresses in the header to compare to the []to array
|
||||
emails, hasBccHeader := scanAddressesInHeader(msg.Header)
|
||||
@@ -225,15 +252,27 @@ func listenAndServe(addr string, handler MsgIDHandler, authHandler AuthHandler)
|
||||
}
|
||||
|
||||
if config.SMTPAuthAllowInsecure {
|
||||
srv.AuthMechs = map[string]bool{"CRAM-MD5": false, "PLAIN": true, "LOGIN": true}
|
||||
srv.AuthMechs = map[string]bool{
|
||||
"CRAM-MD5": false,
|
||||
"PLAIN": true,
|
||||
"LOGIN": true,
|
||||
}
|
||||
}
|
||||
|
||||
if auth.SMTPCredentials != nil {
|
||||
srv.AuthMechs = map[string]bool{"CRAM-MD5": false, "PLAIN": true, "LOGIN": true}
|
||||
srv.AuthMechs = map[string]bool{
|
||||
"CRAM-MD5": false,
|
||||
"PLAIN": true,
|
||||
"LOGIN": true,
|
||||
}
|
||||
srv.AuthHandler = authHandler
|
||||
srv.AuthRequired = true
|
||||
} else if config.SMTPAuthAcceptAny {
|
||||
srv.AuthMechs = map[string]bool{"CRAM-MD5": false, "PLAIN": true, "LOGIN": true}
|
||||
srv.AuthMechs = map[string]bool{
|
||||
"CRAM-MD5": false,
|
||||
"PLAIN": true,
|
||||
"LOGIN": true,
|
||||
}
|
||||
srv.AuthHandler = authHandlerAny
|
||||
}
|
||||
|
||||
|
||||
@@ -2,18 +2,19 @@ package smtpd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/axllent/mailpit/internal/tools"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Wrapper to auto relay messages if configured
|
||||
func autoRelayMessage(from string, to []string, data *[]byte) {
|
||||
func autoRelayMessage(from string, to []string, data *[]byte) error {
|
||||
if config.SMTPRelayConfig.BlockedRecipientsRegexp != nil {
|
||||
filteredTo := []string{}
|
||||
for _, address := range to {
|
||||
@@ -28,16 +29,18 @@ func autoRelayMessage(from string, to []string, data *[]byte) {
|
||||
}
|
||||
|
||||
if len(to) == 0 {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if config.SMTPRelayAll {
|
||||
if err := Relay(from, to, *data); err != nil {
|
||||
logger.Log().Errorf("[relay] error: %s", err.Error())
|
||||
} else {
|
||||
logger.Log().Debugf("[relay] sent message to %s from %s via %s:%d",
|
||||
strings.Join(to, ", "), from, config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port)
|
||||
return errors.WithMessage(err, "[relay] error")
|
||||
}
|
||||
|
||||
logger.Log().Debugf(
|
||||
"[relay] sent message to %s from %s via %s:%d",
|
||||
strings.Join(to, ", "), from, config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port,
|
||||
)
|
||||
} else if config.SMTPRelayMatchingRegexp != nil {
|
||||
filtered := []string{}
|
||||
for _, t := range to {
|
||||
@@ -47,16 +50,20 @@ func autoRelayMessage(from string, to []string, data *[]byte) {
|
||||
}
|
||||
|
||||
if len(filtered) == 0 {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := Relay(from, filtered, *data); err != nil {
|
||||
logger.Log().Errorf("[relay] error: %s", err.Error())
|
||||
} else {
|
||||
logger.Log().Debugf("[relay] auto-relay message to %s from %s via %s:%d",
|
||||
strings.Join(filtered, ", "), from, config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port)
|
||||
return errors.WithMessage(err, "[relay] error")
|
||||
}
|
||||
|
||||
logger.Log().Debugf(
|
||||
"[relay] auto-relay message to %s from %s via %s:%d",
|
||||
strings.Join(filtered, ", "), from, config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createRelaySMTPClient(config config.SMTPRelayConfigStruct, addr string) (*smtp.Client, error) {
|
||||
@@ -84,6 +91,13 @@ func createRelaySMTPClient(config config.SMTPRelayConfigStruct, addr string) (*s
|
||||
return nil, fmt.Errorf("error connecting to %s: %v", addr, err)
|
||||
}
|
||||
|
||||
// Set the hostname for HELO/EHLO
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
if err := client.Hello(hostname); err != nil {
|
||||
return nil, fmt.Errorf("error saying HELO/EHLO to %s: %v", addr, err)
|
||||
}
|
||||
}
|
||||
|
||||
if config.STARTTLS {
|
||||
tlsConf := &tls.Config{ServerName: config.Host} // #nosec
|
||||
tlsConf.InsecureSkipVerify = config.AllowInsecure
|
||||
@@ -126,26 +140,29 @@ func Relay(from string, to []string, msg []byte) error {
|
||||
}
|
||||
|
||||
if err = c.Mail(from); err != nil {
|
||||
return fmt.Errorf("error response to MAIL command: %s", err.Error())
|
||||
return errors.WithMessage(err, "error sending MAIL command")
|
||||
}
|
||||
|
||||
for _, addr := range to {
|
||||
if err = c.Rcpt(addr); err != nil {
|
||||
logger.Log().Warnf("error response to RCPT command for %s: %s", addr, err.Error())
|
||||
if config.SMTPRelayConfig.ForwardSMTPErrors {
|
||||
return errors.WithMessagef(err, "error response to RCPT command for %s", addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error response to DATA command: %s", err.Error())
|
||||
return errors.WithMessage(err, "error response to DATA command")
|
||||
}
|
||||
|
||||
if _, err := w.Write(msg); err != nil {
|
||||
return fmt.Errorf("error sending message: %s", err.Error())
|
||||
return errors.WithMessage(err, "error sending message")
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return fmt.Errorf("error closing connection: %s", err.Error())
|
||||
return errors.WithMessage(err, "error closing connection")
|
||||
}
|
||||
|
||||
return c.Quit()
|
||||
@@ -178,7 +195,10 @@ type loginAuth struct {
|
||||
|
||||
// LoginAuth authentication
|
||||
func LoginAuth(username, password string) smtp.Auth {
|
||||
return &loginAuth{username, password}
|
||||
return &loginAuth{
|
||||
username,
|
||||
password,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *loginAuth) Start(_ *smtp.ServerInfo) (string, []byte, error) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -204,3 +205,94 @@ func BenchmarkImportMime(b *testing.B) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestInlineImageContentIdHandling(t *testing.T) {
|
||||
setup("")
|
||||
defer Close()
|
||||
t.Log("Testing inline content handling")
|
||||
// Test case: Proper inline image with Content-Disposition: inline
|
||||
inlineAttachment, err := os.ReadFile("testdata/inline-attachment.eml")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read test email: %v", err)
|
||||
}
|
||||
storedMessage, err := Store(&inlineAttachment, nil)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to store test case 1:", err)
|
||||
}
|
||||
|
||||
msg, err := GetMessage(storedMessage)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to retrieve test case 1:", err)
|
||||
}
|
||||
// Assert
|
||||
if len(msg.Inline) != 1 {
|
||||
t.Errorf("Test case 1: Expected 1 inline attachment, got %d", len(msg.Inline))
|
||||
}
|
||||
if len(msg.Attachments) != 0 {
|
||||
t.Errorf("Test case 1: Expected 0 regular attachments, got %d", len(msg.Attachments))
|
||||
}
|
||||
if msg.Inline[0].ContentID != "test1@example.com" {
|
||||
t.Errorf("Test case 1: Expected ContentID 'test1@example.com', got '%s'", msg.Inline[0].ContentID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegularAttachmentHandling(t *testing.T) {
|
||||
setup("")
|
||||
defer Close()
|
||||
t.Log("Testing regular attachment handling")
|
||||
// Test case: Regular attachment without Content-ID
|
||||
regularAttachment, err := os.ReadFile("testdata/regular-attachment.eml")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read test email: %v", err)
|
||||
}
|
||||
storedMessage, err := Store(®ularAttachment, nil)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to store test case 3:", err)
|
||||
}
|
||||
msg, err := GetMessage(storedMessage)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to retrieve test case 3:", err)
|
||||
}
|
||||
// Assert
|
||||
if len(msg.Inline) != 0 {
|
||||
t.Errorf("Test case 3: Expected 0 inline attachments, got %d", len(msg.Inline))
|
||||
}
|
||||
if len(msg.Attachments) != 1 {
|
||||
t.Errorf("Test case 3: Expected 1 regular attachment, got %d", len(msg.Attachments))
|
||||
}
|
||||
if msg.Attachments[0].ContentID != "" {
|
||||
t.Errorf("Test case 3: Expected empty ContentID, got '%s'", msg.Attachments[0].ContentID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMixedAttachmentHandling(t *testing.T) {
|
||||
setup("")
|
||||
defer Close()
|
||||
t.Log("Testing mixed attachment handling")
|
||||
// Mixed scenario with both inline and regular attachment
|
||||
mixedAttachment, err := os.ReadFile("testdata/mixed-attachment.eml")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read test email: %v", err)
|
||||
}
|
||||
storedMessage, err := Store(&mixedAttachment, nil)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to store test case 4:", err)
|
||||
}
|
||||
msg, err := GetMessage(storedMessage)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to retrieve test case 4:", err)
|
||||
}
|
||||
// Assert: Should have 1 inline (with ContentID) and 1 attachment (without ContentID)
|
||||
if len(msg.Inline) != 1 {
|
||||
t.Errorf("Test case 4: Expected 1 inline attachment, got %d", len(msg.Inline))
|
||||
}
|
||||
if len(msg.Attachments) != 1 {
|
||||
t.Errorf("Test case 4: Expected 1 regular attachment, got %d", len(msg.Attachments))
|
||||
}
|
||||
if msg.Inline[0].ContentID != "inline@example.com" {
|
||||
t.Errorf("Test case 4: Expected inline ContentID 'inline@example.com', got '%s'", msg.Inline[0].ContentID)
|
||||
}
|
||||
if msg.Attachments[0].ContentID != "" {
|
||||
t.Errorf("Test case 4: Expected attachment ContentID to be empty, got '%s'", msg.Attachments[0].ContentID)
|
||||
}
|
||||
}
|
||||
|
||||
20
internal/storage/testdata/inline-attachment.eml
vendored
Normal file
20
internal/storage/testdata/inline-attachment.eml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
From: sender@example.com
|
||||
To: recipient@example.com
|
||||
Subject: Test inline image proper
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/related; boundary="boundary123"
|
||||
|
||||
--boundary123
|
||||
Content-Type: text/html; charset=utf-8
|
||||
|
||||
<html><body><img src="cid:test1@example.com" alt="Test"/></body></html>
|
||||
|
||||
--boundary123
|
||||
Content-Type: image/png; name="test1.png"
|
||||
Content-Disposition: inline; filename="test1.png"
|
||||
Content-ID: <test1@example.com>
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==
|
||||
|
||||
--boundary123--
|
||||
27
internal/storage/testdata/mixed-attachment.eml
vendored
Normal file
27
internal/storage/testdata/mixed-attachment.eml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
From: sender@example.com
|
||||
To: recipient@example.com
|
||||
Subject: Test mixed attachments
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed; boundary="boundary111"
|
||||
|
||||
--boundary111
|
||||
Content-Type: text/html; charset=utf-8
|
||||
|
||||
<html><body><img src="cid:inline@example.com" alt="Inline"/><p>Document attached</p></body></html>
|
||||
|
||||
--boundary111
|
||||
Content-Type: image/png; name="inline.png"
|
||||
Content-Disposition: inline; filename="inline.png"
|
||||
Content-ID: <inline@example.com>
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==
|
||||
|
||||
--boundary111
|
||||
Content-Type: application/pdf; name="document.pdf"
|
||||
Content-Disposition: attachment; filename="document.pdf"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURlY29kZT4+CnN0cmVhbQo=
|
||||
|
||||
--boundary111--
|
||||
19
internal/storage/testdata/regular-attachment.eml
vendored
Normal file
19
internal/storage/testdata/regular-attachment.eml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
From: sender@example.com
|
||||
To: recipient@example.com
|
||||
Subject: Test regular attachment
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed; boundary="boundary789"
|
||||
|
||||
--boundary789
|
||||
Content-Type: text/html; charset=utf-8
|
||||
|
||||
<html><body><p>Message with regular attachment</p></body></html>
|
||||
|
||||
--boundary789
|
||||
Content-Type: application/pdf; name="document.pdf"
|
||||
Content-Disposition: attachment; filename="document.pdf"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURlY29kZT4+CnN0cmVhbQo=
|
||||
|
||||
--boundary789--
|
||||
1261
package-lock.json
generated
1261
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,7 @@
|
||||
"@types/bootstrap": "^5.2.7",
|
||||
"@types/tinycon": "^0.6.3",
|
||||
"@vue/compiler-sfc": "^3.2.37",
|
||||
"esbuild": "^0.25.0",
|
||||
"esbuild": "^0.27.2",
|
||||
"esbuild-plugin-vue-next": "^0.1.4",
|
||||
"esbuild-sass-plugin": "^3.0.0",
|
||||
"eslint": "^9.29.0",
|
||||
|
||||
@@ -79,9 +79,9 @@ func Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
var dstImageFill *image.NRGBA
|
||||
|
||||
if img.Bounds().Dx() < thumbWidth || img.Bounds().Dy() < thumbHeight {
|
||||
dstImageFill = imaging.Fit(img, thumbWidth, thumbHeight, imaging.Lanczos)
|
||||
dstImageFill = imaging.Fit(img, thumbWidth, thumbHeight, imaging.Lanczos).(*image.NRGBA)
|
||||
} else {
|
||||
dstImageFill = imaging.Fill(img, thumbWidth, thumbHeight, imaging.Center, imaging.Lanczos)
|
||||
dstImageFill = imaging.Fill(img, thumbWidth, thumbHeight, imaging.Center, imaging.Lanczos).(*image.NRGBA)
|
||||
}
|
||||
// create white image and paste image over the top
|
||||
// preventing black backgrounds for transparent GIF/PNG images
|
||||
|
||||
@@ -3,32 +3,102 @@ package handlers
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/axllent/mailpit/internal/storage"
|
||||
"github.com/axllent/mailpit/internal/tools"
|
||||
)
|
||||
|
||||
var linkRe = regexp.MustCompile(`(?i)^https?:\/\/`)
|
||||
var (
|
||||
linkRe = regexp.MustCompile(`(?i)^https?:\/\/`)
|
||||
|
||||
// ProxyHandler is used to proxy assets for printing
|
||||
urlRe = regexp.MustCompile(`(?mU)url\(('|")?(https?:\/\/[^)'"]+)('|")?\)`)
|
||||
|
||||
assetsMutex sync.Mutex
|
||||
|
||||
assets = map[string]MessageAssets{}
|
||||
)
|
||||
|
||||
// MessageAssets represents assets linked in a message
|
||||
type MessageAssets struct {
|
||||
ID string
|
||||
// Created timestamp so we can expire old entries
|
||||
Created time.Time
|
||||
// Assets found in the message
|
||||
Assets []string
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Start a goroutine to clean up old asset entries every minute
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(time.Minute)
|
||||
assetsMutex.Lock()
|
||||
now := time.Now()
|
||||
for id, entry := range assets {
|
||||
if now.Sub(entry.Created) > time.Minute {
|
||||
logger.Log().Debugf("[proxy] cleaning up assets for message %s", id)
|
||||
delete(assets, id)
|
||||
}
|
||||
}
|
||||
assetsMutex.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// ProxyHandler is used to proxy assets for printing.
|
||||
// It accepts a base64-encoded message-id:url string as the `data` query parameter.
|
||||
func ProxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
uri := strings.TrimSpace(r.URL.Query().Get("url"))
|
||||
if uri == "" {
|
||||
logger.Log().Warn("[proxy] URL missing")
|
||||
httpError(w, "Error: URL missing")
|
||||
encoded := strings.TrimSpace(r.URL.Query().Get("data"))
|
||||
if encoded == "" {
|
||||
logger.Log().Warn("[proxy] Data missing")
|
||||
httpError(w, "Error: Data missing")
|
||||
return
|
||||
}
|
||||
|
||||
decoded, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
logger.Log().Warnf("[proxy] Data parameter corrupted: %s", err.Error())
|
||||
httpError(w, "Error: invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.SplitN(string(decoded), ":", 2)
|
||||
if len(parts) != 2 {
|
||||
logger.Log().Warnf("[proxy] Invalid data parameter: %s", string(decoded))
|
||||
httpError(w, "Error: invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
id := parts[0]
|
||||
uri := parts[1]
|
||||
|
||||
links, err := getAssets(id)
|
||||
if err != nil {
|
||||
httpError(w, "Error: invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
if !tools.InArray(uri, links) {
|
||||
logger.Log().Warnf("[proxy] URL %s not found in message %s", uri, id)
|
||||
httpError(w, "Error: invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
if !linkRe.MatchString(uri) {
|
||||
logger.Log().Warnf("[proxy] invalid URL %s", uri)
|
||||
httpError(w, "Error: invalid URL")
|
||||
logger.Log().Warnf("[proxy] invalid request %s", uri)
|
||||
httpError(w, "Error: invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -46,7 +116,7 @@ func ProxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := http.NewRequest("GET", uri, nil)
|
||||
if err != nil {
|
||||
logger.Log().Warnf("[proxy] %s", err.Error())
|
||||
httpError(w, err.Error())
|
||||
httpError(w, "Error: invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -56,23 +126,34 @@ func ProxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Log().Warnf("[proxy] %s", err.Error())
|
||||
httpError(w, err.Error())
|
||||
httpError(w, "Error: invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
logger.Log().Warnf("[proxy] received status code %d for %s", resp.StatusCode, uri)
|
||||
httpError(w, "Error: invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
ct := strings.ToLower(resp.Header.Get("content-type"))
|
||||
if !supportedProxyContentType(ct) {
|
||||
logger.Log().Warnf("[proxy] blocking unsupported content-type %s for %s", ct, uri)
|
||||
httpError(w, "Error: invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
logger.Log().Warnf("[proxy] %s", err.Error())
|
||||
httpError(w, err.Error())
|
||||
httpError(w, "Error: invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
// relay common headers
|
||||
if resp.Header.Get("content-type") != "" {
|
||||
w.Header().Set("content-type", resp.Header.Get("content-type"))
|
||||
}
|
||||
w.Header().Set("content-type", ct)
|
||||
if resp.Header.Get("last-modified") != "" {
|
||||
w.Header().Set("last-modified", resp.Header.Get("last-modified"))
|
||||
}
|
||||
@@ -83,7 +164,7 @@ func ProxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("cache-control", resp.Header.Get("cache-control"))
|
||||
}
|
||||
|
||||
// replace url() values with proxy address, eg: fonts & images
|
||||
// replace CSS url() values with proxy address, eg: fonts & images
|
||||
if strings.HasPrefix(resp.Header.Get("content-type"), "text/css") {
|
||||
var re = regexp.MustCompile(`(?mi)(url\((\'|\")?([^\)\'\"]+)(\'|\")?\))`)
|
||||
body = re.ReplaceAllFunc(body, func(s []byte) []byte {
|
||||
@@ -100,7 +181,20 @@ func ProxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return []byte(parts[3])
|
||||
}
|
||||
|
||||
return []byte("url(" + parts[2] + config.Webroot + "proxy?url=" + url.QueryEscape(address) + parts[4] + ")")
|
||||
// store asset address against message ID
|
||||
if result, ok := assets[id]; ok {
|
||||
if !tools.InArray(address, result.Assets) {
|
||||
assetsMutex.Lock()
|
||||
result.Assets = append(result.Assets, address)
|
||||
assets[id] = result
|
||||
assetsMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// encode with base64 to handle any special characters and group message ID with URL
|
||||
encoded := base64.StdEncoding.EncodeToString([]byte(id + ":" + address))
|
||||
|
||||
return []byte("url(" + parts[2] + config.Webroot + "proxy?data=" + encoded + parts[4] + ")")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -114,7 +208,82 @@ func ProxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// AbsoluteURL will return a full URL regardless whether it is relative or absolute
|
||||
// GetAssets retrieves and parses the message to return linked assets.
|
||||
// Linked CSS files are appended to the assets list via the ProxyHandler when proxying CSS files.
|
||||
func getAssets(id string) ([]string, error) {
|
||||
assetsMutex.Lock()
|
||||
defer assetsMutex.Unlock()
|
||||
|
||||
result, ok := assets[id]
|
||||
if ok {
|
||||
// return cached assets
|
||||
return result.Assets, nil
|
||||
}
|
||||
|
||||
msg, err := storage.GetMessage(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
links := []string{}
|
||||
|
||||
reader := strings.NewReader(msg.HTML)
|
||||
|
||||
// load the HTML document
|
||||
doc, err := goquery.NewDocumentFromReader(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// css & font links
|
||||
doc.Find("link").Each(func(_ int, s *goquery.Selection) {
|
||||
if href, exists := s.Attr("href"); exists {
|
||||
if linkRe.MatchString(href) && !tools.InArray(href, links) {
|
||||
links = append(links, href)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// images
|
||||
doc.Find("img").Each(func(_ int, s *goquery.Selection) {
|
||||
if src, exists := s.Attr("src"); exists {
|
||||
if linkRe.MatchString(src) && !tools.InArray(src, links) {
|
||||
links = append(links, src)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// background="<>" links
|
||||
doc.Find("[background]").Each(func(_ int, s *goquery.Selection) {
|
||||
if bg, exists := s.Attr("background"); exists {
|
||||
if linkRe.MatchString(bg) && !tools.InArray(bg, links) {
|
||||
links = append(links, bg)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// url(<>) links in style blocks
|
||||
matches := urlRe.FindAllStringSubmatch(msg.HTML, -1)
|
||||
for _, match := range matches {
|
||||
if len(match) >= 3 {
|
||||
link := match[2]
|
||||
if linkRe.MatchString(link) && !tools.InArray(link, links) {
|
||||
links = append(links, link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r := MessageAssets{}
|
||||
r.ID = id
|
||||
r.Created = time.Now()
|
||||
r.Assets = links
|
||||
assets[id] = r
|
||||
|
||||
return links, nil
|
||||
}
|
||||
|
||||
// AbsoluteURL will return a full URL regardless whether it is relative or absolute.
|
||||
// This is used to replace relative CSS url(...) links when proxying.
|
||||
func absoluteURL(link, baseURL string) (string, error) {
|
||||
// scheme relative links, eg <script src="//example.com/script.js">
|
||||
if len(link) > 1 && link[0:2] == "//" {
|
||||
@@ -156,3 +325,35 @@ func httpError(w http.ResponseWriter, msg string) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
_, _ = fmt.Fprint(w, msg)
|
||||
}
|
||||
|
||||
// SupportedProxyContentType checks if the content-type is supported for proxying.
|
||||
// This is limited to fonts, images and css only.
|
||||
func supportedProxyContentType(ct string) bool {
|
||||
ct = strings.ToLower(ct)
|
||||
|
||||
types := []string{
|
||||
"font/otf",
|
||||
"font/ttf",
|
||||
"font/woff",
|
||||
"font/woff2",
|
||||
"image/apng",
|
||||
"image/avif",
|
||||
"image/bmp",
|
||||
"image/gif",
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/png",
|
||||
"image/tiff",
|
||||
"image/svg+xml",
|
||||
"image/webp",
|
||||
"text/css",
|
||||
}
|
||||
|
||||
for _, t := range types {
|
||||
if strings.HasPrefix(ct, t) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -86,9 +86,6 @@ func Listen() {
|
||||
r.Path(config.Webroot + "search").Handler(middleWareFunc(index)).Methods("GET")
|
||||
r.Path(config.Webroot).Handler(middleWareFunc(index)).Methods("GET")
|
||||
|
||||
// put it all together
|
||||
http.Handle("/", r)
|
||||
|
||||
if auth.UICredentials != nil {
|
||||
logger.Log().Info("[http] enabling basic authentication")
|
||||
}
|
||||
@@ -100,6 +97,7 @@ func Listen() {
|
||||
Addr: config.HTTPListen,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
// add temporary self-signed certificates to get deleted afterwards
|
||||
|
||||
@@ -21,3 +21,4 @@ $enable-negative-margins: true;
|
||||
$body-color-dark: #e7eaed;
|
||||
$offcanvas-border-width: 0;
|
||||
$body-color: #080808;
|
||||
$btn-disabled-opacity: 0.4;
|
||||
|
||||
@@ -53,5 +53,10 @@ export default {
|
||||
navigator.setAppBadge(this.mailboxUnread);
|
||||
},
|
||||
},
|
||||
|
||||
render() {
|
||||
// to remove webkit warnings about missing template or render function
|
||||
return false;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -112,5 +112,10 @@ export default {
|
||||
this.favicon.href = canvas.toDataURL("image/png");
|
||||
},
|
||||
},
|
||||
|
||||
render() {
|
||||
// to remove webkit warnings about missing template or render function
|
||||
return false;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -14,6 +14,9 @@ export default {
|
||||
timezones,
|
||||
chaosConfig: false,
|
||||
chaosUpdated: false,
|
||||
defaultReleaseAddressesOptions: localStorage.getItem("defaultReleaseAddresses")
|
||||
? JSON.parse(localStorage.getItem("defaultReleaseAddresses"))
|
||||
: [], // set with default release addresses
|
||||
};
|
||||
},
|
||||
|
||||
@@ -45,11 +48,13 @@ export default {
|
||||
|
||||
mounted() {
|
||||
this.setTheme();
|
||||
this.$nextTick(() => {
|
||||
Tags.init("select.tz");
|
||||
});
|
||||
|
||||
mailbox.skipConfirmations = !!localStorage.getItem("skip-confirmations");
|
||||
mailbox.skipConfirmations = localStorage.getItem("skip-confirmations");
|
||||
|
||||
window.setTimeout(() => {
|
||||
Tags.init("select.tz");
|
||||
Tags.init("select.default-release-addresses");
|
||||
}, 500);
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -98,7 +103,7 @@ export default {
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul v-if="mailbox.uiConfig.ChaosEnabled" id="myTab" class="nav nav-tabs" role="tablist">
|
||||
<ul id="myTab" class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button
|
||||
id="ui-tab"
|
||||
@@ -113,7 +118,25 @@ export default {
|
||||
Web UI
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<li
|
||||
v-if="mailbox.uiConfig.MessageRelay && mailbox.uiConfig.MessageRelay.Enabled"
|
||||
class="nav-item"
|
||||
role="presentation"
|
||||
>
|
||||
<button
|
||||
id="relay-tab"
|
||||
class="nav-link"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#relay-tab-pane"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="relay-tab-pane"
|
||||
aria-selected="false"
|
||||
>
|
||||
Message release
|
||||
</button>
|
||||
</li>
|
||||
<li v-if="mailbox.uiConfig.ChaosEnabled" class="nav-item" role="presentation">
|
||||
<button
|
||||
id="chaos-tab"
|
||||
class="nav-link"
|
||||
@@ -234,6 +257,50 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Default relay addresses -->
|
||||
<div
|
||||
v-if="mailbox.uiConfig.MessageRelay && mailbox.uiConfig.MessageRelay.Enabled"
|
||||
id="relay-tab-pane"
|
||||
class="tab-pane fade"
|
||||
role="tabpanel"
|
||||
aria-labelledby="relay-tab"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="my-3 mb-5">
|
||||
<label class="form-label">Default release address(es)</label>
|
||||
<div class="form-text mb-2">
|
||||
You can designate the default "send to" addresses here, which will automatically
|
||||
populate the field in the message release dialog. This setting applies only to your
|
||||
browser. If this field is left empty, it will revert to the original recipients of
|
||||
the message.
|
||||
</div>
|
||||
<select
|
||||
v-model="mailbox.defaultReleaseAddresses"
|
||||
class="form-select tag-selector default-release-addresses"
|
||||
multiple
|
||||
data-allow-new="true"
|
||||
data-clear-end="true"
|
||||
data-allow-clear="true"
|
||||
data-placeholder="Enter email addresses..."
|
||||
data-add-on-blur="true"
|
||||
data-badge-style="primary"
|
||||
data-regex='^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$'
|
||||
data-separator="|,|"
|
||||
>
|
||||
<option value="">Enter email addresses...</option>
|
||||
<!-- you need at least one option with the placeholder -->
|
||||
<option
|
||||
v-for="t in defaultReleaseAddressesOptions"
|
||||
:key="'address+' + t"
|
||||
:value="t"
|
||||
>
|
||||
{{ t }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="invalid-feedback">Invalid email address</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="mailbox.uiConfig.ChaosEnabled"
|
||||
id="chaos-tab-pane"
|
||||
|
||||
@@ -85,8 +85,9 @@ export default {
|
||||
<template>
|
||||
<select
|
||||
v-model="pagination.limit"
|
||||
class="form-select form-select-sm d-inline w-auto me-2"
|
||||
class="form-select form-select-sm d-inline w-auto me-2 me-xl-3"
|
||||
:disabled="total == 0"
|
||||
title="The number of messages displayed per page"
|
||||
@change="changeLimit"
|
||||
>
|
||||
<option v-for="option in limitOptions" :key="option" :value="option">{{ option }}</option>
|
||||
@@ -98,11 +99,11 @@ export default {
|
||||
<small>of</small>
|
||||
{{ formatNumber(total) }}
|
||||
</template>
|
||||
<span v-else class="text-muted">0 of 0</span>
|
||||
<span v-else class="text-light">0 of 0</span>
|
||||
</small>
|
||||
|
||||
<button
|
||||
class="btn btn-outline-light ms-2 me-1"
|
||||
class="btn btn-outline-light ms-2 ms-xl-3 me-1"
|
||||
:disabled="!canPrev"
|
||||
:title="'View previous ' + pagination.limit + ' messages'"
|
||||
@click="viewPrev"
|
||||
|
||||
@@ -44,7 +44,20 @@ export default {
|
||||
// include only unique email addresses, regardless of casing
|
||||
this.allAddresses = JSON.parse(JSON.stringify([...new Map(a.map((ad) => [ad.toLowerCase(), ad])).values()]));
|
||||
|
||||
this.addresses = this.allAddresses;
|
||||
// include default release addresses from mailbox settings
|
||||
const defaultAddr = mailbox.defaultReleaseAddresses;
|
||||
for (const i in defaultAddr) {
|
||||
if (!this.allAddresses.includes(defaultAddr[i])) {
|
||||
this.allAddresses.push(defaultAddr[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultAddr.length === 0) {
|
||||
// prefill with all addresses if no default is set
|
||||
this.addresses = this.allAddresses;
|
||||
} else {
|
||||
this.addresses = defaultAddr;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -140,6 +153,13 @@ export default {
|
||||
<option v-for="t in allAddresses" :key="'address+' + t" :value="t">{{ t }}</option>
|
||||
</select>
|
||||
<div class="invalid-feedback">Invalid email address</div>
|
||||
<div class="form-text mt-1">
|
||||
Default release addresses can be configured in
|
||||
<a href="#" data-bs-toggle="modal" data-bs-target="#SettingsModal">
|
||||
<i class="bi bi-gear-fill ms-1"></i>
|
||||
Settings </a
|
||||
>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
|
||||
@@ -27,9 +27,13 @@ export default {
|
||||
methods: {
|
||||
initScreenshot() {
|
||||
this.loading = 1;
|
||||
const baseUrl = `${location.protocol}//${location.host}/`;
|
||||
// absolute proxy URL
|
||||
const proxy = new URL(this.resolve("/proxy"), baseUrl).href;
|
||||
const urlRegex = /(url\(('|")?(https?:\/\/[^)'"]+)('|")?\))/gim;
|
||||
|
||||
// remove base tag, if set
|
||||
let h = this.message.HTML.replace(/<base .*>/im, "");
|
||||
const proxy = this.resolve("/proxy");
|
||||
|
||||
// Outlook hacks - else screenshot returns blank image
|
||||
h = h.replace(/<html [^>]+>/gim, "<html>"); // remove html attributes
|
||||
@@ -37,19 +41,10 @@ export default {
|
||||
h = h.replace(/<o:/gm, "<"); // replace `<o:p>` tags with `<p>`
|
||||
h = h.replace(/<\/o:/gm, "</"); // replace `</o:p>` tags with `</p>`
|
||||
|
||||
// update any inline `url(...)` absolute links
|
||||
const urlRegex = /(url\(('|")?(https?:\/\/[^)'"]+)('|")?\))/gim;
|
||||
h = h.replaceAll(urlRegex, (match, p1, p2, p3) => {
|
||||
if (typeof p2 === "string") {
|
||||
return `url(${p2}${proxy}?url=` + encodeURIComponent(this.decodeEntities(p3)) + `${p2})`;
|
||||
}
|
||||
return `url(${proxy}?url=` + encodeURIComponent(this.decodeEntities(p3)) + `)`;
|
||||
});
|
||||
|
||||
// create temporary document to manipulate
|
||||
const doc = document.implementation.createHTMLDocument();
|
||||
doc.open();
|
||||
doc.write(h);
|
||||
doc.writeln(h);
|
||||
doc.close();
|
||||
|
||||
// remove any <script> tags
|
||||
@@ -58,17 +53,30 @@ export default {
|
||||
i.parentNode.removeChild(i);
|
||||
}
|
||||
|
||||
// replace any url(...) links in <style> blocks
|
||||
const styles = doc.getElementsByTagName("style");
|
||||
for (const i of styles) {
|
||||
i.innerHTML = i.innerHTML.replaceAll(urlRegex, (match, p1, p2, p3) => {
|
||||
if (typeof p2 === "string") {
|
||||
// quoted URL
|
||||
return (
|
||||
`url(${p2}${proxy}?data=` + btoa(this.message.ID + ":" + this.decodeEntities(p3)) + `${p2})`
|
||||
);
|
||||
}
|
||||
return `url(${proxy}?data=` + btoa(this.message.ID + ":" + this.decodeEntities(p3)) + `)`;
|
||||
});
|
||||
}
|
||||
|
||||
// replace stylesheet links with proxy links
|
||||
const stylesheets = doc.getElementsByTagName("link");
|
||||
for (const i of stylesheets) {
|
||||
const src = i.getAttribute("href");
|
||||
|
||||
if (
|
||||
src &&
|
||||
src.match(/^https?:\/\//i) &&
|
||||
src.indexOf(window.location.origin + window.location.pathname) !== 0
|
||||
) {
|
||||
i.setAttribute("href", `${proxy}?url=` + encodeURIComponent(this.decodeEntities(src)));
|
||||
i.setAttribute("href", `${proxy}?data=` + btoa(this.message.ID + ":" + this.decodeEntities(src)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +89,7 @@ export default {
|
||||
src.match(/^https?:\/\//i) &&
|
||||
src.indexOf(window.location.origin + window.location.pathname) !== 0
|
||||
) {
|
||||
i.setAttribute("src", `${proxy}?url=` + encodeURIComponent(this.decodeEntities(src)));
|
||||
i.setAttribute("src", `${proxy}?data=` + btoa(this.message.ID + ":" + this.decodeEntities(src)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +104,10 @@ export default {
|
||||
src.indexOf(window.location.origin + window.location.pathname) !== 0
|
||||
) {
|
||||
// replace with proxy link
|
||||
i.setAttribute("background", `${proxy}?url=` + encodeURIComponent(this.decodeEntities(src)));
|
||||
i.setAttribute(
|
||||
"background",
|
||||
`${proxy}?data=` + btoa(this.message.ID + ":" + this.decodeEntities(src)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ export const mailbox = reactive({
|
||||
appInfo: {}, // application information
|
||||
uiConfig: {}, // configuration for UI
|
||||
lastMessage: false, // return scrolling
|
||||
defaultReleaseAddresses: localStorage.getItem("defaultReleaseAddresses")
|
||||
? JSON.parse(localStorage.getItem("defaultReleaseAddresses"))
|
||||
: [], // default release addresses for released messages
|
||||
|
||||
// settings
|
||||
showTagColors: !localStorage.getItem("hideTagColors"),
|
||||
@@ -82,6 +85,17 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => mailbox.defaultReleaseAddresses,
|
||||
(v) => {
|
||||
if (v.length) {
|
||||
localStorage.setItem("defaultReleaseAddresses", JSON.stringify(v));
|
||||
} else {
|
||||
localStorage.removeItem("defaultReleaseAddresses");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => mailbox.timeZone,
|
||||
(v) => {
|
||||
|
||||
@@ -176,7 +176,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="navbar navbar-expand-lg navbar-dark row flex-shrink-0 bg-primary text-white d-print-none">
|
||||
<div class="navbar navbar-expand-lg row flex-shrink-0 bg-primary text-white d-print-none" data-bs-theme="dark">
|
||||
<div class="col-xl-2 col-md-3 col-auto pe-0">
|
||||
<RouterLink to="/" class="navbar-brand text-white me-0" @click="reloadMailbox">
|
||||
<img :src="resolve('/mailpit.svg')" alt="Mailpit" />
|
||||
|
||||
@@ -442,7 +442,11 @@ export default {
|
||||
if (pagination.limit !== pagination.defaultLimit) {
|
||||
p.limit = pagination.limit.toString();
|
||||
}
|
||||
this.$router.push("/?" + new URLSearchParams(p).toString());
|
||||
if (p.start || p.limit) {
|
||||
this.$router.push("/?" + new URLSearchParams(p).toString());
|
||||
} else {
|
||||
this.$router.push("/");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -455,7 +459,6 @@ export default {
|
||||
window.setTimeout(() => {
|
||||
// delay to allow elements to load / focus
|
||||
this.$refs.ReleaseRef.initTags();
|
||||
document.querySelector('#ReleaseModal input[role="combobox"]').focus();
|
||||
}, 500);
|
||||
},
|
||||
},
|
||||
@@ -463,7 +466,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="navbar navbar-expand-lg navbar-dark row flex-shrink-0 bg-primary text-white d-print-none">
|
||||
<div class="navbar navbar-expand-lg row flex-shrink-0 bg-primary text-white d-print-none" data-bs-theme="dark">
|
||||
<div class="d-none d-xl-block col-xl-3 col-auto pe-0">
|
||||
<RouterLink to="/" class="navbar-brand text-white me-0" @click="pagination.start = 0">
|
||||
<img :src="resolve('/mailpit.svg')" alt="Mailpit" />
|
||||
@@ -485,11 +488,11 @@ export default {
|
||||
title="Release message"
|
||||
@click="initReleaseModal()"
|
||||
>
|
||||
<i class="bi bi-send"></i>
|
||||
<i class="bi bi-send me-md-2"></i>
|
||||
<span class="d-none d-md-inline">Release</span>
|
||||
</button>
|
||||
<button class="btn btn-outline-light me-1 me-sm-2" title="Delete message" @click="deleteMessage()">
|
||||
<i class="bi bi-trash-fill"></i>
|
||||
<i class="bi bi-trash-fill me-md-2"></i>
|
||||
<span class="d-none d-md-inline">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -124,7 +124,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="navbar navbar-expand-lg navbar-dark row flex-shrink-0 bg-primary text-white d-print-none">
|
||||
<div class="navbar navbar-expand-lg row flex-shrink-0 bg-primary text-white d-print-none" data-bs-theme="dark">
|
||||
<div class="col-xl-2 col-md-3 col-auto pe-0">
|
||||
<RouterLink to="/" class="navbar-brand text-white me-0" @click="pagination.start = 0">
|
||||
<img :src="resolve('/mailpit.svg')" alt="Mailpit" />
|
||||
|
||||
@@ -34,8 +34,7 @@ var (
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool { return true }, // allow multi-domain
|
||||
EnableCompression: true, // experimental compression
|
||||
EnableCompression: true,
|
||||
}
|
||||
|
||||
// Client is a middleman between the websocket connection and the hub.
|
||||
|
||||
Reference in New Issue
Block a user