mirror of
https://github.com/axllent/mailpit.git
synced 2026-03-06 23:47:01 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26c6f9d965 | ||
|
|
76a261bf06 | ||
|
|
86a3bea300 | ||
|
|
5fa6b20a53 | ||
|
|
3ad62769a6 | ||
|
|
a63952aee6 | ||
|
|
de95910539 | ||
|
|
60a41ce3ca | ||
|
|
898b36ce0b | ||
|
|
b4a4d44492 | ||
|
|
64e4e4240a | ||
|
|
0477c6573f | ||
|
|
28ac6d2099 | ||
|
|
43a1dbe3f0 | ||
|
|
aa3f860540 | ||
|
|
f54a2187ac | ||
|
|
063eab2c6a | ||
|
|
b282e6663b | ||
|
|
df777c6e90 |
30
CHANGELOG.md
30
CHANGELOG.md
@@ -2,6 +2,36 @@
|
||||
|
||||
Notable changes to Mailpit will be documented in this file.
|
||||
|
||||
## [v1.9.2]
|
||||
|
||||
### Fix
|
||||
- Delete all messages matching search when more than 1000 results
|
||||
|
||||
### Libs
|
||||
- Update node modules
|
||||
|
||||
### Tests
|
||||
- Add message tag tests
|
||||
- Add search delete tests
|
||||
|
||||
### UI
|
||||
- Reset pagination when returning to inbox from search
|
||||
|
||||
|
||||
## [v1.9.1]
|
||||
|
||||
### Chore
|
||||
- Update caniemail data
|
||||
|
||||
### Libs
|
||||
- Update Go modules
|
||||
|
||||
### UI
|
||||
- Set 404 page when loading a non-existent message
|
||||
- Link email addresses in message summary to search
|
||||
- Better support for mobile screen sizes
|
||||
|
||||
|
||||
## [v1.9.0]
|
||||
|
||||
### API
|
||||
|
||||
@@ -16,7 +16,6 @@ Returns a JSON summary of the message and attachments.
|
||||
{
|
||||
"ID": "d7a5543b-96dd-478b-9b60-2b465c9884de",
|
||||
"MessageID": "12345.67890@localhost",
|
||||
"Read": true,
|
||||
"From": {
|
||||
"Name": "John Doe",
|
||||
"Address": "john@example.com"
|
||||
@@ -58,7 +57,6 @@ Returns a JSON summary of the message and attachments.
|
||||
```
|
||||
### Notes
|
||||
|
||||
- `Read` - always true (message marked read on open)
|
||||
- `From` - Name & Address, or null
|
||||
- `To`, `CC`, `BCC`, `ReplyTo` - Array of Names & Address
|
||||
- `Date` - Parsed email local date & time from headers
|
||||
|
||||
2
go.mod
2
go.mod
@@ -7,7 +7,7 @@ require (
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/axllent/semver v0.0.1
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/gomarkdown/markdown v0.0.0-20230916125811-7478c230c7cd
|
||||
github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/jhillyerd/enmime v1.0.1
|
||||
|
||||
4
go.sum
4
go.sum
@@ -51,8 +51,8 @@ github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomarkdown/markdown v0.0.0-20230916125811-7478c230c7cd h1:laCEzrtkKEkT2424vMTGl6N1m0xN8kq371hksD5Be+8=
|
||||
github.com/gomarkdown/markdown v0.0.0-20230916125811-7478c230c7cd/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 h1:EcQR3gusLHN46TAD+G+EbaaqJArt5vHhNpXAa12PQf4=
|
||||
github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
|
||||
18
package-lock.json
generated
18
package-lock.json
generated
@@ -2152,9 +2152,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/short-unique-id": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/short-unique-id/-/short-unique-id-5.0.2.tgz",
|
||||
"integrity": "sha512-4wZq1VLV4hsEx8guP5bN7XnY8UDsVXtdUDWFMP1gvEieAXolq5fWGKpuua21PRXaLn3OybTKFQNm7JGcHSWu/Q==",
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/short-unique-id/-/short-unique-id-5.0.3.tgz",
|
||||
"integrity": "sha512-yhniEILouC0s4lpH0h7rJsfylZdca10W9mDJRAFh3EpcSUanCHGb0R7kcFOIUCZYSAPo0PUD5ZxWQdW0T4xaug==",
|
||||
"bin": {
|
||||
"short-unique-id": "bin/short-unique-id",
|
||||
"suid": "bin/short-unique-id"
|
||||
@@ -2401,9 +2401,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "5.25.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.25.1.tgz",
|
||||
"integrity": "sha512-nTw6b2G2OqP6btYPyghCgV4hSwjJlL/78FMJatVLCa3otj6PCOQSt6dVtYt82OtNqFz8XsnJ+vsXLADPXjPhqw==",
|
||||
"version": "5.25.2",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.25.2.tgz",
|
||||
"integrity": "sha512-tch8RbCfn1UUH1PeVCXva4V8gDpGAud/w0WubD6sHC46vYQ3KDxL+xv1A2UxK0N6jrVedutuPHxe1XIoqerwMw==",
|
||||
"dependencies": {
|
||||
"busboy": "^1.6.0"
|
||||
},
|
||||
@@ -2443,9 +2443,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.4.tgz",
|
||||
"integrity": "sha512-9PISkmaCO02OzPVOMq2w82ilty6+xJmQrarYZDkjZBfl4RvYAlt4PKnEX21oW4KTtWfa9OuO/b3qk1Od3AEdCQ==",
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.5.tgz",
|
||||
"integrity": "sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
|
||||
@@ -228,6 +228,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.messageHeaders {
|
||||
th {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.list-group-item.message:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export default {
|
||||
<template>
|
||||
<template v-if="!modals">
|
||||
<div class="list-group my-2">
|
||||
<RouterLink to="/" class="list-group-item list-group-item-action">
|
||||
<RouterLink to="/" class="list-group-item list-group-item-action" @click="pagination.start = 0">
|
||||
<i class="bi bi-arrow-return-left me-1"></i>
|
||||
<span class="ms-1">Inbox</span>
|
||||
<span class="badge rounded-pill ms-1 float-end text-bg-secondary" title="Unread messages"
|
||||
@@ -71,18 +71,6 @@ export default {
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<!-- <button class="list-group-item list-group-item-action" data-bs-toggle="modal" data-bs-target="#MarkAllReadModal"
|
||||
:disabled="!mailbox.unread">
|
||||
<i class="bi bi-eye-fill me-1"></i>
|
||||
Mark all read
|
||||
</button>
|
||||
|
||||
<button class="list-group-item list-group-item-action" data-bs-toggle="modal" data-bs-target="#DeleteAllModal"
|
||||
:disabled="!mailbox.total">
|
||||
<i class="bi bi-trash-fill me-1 text-danger"></i>
|
||||
Delete all
|
||||
</button> -->
|
||||
|
||||
<NavSelected @loadMessages="loadMessages" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -232,7 +232,9 @@ export default {
|
||||
<span v-if="message.From">
|
||||
<span v-if="message.From.Name">{{ message.From.Name + " " }}</span>
|
||||
<span v-if="message.From.Address" class="small">
|
||||
<{{ message.From.Address }}>
|
||||
<<a :href="searchURI(message.From.Address)" class="text-body">
|
||||
{{ message.From.Address }}
|
||||
</a>>
|
||||
</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
@@ -245,7 +247,12 @@ export default {
|
||||
<td class="privacy">
|
||||
<span v-if="message.To && message.To.length" v-for="(t, i) in message.To">
|
||||
<template v-if="i > 0">, </template>
|
||||
<span class="text-nowrap">{{ t.Name + " <" + t.Address + ">" }}</span>
|
||||
<span>
|
||||
{{ t.Name }}
|
||||
<<a :href="searchURI(t.Address)" class="text-body">
|
||||
{{ t.Address }}
|
||||
</a>>
|
||||
</span>
|
||||
</span>
|
||||
<span v-else class="text-body-secondary">[Undisclosed recipients]</span>
|
||||
</td>
|
||||
@@ -255,7 +262,11 @@ export default {
|
||||
<td class="privacy">
|
||||
<span v-for="(t, i) in message.Cc">
|
||||
<template v-if="i > 0">,</template>
|
||||
{{ t.Name + " <" + t.Address + ">" }} </span>
|
||||
{{ t.Name }}
|
||||
<<a :href="searchURI(t.Address)" class="text-body">
|
||||
{{ t.Address }}
|
||||
</a>>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="message.Bcc && message.Bcc.length" class="small">
|
||||
@@ -263,21 +274,32 @@ export default {
|
||||
<td class="privacy">
|
||||
<span v-for="(t, i) in message.Bcc">
|
||||
<template v-if="i > 0">,</template>
|
||||
{{ t.Name + " <" + t.Address + ">" }}
|
||||
{{ t.Name }}
|
||||
<<a :href="searchURI(t.Address)" class="text-body">
|
||||
{{ t.Address }}
|
||||
</a>>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="message.ReplyTo && message.ReplyTo.length" class="small">
|
||||
<th class="text-nowrap">Reply-To</th>
|
||||
<td class="privacy text-body-secondary">
|
||||
<td class="privacy text-body-secondary text-break">
|
||||
<span v-for="(t, i) in message.ReplyTo">
|
||||
<template v-if="i > 0">,</template>
|
||||
{{ t.Name + " <" + t.Address + ">" }} </span>
|
||||
{{ t.Name }}
|
||||
<<a :href="searchURI(t.Address)" class="text-body-secondary">
|
||||
{{ t.Address }}
|
||||
</a>>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="message.ReturnPath && message.ReturnPath != message.From.Address" class="small">
|
||||
<th class="text-nowrap">Return-Path</th>
|
||||
<td class="privacy text-body-secondary"><{{ message.ReturnPath }}></td>
|
||||
<td class="privacy text-body-secondary text-break">
|
||||
<<a :href="searchURI(message.ReturnPath)" class="text-body-secondary">
|
||||
{{ message.ReturnPath }}
|
||||
</a>>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="small">Subject</th>
|
||||
|
||||
@@ -28,6 +28,10 @@ export default {
|
||||
return this.$router.resolve(u).href
|
||||
},
|
||||
|
||||
searchURI: function (s) {
|
||||
return this.resolve('/search') + '?q=' + encodeURIComponent(s)
|
||||
},
|
||||
|
||||
getFileSize: function (bytes) {
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(1024))
|
||||
return (bytes / Math.pow(1024, i)).toFixed(1) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]
|
||||
@@ -63,28 +67,6 @@ export default {
|
||||
return q
|
||||
},
|
||||
|
||||
// Ajax error message
|
||||
handleError: function (error) {
|
||||
// handle error
|
||||
if (error.response && error.response.data) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
if (error.response.data.Error) {
|
||||
alert(error.response.data.Error)
|
||||
} else {
|
||||
alert(error.response.data)
|
||||
}
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
// http.ClientRequest in node.js
|
||||
alert('Error sending data to the server. Please try again.')
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
alert(error.message)
|
||||
}
|
||||
},
|
||||
|
||||
// generic modal get/set function
|
||||
modal: function (id) {
|
||||
let e = document.getElementById(id)
|
||||
@@ -109,13 +91,20 @@ export default {
|
||||
* @params string url
|
||||
* @params array array parameters Object/array
|
||||
* @params function callback function
|
||||
* @params function error callback function
|
||||
*/
|
||||
get: function (url, values, callback) {
|
||||
get: function (url, values, callback, errorCallback) {
|
||||
let self = this
|
||||
self.loading++
|
||||
axios.get(url, { params: values })
|
||||
.then(callback)
|
||||
.catch(self.handleError)
|
||||
.catch(function (err) {
|
||||
if (typeof errorCallback == 'function') {
|
||||
return errorCallback(err)
|
||||
}
|
||||
|
||||
self.handleError(err)
|
||||
})
|
||||
.then(function () {
|
||||
// always executed
|
||||
if (self.loading > 0) {
|
||||
@@ -187,6 +176,26 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
// Ajax error message
|
||||
handleError: function (error) {
|
||||
// handle error
|
||||
if (error.response && error.response.data) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
if (error.response.data.Error) {
|
||||
alert(error.response.data.Error)
|
||||
} else {
|
||||
alert(error.response.data)
|
||||
}
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
alert('Error sending data to the server. Please try again.')
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
alert(error.message)
|
||||
}
|
||||
},
|
||||
|
||||
allAttachments: function (message) {
|
||||
let a = []
|
||||
for (let i in message.Attachments) {
|
||||
|
||||
@@ -26,6 +26,7 @@ export default {
|
||||
message: false,
|
||||
prevLink: false,
|
||||
nextLink: false,
|
||||
errorMessage: false,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -45,6 +46,8 @@ export default {
|
||||
this.message = false
|
||||
let uri = self.resolve('/api/v1/message/' + this.$route.params.id)
|
||||
self.get(uri, false, function (response) {
|
||||
self.errorMessage = false
|
||||
|
||||
let d = response.data
|
||||
|
||||
if (self.wasUnread(d.ID)) {
|
||||
@@ -94,7 +97,23 @@ export default {
|
||||
self.message = d
|
||||
|
||||
self.detectPrevNext()
|
||||
})
|
||||
},
|
||||
function (error) {
|
||||
self.errorMessage = true
|
||||
if (error.response && error.response.data) {
|
||||
if (error.response.data.Error) {
|
||||
self.errorMessage = error.response.data.Error
|
||||
} else {
|
||||
self.errorMessage = error.response.data
|
||||
}
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
self.errorMessage = 'Error sending data to the server. Please refresh the page.'
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
self.errorMessage = error.message
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// try detect whether this message was unread based on messages listing
|
||||
@@ -194,23 +213,22 @@ export default {
|
||||
<span class="ms-2 d-none d-sm-inline">Mailpit</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="col col-md-4k col-lg-5 col-xl-6">
|
||||
<button @click="goBack()" class="btn btn-outline-light me-4 d-md-none" title="Return to messages">
|
||||
<div class="col col-md-4k col-lg-5 col-xl-6" v-if="!errorMessage">
|
||||
<button @click="goBack()" class="btn btn-outline-light me-3 me-sm-4 d-md-none" title="Return to messages">
|
||||
<i class="bi bi-arrow-return-left"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-light me-2" title="Mark unread" v-on:click="markUnread">
|
||||
<button class="btn btn-outline-light me-1 me-sm-2" title="Mark unread" v-on:click="markUnread">
|
||||
<i class="bi bi-eye-slash"></i> <span class="d-none d-md-inline">Mark unread</span>
|
||||
</button>
|
||||
<button class="btn btn-outline-light me-2" title="Release message"
|
||||
<button class="btn btn-outline-light me-1 me-sm-2" title="Release message"
|
||||
v-if="mailbox.uiConfig.MessageRelay && mailbox.uiConfig.MessageRelay.Enabled" v-on:click="initReleaseModal">
|
||||
<i class="bi bi-send"></i> <span class="d-none d-md-inline">Release</span>
|
||||
</button>
|
||||
<button class="btn btn-outline-light me-2" title="Delete message" v-on:click="deleteMessage">
|
||||
<button class="btn btn-outline-light me-1 me-sm-2" title="Delete message" v-on:click="deleteMessage">
|
||||
<i class="bi bi-trash-fill"></i> <span class="d-none d-md-inline">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-auto col-lg-4 col-xl-4 text-end">
|
||||
|
||||
<div class="col-auto col-lg-4 col-xl-4 text-end" v-if="!errorMessage">
|
||||
<div class="dropdown d-inline-block" id="DownloadBtn">
|
||||
<button type="button" class="btn btn-outline-light dropdown-toggle" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
@@ -267,7 +285,7 @@ export default {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<RouterLink :to="'/view/' + prevLink" class="btn btn-outline-light ms-2 me-1"
|
||||
<RouterLink :to="'/view/' + prevLink" class="btn btn-outline-light ms-1 ms-sm-2 me-1"
|
||||
:class="prevLink ? '' : 'disabled'" title="View previous message">
|
||||
<i class="bi bi-caret-left-fill"></i>
|
||||
</RouterLink>
|
||||
@@ -286,13 +304,13 @@ export default {
|
||||
<i class="bi bi-arrow-return-left me-1"></i>
|
||||
<span class="ms-1">Return</span>
|
||||
<span class="badge rounded-pill ms-1 float-end text-bg-secondary" title="Unread messages"
|
||||
v-if="mailbox.unread">
|
||||
v-if="mailbox.unread && !errorMessage">
|
||||
{{ formatNumber(mailbox.unread) }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card mt-4">
|
||||
<div class="card mt-4" v-if="!errorMessage">
|
||||
<div class="card-body text-body-secondary small">
|
||||
<p class="card-text">
|
||||
<b>Message date:</b><br>
|
||||
@@ -311,7 +329,12 @@ export default {
|
||||
|
||||
<div class="col-xl-10 col-md-9 mh-100 ps-0 ps-md-2 pe-0">
|
||||
<div class="mh-100" style="overflow-y: auto;" id="message-page">
|
||||
<Message v-if="message" :key="message.ID" :message="message" />
|
||||
<template v-if="errorMessage">
|
||||
<h3 class="text-center my-3">
|
||||
{{ errorMessage }}
|
||||
</h3>
|
||||
</template>
|
||||
<Message v-else-if="message" :key="message.ID" :message="message" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -203,7 +203,8 @@ func Close() {
|
||||
}
|
||||
}
|
||||
|
||||
// Store will save an email to the database tables
|
||||
// Store will save an email to the database tables.
|
||||
// Returns the database ID of the saved message.
|
||||
func Store(body []byte) (string, error) {
|
||||
// Parse message body with enmime
|
||||
env, err := enmime.ReadEnvelope(bytes.NewReader(body))
|
||||
|
||||
@@ -1,22 +1,8 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/utils/logger"
|
||||
"github.com/jhillyerd/enmime"
|
||||
)
|
||||
|
||||
var (
|
||||
testTextEmail []byte
|
||||
testMimeEmail []byte
|
||||
testRuns = 100
|
||||
)
|
||||
|
||||
func TestTextEmailInserts(t *testing.T) {
|
||||
@@ -63,8 +49,6 @@ func TestMimeEmailInserts(t *testing.T) {
|
||||
|
||||
start := time.Now()
|
||||
|
||||
assertEqualStats(t, 0, 0)
|
||||
|
||||
for i := 0; i < testRuns; i++ {
|
||||
if _, err := Store(testMimeEmail); err != nil {
|
||||
t.Log("error ", err)
|
||||
@@ -87,8 +71,6 @@ func TestMimeEmailInserts(t *testing.T) {
|
||||
assertEqual(t, CountTotal(), 0, "incorrect number of mime emails deleted")
|
||||
|
||||
t.Logf("Deleted %d mime emails in %s", testRuns, time.Since(delStart))
|
||||
|
||||
assertEqualStats(t, 0, 0)
|
||||
}
|
||||
|
||||
func TestRetrieveMimeEmail(t *testing.T) {
|
||||
@@ -110,11 +92,11 @@ func TestRetrieveMimeEmail(t *testing.T) {
|
||||
}
|
||||
|
||||
assertEqual(t, msg.From.Name, "Sender Smith", "\"From\" name does not match")
|
||||
assertEqual(t, msg.From.Address, "sender@example.com", "\"From\" address does not match")
|
||||
assertEqual(t, msg.From.Address, "sender2@example.com", "\"From\" address does not match")
|
||||
assertEqual(t, msg.Subject, "inline + attachment", "subject does not match")
|
||||
assertEqual(t, len(msg.To), 1, "incorrect number of recipients")
|
||||
assertEqual(t, msg.To[0].Name, "Recipient Ross", "\"To\" name does not match")
|
||||
assertEqual(t, msg.To[0].Address, "recipient@example.com", "\"To\" address does not match")
|
||||
assertEqual(t, msg.To[0].Address, "recipient2@example.com", "\"To\" address does not match")
|
||||
assertEqual(t, len(msg.Attachments), 1, "incorrect number of attachments")
|
||||
assertEqual(t, msg.Attachments[0].FileName, "Sample PDF.pdf", "attachment filename does not match")
|
||||
assertEqual(t, len(msg.Inline), 1, "incorrect number of inline attachments")
|
||||
@@ -135,76 +117,6 @@ func TestRetrieveMimeEmail(t *testing.T) {
|
||||
assertEqual(t, len(inlineData.Content), msg.Inline[0].Size, "inline attachment size does not match")
|
||||
}
|
||||
|
||||
func TestSearch(t *testing.T) {
|
||||
setup()
|
||||
defer Close()
|
||||
|
||||
t.Log("Testing search")
|
||||
for i := 0; i < testRuns; i++ {
|
||||
msg := enmime.Builder().
|
||||
From(fmt.Sprintf("From %d", i), fmt.Sprintf("from-%d@example.com", i)).
|
||||
Subject(fmt.Sprintf("Subject line %d end", i)).
|
||||
Text([]byte(fmt.Sprintf("This is the email body %d <jdsauk;dwqmdqw;>.", i))).
|
||||
To(fmt.Sprintf("To %d", i), fmt.Sprintf("to-%d@example.com", i))
|
||||
|
||||
env, err := msg.Build()
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
if err := env.Encode(buf); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if _, err := Store(buf.Bytes()); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
for i := 1; i < 51; i++ {
|
||||
// search a random something that will return a single result
|
||||
searchIdx := rand.Intn(4) + 1
|
||||
var search string
|
||||
switch searchIdx {
|
||||
case 1:
|
||||
search = fmt.Sprintf("from-%d@example.com", i)
|
||||
case 2:
|
||||
search = fmt.Sprintf("to-%d@example.com", i)
|
||||
case 3:
|
||||
search = fmt.Sprintf("\"Subject line %d end\"", i)
|
||||
default:
|
||||
search = fmt.Sprintf("\"the email body %d jdsauk dwqmdqw\"", i)
|
||||
}
|
||||
|
||||
summaries, _, err := Search(search, 0, 100)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
assertEqual(t, len(summaries), 1, "1 search result expected")
|
||||
|
||||
assertEqual(t, summaries[0].From.Name, fmt.Sprintf("From %d", i), "\"From\" name does not match")
|
||||
assertEqual(t, summaries[0].From.Address, fmt.Sprintf("from-%d@example.com", i), "\"From\" address does not match")
|
||||
assertEqual(t, summaries[0].To[0].Name, fmt.Sprintf("To %d", i), "\"To\" name does not match")
|
||||
assertEqual(t, summaries[0].To[0].Address, fmt.Sprintf("to-%d@example.com", i), "\"To\" address does not match")
|
||||
assertEqual(t, summaries[0].Subject, fmt.Sprintf("Subject line %d end", i), "\"Subject\" does not match")
|
||||
}
|
||||
|
||||
// search something that will return 200 results
|
||||
summaries, _, err := Search("This is the email body", 0, testRuns)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
assertEqual(t, len(summaries), testRuns, "search results expected")
|
||||
}
|
||||
|
||||
func BenchmarkImportText(b *testing.B) {
|
||||
setup()
|
||||
defer Close()
|
||||
@@ -229,44 +141,3 @@ func BenchmarkImportMime(b *testing.B) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func setup() {
|
||||
logger.NoLogging = true
|
||||
config.MaxMessages = 0
|
||||
config.DataFile = ""
|
||||
|
||||
if err := InitDB(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
testTextEmail, err = ioutil.ReadFile("testdata/plain-text.eml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
testMimeEmail, err = ioutil.ReadFile("testdata/mime-attachment.eml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqual(t *testing.T, a interface{}, b interface{}, message string) {
|
||||
if a == b {
|
||||
return
|
||||
}
|
||||
message = fmt.Sprintf("%s: \"%v\" != \"%v\"", message, a, b)
|
||||
t.Fatal(message)
|
||||
}
|
||||
|
||||
func assertEqualStats(t *testing.T, total int, unread int) {
|
||||
s := StatsGet()
|
||||
if total != s.Total {
|
||||
t.Fatalf("Incorrect total mailbox stats: \"%d\" != \"%d\"", total, s.Total)
|
||||
}
|
||||
|
||||
if unread != s.Unread {
|
||||
t.Fatalf("Incorrect unread mailbox stats: \"%d\" != \"%d\"", unread, s.Unread)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ func DeleteSearch(search string) error {
|
||||
if len(ids) > 0 {
|
||||
total := len(ids)
|
||||
|
||||
// split ids into chunks
|
||||
// split ids into chunks of 1000 ids
|
||||
var chunks [][]string
|
||||
if total > 1000 {
|
||||
chunkSize := 1000
|
||||
@@ -132,6 +132,10 @@ func DeleteSearch(search string) error {
|
||||
for chunkSize < len(ids) {
|
||||
ids, chunks = ids[chunkSize:], append(chunks, ids[0:chunkSize:chunkSize])
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
// add remaining ids <= 1000
|
||||
chunks = append(chunks, ids)
|
||||
}
|
||||
} else {
|
||||
chunks = append(chunks, ids)
|
||||
}
|
||||
|
||||
152
storage/search_test.go
Normal file
152
storage/search_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/jhillyerd/enmime"
|
||||
)
|
||||
|
||||
func TestSearch(t *testing.T) {
|
||||
setup()
|
||||
defer Close()
|
||||
|
||||
t.Log("Testing search")
|
||||
for i := 0; i < testRuns; i++ {
|
||||
msg := enmime.Builder().
|
||||
From(fmt.Sprintf("From %d", i), fmt.Sprintf("from-%d@example.com", i)).
|
||||
Subject(fmt.Sprintf("Subject line %d end", i)).
|
||||
Text([]byte(fmt.Sprintf("This is the email body %d <jdsauk;dwqmdqw;>.", i))).
|
||||
To(fmt.Sprintf("To %d", i), fmt.Sprintf("to-%d@example.com", i))
|
||||
|
||||
env, err := msg.Build()
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
if err := env.Encode(buf); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if _, err := Store(buf.Bytes()); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
for i := 1; i < 51; i++ {
|
||||
// search a random something that will return a single result
|
||||
searchIdx := rand.Intn(4) + 1
|
||||
var search string
|
||||
switch searchIdx {
|
||||
case 1:
|
||||
search = fmt.Sprintf("from-%d@example.com", i)
|
||||
case 2:
|
||||
search = fmt.Sprintf("to-%d@example.com", i)
|
||||
case 3:
|
||||
search = fmt.Sprintf("\"Subject line %d end\"", i)
|
||||
default:
|
||||
search = fmt.Sprintf("\"the email body %d jdsauk dwqmdqw\"", i)
|
||||
}
|
||||
|
||||
summaries, _, err := Search(search, 0, 100)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
assertEqual(t, len(summaries), 1, "1 search result expected")
|
||||
|
||||
assertEqual(t, summaries[0].From.Name, fmt.Sprintf("From %d", i), "\"From\" name does not match")
|
||||
assertEqual(t, summaries[0].From.Address, fmt.Sprintf("from-%d@example.com", i), "\"From\" address does not match")
|
||||
assertEqual(t, summaries[0].To[0].Name, fmt.Sprintf("To %d", i), "\"To\" name does not match")
|
||||
assertEqual(t, summaries[0].To[0].Address, fmt.Sprintf("to-%d@example.com", i), "\"To\" address does not match")
|
||||
assertEqual(t, summaries[0].Subject, fmt.Sprintf("Subject line %d end", i), "\"Subject\" does not match")
|
||||
}
|
||||
|
||||
// search something that will return 200 results
|
||||
summaries, _, err := Search("This is the email body", 0, testRuns)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
assertEqual(t, len(summaries), testRuns, "search results expected")
|
||||
}
|
||||
|
||||
func TestSearchDelete100(t *testing.T) {
|
||||
setup()
|
||||
defer Close()
|
||||
|
||||
t.Log("Testing search delete of 100 messages")
|
||||
for i := 0; i < 100; i++ {
|
||||
if _, err := Store(testTextEmail); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
if _, err := Store(testMimeEmail); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
_, total, err := Search("from:sender@example.com", 0, 100)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
assertEqual(t, total, 100, "100 search results expected")
|
||||
|
||||
if err := DeleteSearch("from:sender@example.com"); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
_, total, err = Search("from:sender@example.com", 0, 100)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
assertEqual(t, total, 0, "0 search results expected")
|
||||
}
|
||||
|
||||
func TestSearchDelete1100(t *testing.T) {
|
||||
setup()
|
||||
defer Close()
|
||||
|
||||
t.Log("Testing search delete of 1100 messages")
|
||||
for i := 0; i < 1100; i++ {
|
||||
if _, err := Store(testTextEmail); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
_, total, err := Search("from:sender@example.com", 0, 100)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
assertEqual(t, total, 1100, "100 search results expected")
|
||||
|
||||
if err := DeleteSearch("from:sender@example.com"); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
_, total, err = Search("from:sender@example.com", 0, 100)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
assertEqual(t, total, 0, "0 search results expected")
|
||||
}
|
||||
43
storage/tags_test.go
Normal file
43
storage/tags_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTags(t *testing.T) {
|
||||
setup()
|
||||
defer Close()
|
||||
|
||||
t.Log("Testing tags")
|
||||
|
||||
ids := []string{}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
id, err := Store(testMimeEmail)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
if err := SetTags(ids[i], []string{fmt.Sprintf("Tag-%d", i)}); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
message, err := GetMessage(ids[i])
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if len(message.Tags) != 1 || message.Tags[0] != fmt.Sprintf("Tag-%d", i) {
|
||||
t.Fatal("Message tags do not match")
|
||||
}
|
||||
}
|
||||
}
|
||||
57
storage/test_shared.go
Normal file
57
storage/test_shared.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/utils/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
testTextEmail []byte
|
||||
testMimeEmail []byte
|
||||
testRuns = 100
|
||||
)
|
||||
|
||||
func setup() {
|
||||
logger.NoLogging = true
|
||||
config.MaxMessages = 0
|
||||
config.DataFile = ""
|
||||
|
||||
if err := InitDB(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
testTextEmail, err = os.ReadFile("testdata/plain-text.eml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
testMimeEmail, err = os.ReadFile("testdata/mime-attachment.eml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqual(t *testing.T, a interface{}, b interface{}, message string) {
|
||||
if a == b {
|
||||
return
|
||||
}
|
||||
message = fmt.Sprintf("%s: \"%v\" != \"%v\"", message, a, b)
|
||||
t.Fatal(message)
|
||||
}
|
||||
|
||||
func assertEqualStats(t *testing.T, total int, unread int) {
|
||||
s := StatsGet()
|
||||
if total != s.Total {
|
||||
t.Fatalf("Incorrect total mailbox stats: \"%d\" != \"%d\"", total, s.Total)
|
||||
}
|
||||
|
||||
if unread != s.Unread {
|
||||
t.Fatalf("Incorrect unread mailbox stats: \"%d\" != \"%d\"", unread, s.Unread)
|
||||
}
|
||||
}
|
||||
20
storage/testdata/mime-attachment.eml
vendored
20
storage/testdata/mime-attachment.eml
vendored
@@ -1,4 +1,4 @@
|
||||
Delivered-To: recipient@example.com
|
||||
Delivered-To: recipient2@example.com
|
||||
Received: by 2002:a0c:fe87:0:0:0:0:0 with SMTP id d7csp145570qvs;
|
||||
Tue, 26 Jul 2022 20:42:36 -0700 (PDT)
|
||||
X-Received: by 2002:a17:902:f788:b0:16c:f48b:905e with SMTP id q8-20020a170902f78800b0016cf48b905emr19885972pln.60.1658893355881;
|
||||
@@ -23,18 +23,18 @@ ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc
|
||||
uSfA==
|
||||
ARC-Authentication-Results: i=1; mx.google.com;
|
||||
dkim=pass header.i=@gmail.com header.s=20210112 header.b=mywi6bMa;
|
||||
spf=pass (google.com: domain of sender@example.com designates 209.85.220.41 as permitted sender) smtp.mailfrom=sender@example.com;
|
||||
spf=pass (google.com: domain of sender2@example.com designates 209.85.220.41 as permitted sender) smtp.mailfrom=sender2@example.com;
|
||||
dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com
|
||||
Return-Path: <sender@example.com>
|
||||
Return-Path: <sender2@example.com>
|
||||
Received: from mail-sor-f41.google.com (mail-sor-f41.google.com. [209.85.220.41])
|
||||
by mx.google.com with SMTPS id 11-20020aa7914b000000b0052ab192de4fsor8543241pfi.101.2022.07.26.20.42.35
|
||||
for <recipient@example.com>
|
||||
for <recipient2@example.com>
|
||||
(Google Transport Security);
|
||||
Tue, 26 Jul 2022 20:42:35 -0700 (PDT)
|
||||
Received-SPF: pass (google.com: domain of sender@example.com designates 209.85.220.41 as permitted sender) client-ip=209.85.220.41;
|
||||
Received-SPF: pass (google.com: domain of sender2@example.com designates 209.85.220.41 as permitted sender) client-ip=209.85.220.41;
|
||||
Authentication-Results: mx.google.com;
|
||||
dkim=pass header.i=@gmail.com header.s=20210112 header.b=mywi6bMa;
|
||||
spf=pass (google.com: domain of sender@example.com designates 209.85.220.41 as permitted sender) smtp.mailfrom=sender@example.com;
|
||||
spf=pass (google.com: domain of sender2@example.com designates 209.85.220.41 as permitted sender) smtp.mailfrom=sender2@example.com;
|
||||
dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||
d=gmail.com; s=20210112;
|
||||
@@ -63,10 +63,10 @@ X-Gm-Message-State: AJIora/WUqr3biShTHQBjSlCKazFbrLxeYpxmr1VF0TpBUbjnJrcLT77
|
||||
X-Google-Smtp-Source: AGRyM1tai6X1Bx130Y1yHG5w2e0r8wx6bbI+H+YppWmQoT28TV3dSoYCqmeQK5VViW8WuvdOpQzhPQ==
|
||||
X-Received: by 2002:a62:29c3:0:b0:52b:f774:7242 with SMTP id p186-20020a6229c3000000b0052bf7747242mr12504553pfp.67.1658893354675;
|
||||
Tue, 26 Jul 2022 20:42:34 -0700 (PDT)
|
||||
Return-Path: <sender@example.com>
|
||||
Return-Path: <sender2@example.com>
|
||||
Received: from [192.168.1.2] ([8.8.8.8])
|
||||
by smtp.gmail.com with ESMTPSA id oj16-20020a17090b4d9000b001f291c9d3bdsm387578pjb.48.2022.07.26.20.42.32
|
||||
for <recipient@example.com>
|
||||
for <recipient2@example.com>
|
||||
(version=TLS1_3 cipher=TLS_AES_128_GCM_SHA256 bits=128/128);
|
||||
Tue, 26 Jul 2022 20:42:33 -0700 (PDT)
|
||||
Content-Type: multipart/mixed; boundary="------------ae0qIOkrNQLQHe1YyfTsUXrk"
|
||||
@@ -76,8 +76,8 @@ MIME-Version: 1.0
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101
|
||||
Thunderbird/91.11.0
|
||||
Content-Language: en-NZ
|
||||
To: "Recipient Ross" <recipient@example.com>
|
||||
From: Sender Smith <sender@example.com>
|
||||
To: "Recipient Ross" <recipient2@example.com>
|
||||
From: Sender Smith <sender2@example.com>
|
||||
Subject: inline + attachment
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
|
||||
@@ -37,6 +37,8 @@ func createSearchText(env *enmime.Envelope) string {
|
||||
b.WriteString(env.GetHeader("To") + " ")
|
||||
b.WriteString(env.GetHeader("Cc") + " ")
|
||||
b.WriteString(env.GetHeader("Bcc") + " ")
|
||||
b.WriteString(env.GetHeader("Reply-To") + " ")
|
||||
b.WriteString(env.GetHeader("Return-Path") + " ")
|
||||
h := strings.TrimSpace(
|
||||
html2text.HTML2TextWithOptions(
|
||||
env.HTML,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"api_version":"1.0.4",
|
||||
"last_update_date":"2023-07-25 17:42:58 +0000",
|
||||
"last_update_date":"2023-09-22 13:57:52 +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"},"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":[
|
||||
{
|
||||
@@ -318,7 +318,7 @@
|
||||
"last_test_date":"2023-07-24",
|
||||
"test_url":"https://www.caniemail.com/tests/css-background.html",
|
||||
"test_results_url":"https://app.emailonacid.com/app/acidtest/04SuPXr8tEGhWRlJ2Us6dA8BzgREpyxHYEmSBeyNuWyWo/list",
|
||||
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"y","2023-07":"a #6"},"ios":{"2018-09":"a #1","2018-10":"y","2019-02":"y"},"android":{"2018-09":"a #1","2018-10":"y","2019-02":"y"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2019-08":"y","2021-03":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"outlook":{"windows":{"2007":"n #5","2010":"n #5","2013":"n #5","2016":"n #5","2019":"n #5"},"windows-mail":{"2019-02":"n"},"macos":{"2019-02":"y"},"outlook-com":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"a #3 #4","2021-10":"a #3"},"ios":{"2019-02":"a #3 #4","2021-10":"a #3"},"android":{"2019-02":"a #3 #4","2021-10":"a #3"}},"aol":{"desktop-webmail":{"2019-02":"a #3 #4","2021-10":"a #3"},"ios":{"2019-02":"a #3 #4","2021-10":"a #3"},"android":{"2019-02":"a #3 #4","2021-10":"a #3"}},"samsung-email":{"android":{"5.0.10.2":"a #2","6.0.04.6":"y"}},"sfr":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"thunderbird":{"macos":{"60.5.0":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"t-online-de":{"desktop-webmail":{"2021-11":"n"}},"free-fr":{"desktop-webmail":{"2021-11":"n"}},"gmx":{"desktop-webmail":{"2022-06":"a #3"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"a #3"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
|
||||
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"y","2023-07":"a #6","2023-08":"y"},"ios":{"2018-09":"a #1","2018-10":"y","2019-02":"y"},"android":{"2018-09":"a #1","2018-10":"y","2019-02":"y"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2019-08":"y","2021-03":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"outlook":{"windows":{"2007":"n #5","2010":"n #5","2013":"n #5","2016":"n #5","2019":"n #5"},"windows-mail":{"2019-02":"n"},"macos":{"2019-02":"y"},"outlook-com":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"a #3 #4","2021-10":"a #3"},"ios":{"2019-02":"a #3 #4","2021-10":"a #3"},"android":{"2019-02":"a #3 #4","2021-10":"a #3"}},"aol":{"desktop-webmail":{"2019-02":"a #3 #4","2021-10":"a #3"},"ios":{"2019-02":"a #3 #4","2021-10":"a #3"},"android":{"2019-02":"a #3 #4","2021-10":"a #3"}},"samsung-email":{"android":{"5.0.10.2":"a #2","6.0.04.6":"y"}},"sfr":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"thunderbird":{"macos":{"60.5.0":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"t-online-de":{"desktop-webmail":{"2021-11":"n"}},"free-fr":{"desktop-webmail":{"2021-11":"n"}},"gmx":{"desktop-webmail":{"2022-06":"a #3"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"a #3"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":{"1":"Partial. Not supported with non Gmail accounts.","2":"Buggy. Requires at least one `<img>` element in the email to download all images.","3":"Partial. Does not support multiple values. The comma between two values is removed.","4":"Partial. Images URL must be between quotes.","5":"Background images can be used in VML. See [backgrounds.cm](https://backgrounds.cm/) and [VML documentation](https://docs.microsoft.com/en-us/windows/win32/vml/web-workshop---how-to-use-vml-on-web-pages-----fill--element).","6":"Partial and buggy. Removes the entire `style` attribute or `<style>` tag when a `url()` function with a valid image URL is present. See [Gmail rolling out changes that strip CSS with background images](https://freshinbox.com/blog/gmail-rolling-out-changes-that-strip-background-image-css/) and [Gmail and background images](https://parcel.io/blog/gmail-and-background-images)."}
|
||||
},
|
||||
@@ -398,7 +398,7 @@
|
||||
"last_test_date":"2023-07-24",
|
||||
"test_url":"https://www.caniemail.com/tests/css-background.html",
|
||||
"test_results_url":"https://app.emailonacid.com/app/acidtest/04SuPXr8tEGhWRlJ2Us6dA8BzgREpyxHYEmSBeyNuWyWo/list",
|
||||
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"y","2023-07":"a #6 #7"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2019-08":"y","2021-03":"a #6"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"outlook":{"windows":{"2007":"a #3","2010":"a #3","2013":"a #3","2016":"a #3","2019":"a #3"},"windows-mail":{"2019-02":"n","2021-10":"a #3"},"macos":{"2019-02":"y"},"outlook-com":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"a #1 #2 #4","2021-10":"a #1 #2"},"ios":{"2019-02":"a #1 #2 #4","2021-10":"a #1 #2"},"android":{"2019-02":"a #1 #2 #4","2021-10":"a #1 #2"}},"aol":{"desktop-webmail":{"2019-02":"a #1 #2 #4","2021-10":"a #1 #2"},"ios":{"2019-02":"a #1 #2 #4","2021-10":"a #1 #2"},"android":{"2019-02":"a #1 #2 #4","2021-10":"a #1 #2"}},"samsung-email":{"android":{"5.0.10.2":"y","6.0.04.6":"y"}},"sfr":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"thunderbird":{"macos":{"60.5.0":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"y #5"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"a #1 #5 #6"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"a #1 #5 #6"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}},
|
||||
"stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.1":"y"}},"gmail":{"desktop-webmail":{"2019-02":"y","2023-07":"a #6 #7","2023-08":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"},"mobile-webmail":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2019-08":"y","2021-03":"a #6"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"outlook":{"windows":{"2007":"a #3","2010":"a #3","2013":"a #3","2016":"a #3","2019":"a #3"},"windows-mail":{"2019-02":"n","2021-10":"a #3"},"macos":{"2019-02":"y"},"outlook-com":{"2019-02":"y"},"ios":{"2019-02":"y"},"android":{"2019-02":"y"}},"yahoo":{"desktop-webmail":{"2019-02":"a #1 #2 #4","2021-10":"a #1 #2"},"ios":{"2019-02":"a #1 #2 #4","2021-10":"a #1 #2"},"android":{"2019-02":"a #1 #2 #4","2021-10":"a #1 #2"}},"aol":{"desktop-webmail":{"2019-02":"a #1 #2 #4","2021-10":"a #1 #2"},"ios":{"2019-02":"a #1 #2 #4","2021-10":"a #1 #2"},"android":{"2019-02":"a #1 #2 #4","2021-10":"a #1 #2"}},"samsung-email":{"android":{"5.0.10.2":"y","6.0.04.6":"y"}},"sfr":{"desktop-webmail":{"2019-08":"y"},"ios":{"2019-08":"y"},"android":{"2019-08":"y"}},"thunderbird":{"macos":{"60.5.0":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"y #5"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"a #1 #5 #6"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"a #1 #5 #6"},"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 multiple values. The comma between two values is removed.","2":"Partial. Does not support the `/ value` shorthand for `background-size`. But it can be used in the `background-size` property instead.","3":"Partial. Only `background-color` values are supported.","4":"Partial. Images URL must be between quotes.","5":"Partial. Does not support multiple values. The entire property is removed if so.","6":"Partial. Does not support the `/ value` shorthand for `background-size`.","7":"Partial and buggy. Removes the entire `style` attribute or `<style>` tag when a `url()` function with a valid image URL is present. See [Gmail rolling out changes that strip CSS with background images](https://freshinbox.com/blog/gmail-rolling-out-changes-that-strip-background-image-css/) and [Gmail and background images](https://parcel.io/blog/gmail-and-background-images)."}
|
||||
},
|
||||
@@ -1262,7 +1262,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":"n #2","6.1":"n #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":"y #1"},"windows-mail":{"2020-01":"y #1"},"macos":{"2011":"y","2016":"y"},"outlook-com":{"2019-08":"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"},"outlook-com":{"2019-08":"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"}}},
|
||||
"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)."}
|
||||
},
|
||||
@@ -1347,6 +1347,22 @@
|
||||
"notes_by_num":{"1":"Depends on browser support.","2":"Using this syntax for an inline style will remove all inline styles applied to that element."}
|
||||
},
|
||||
|
||||
{
|
||||
"slug":"css-nesting",
|
||||
"title":"CSS Nesting",
|
||||
"description":"A syntax for nesting selectors, providing the ability to nest one style rule inside another.",
|
||||
"url":"https://www.caniemail.com/features/css-nesting/",
|
||||
"category":"css",
|
||||
"tags":[],
|
||||
"keywords":null,
|
||||
"last_test_date":"2023-08-31",
|
||||
"test_url":"https://www.caniemail.com/tests/css-nesting.html",
|
||||
"test_results_url":"https://app.emailonacid.com/app/acidtest/8z9ecWkyaSHebmYl0r6dlWFfcia0VNfeKu6s01l5Fw3M0/list",
|
||||
"stats":{"apple-mail":{"macos":{"16.0":"a #1"},"ios":{"16.6":"a #1"}},"gmail":{"desktop-webmail":{"2023-08":"n"},"ios":{"2023-08":"n"},"android":{"2023-08":"n"},"mobile-webmail":{"2023-08":"n"}},"orange":{"desktop-webmail":{"2023-08":"a #2"},"ios":{"2023-08":"a #2"},"android":{"2023-08":"u"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2023-08":"n"},"macos":{"16.78":"a #1"},"outlook-com":{"2023-08":"n"},"ios":{"2023-08":"n"},"android":{"2023-08":"u"}},"samsung-email":{"android":{"6.0":"u"}},"sfr":{"desktop-webmail":{"2023-08":"a #1 #2"},"ios":{"2023-08":"n"},"android":{"2023-08":"u"}},"thunderbird":{"macos":{"102.15":"n"}},"aol":{"desktop-webmail":{"2023-08":"u"},"ios":{"2023-08":"u"},"android":{"2023-08":"u"}},"yahoo":{"desktop-webmail":{"2023-08":"n #3"},"ios":{"2023-08":"n #3"},"android":{"2023-08":"u"}},"protonmail":{"desktop-webmail":{"2023-08":"u"},"ios":{"2023-08":"u"},"android":{"2023-08":"u"}},"hey":{"desktop-webmail":{"2023-08":"u"}},"mail-ru":{"desktop-webmail":{"2023-08":"u"}},"fastmail":{"desktop-webmail":{"2023-08":"u"}},"laposte":{"desktop-webmail":{"2023-08":"u"}},"free-fr":{"desktop-webmail":{"2023-08":"u"}},"t-online-de":{"desktop-webmail":{"2023-08":"u"}},"gmx":{"desktop-webmail":{"2021-12":"u"},"ios":{"2021-12":"u"},"android":{"2021-12":"u"}},"web-de":{"desktop-webmail":{"2021-12":"u"},"ios":{"2021-12":"u"},"android":{"2021-12":"u"}},"ionos-1and1":{"desktop-webmail":{"2021-12":"u"},"android":{"2021-12":"u"}}},
|
||||
"notes":null,
|
||||
"notes_by_num":{"1":"Buggy. `E { F {}}` doesn’t work, but `E { & F {}}` does.","2":"Buggy. The syntax is supported, but nested selectors are prefixed by the webmail, which might invalidate the selector.","3":"Not supported. The nested selectors are removed, making the nest properties apply to the parent selector."}
|
||||
},
|
||||
|
||||
{
|
||||
"slug":"css-object-fit",
|
||||
"title":"object-fit",
|
||||
@@ -1630,7 +1646,7 @@
|
||||
"last_test_date":"2022-03-15",
|
||||
"test_url":"https://www.caniemail.com/tests/css-has.html",
|
||||
"test_results_url":"",
|
||||
"stats":{"apple-mail":{"macos":{"15.0":"n","16.0":"y"},"ios":{"15.1":"n","15.4":"y"}},"gmail":{"desktop-webmail":{"2021-12":"n"},"ios":{"2021-12":"n"},"android":{"2021-12":"n"},"mobile-webmail":{"2021-12":"n"}},"orange":{"desktop-webmail":{"2021-12":"y"},"ios":{"2021-12":"n"},"android":{"2021-12":"n"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2021-12":"n"},"macos":{"16.56":"n"},"outlook-com":{"2021-12":"n"},"ios":{"2021-12":"n"},"android":{"2021-12":"n"}},"samsung-email":{"android":{"6.0":"n"}},"sfr":{"desktop-webmail":{"2021-12":"y"},"ios":{"2021-12":"n"},"android":{"2021-12":"n"}},"thunderbird":{"macos":{"78.14":"n"}},"aol":{"desktop-webmail":{"2021-12":"n #1"},"ios":{"2021-12":"n"},"android":{"2021-12":"n"}},"yahoo":{"desktop-webmail":{"2021-12":"n #1"},"ios":{"2021-12":"n"},"android":{"2021-12":"n"}},"protonmail":{"desktop-webmail":{"2021-12":"n #2"},"ios":{"2021-12":"n"},"android":{"2021-12":"n"}},"hey":{"desktop-webmail":{"2021-12":"y"}},"mail-ru":{"desktop-webmail":{"2021-12":"n"}},"fastmail":{"desktop-webmail":{"2021-12":"n"}},"laposte":{"desktop-webmail":{"2021-12":"y"}},"gmx":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"y"},"android":{"2022-07":"y"}},"web-de":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"y"},"android":{"2022-07":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-07":"y"},"android":{"2022-07":"y"}}},
|
||||
"stats":{"apple-mail":{"macos":{"15.0":"n","16.0":"y"},"ios":{"15.1":"n","15.4":"y"}},"gmail":{"desktop-webmail":{"2021-12":"n","2023-09":"n"},"ios":{"2021-12":"n","2023-09":"n"},"android":{"2021-12":"n","2023-09":"n"},"mobile-webmail":{"2021-12":"n"}},"orange":{"desktop-webmail":{"2021-12":"y"},"ios":{"2021-12":"n"},"android":{"2021-12":"n"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2021-12":"n"},"macos":{"16.56":"n","16.73":"y"},"outlook-com":{"2021-12":"n","2023-09":"n"},"ios":{"2021-12":"n","2023-09":"n"},"android":{"2021-12":"n"}},"samsung-email":{"android":{"6.0":"n","6.1.82":"y"}},"sfr":{"desktop-webmail":{"2021-12":"y"},"ios":{"2021-12":"n"},"android":{"2021-12":"n"}},"thunderbird":{"macos":{"78.14":"n","115.2":"n"}},"aol":{"desktop-webmail":{"2021-12":"n #1"},"ios":{"2021-12":"n"},"android":{"2021-12":"n"}},"yahoo":{"desktop-webmail":{"2021-12":"n #1","2023-09":"n #1"},"ios":{"2021-12":"n","2023-09":"n"},"android":{"2021-12":"n","2023-09":"n"}},"protonmail":{"desktop-webmail":{"2021-12":"n #2","2023-09":"y"},"ios":{"2021-12":"n","2023-09":"y"},"android":{"2021-12":"n","2023-09":"n"}},"hey":{"desktop-webmail":{"2021-12":"y"}},"mail-ru":{"desktop-webmail":{"2021-12":"n","2023-09":"n"}},"fastmail":{"desktop-webmail":{"2021-12":"n","2023-09":"y"}},"laposte":{"desktop-webmail":{"2021-12":"y"}},"gmx":{"desktop-webmail":{"2022-07":"n","2023-09":"n"},"ios":{"2022-07":"y"},"android":{"2022-07":"y"}},"web-de":{"desktop-webmail":{"2022-07":"n"},"ios":{"2022-07":"y"},"android":{"2022-07":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-07":"y"},"android":{"2022-07":"y"}}},
|
||||
"notes":"As of december 2021, `:has()` is only supported in [Safari Technology Preview 137](https://webkit.org/blog/12156/release-notes-for-safari-technology-preview-137/). As of march 2022, it is supported in Safari 15.4.",
|
||||
"notes_by_num":{"1":"Not supported. `:has(…)` is replaced by `:has`.","2":"Not supported. But the pseudo-class seems interpreted and computed server side."}
|
||||
},
|
||||
@@ -2751,8 +2767,8 @@
|
||||
"test_url":"https://www.caniemail.com/tests/css-vertical-align-html-valign.html",
|
||||
"test_results_url":"https://app.emailonacid.com/app/acidtest/XDUBIjG7AOXLUwfUUDYDO68OO1POjklmaeeqkOeSylkJL/list",
|
||||
"stats":{"apple-mail":{"macos":{"13":"y"},"ios":{"13":"y"}},"gmail":{"desktop-webmail":{"2020-12":"y"},"ios":{"2020-12":"y"},"android":{"2020-12":"y"},"mobile-webmail":{"2020-12":"y"}},"orange":{"desktop-webmail":{"2020-12":"y","2021-03":"y"},"ios":{"2020-12":"y"},"android":{"2020-12":"y"}},"outlook":{"windows":{"2003":"y","2007":"y","2010":"y","2013":"y","2016":"y","2019":"y"},"windows-mail":{"2020-12":"y"},"macos":{"2016":"y"},"outlook-com":{"2020-12":"y"},"ios":{"2020-12":"y"},"android":{"2020-12":"y"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2020-12":"y"},"ios":{"2020-12":"y"},"android":{"2020-12":"y"}},"thunderbird":{"macos":{"78.6":"y"}},"aol":{"desktop-webmail":{"2020-12":"y"},"ios":{"2020-12":"y"},"android":{"2020-12":"y"}},"yahoo":{"desktop-webmail":{"2020-12":"y"},"ios":{"2020-12":"y"},"android":{"2020-12":"y"}},"protonmail":{"desktop-webmail":{"2020-12":"y"},"ios":{"2020-12":"y"},"android":{"2020-12":"y"}},"hey":{"desktop-webmail":{"2020-12":"y"}},"mail-ru":{"desktop-webmail":{"2020-12":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-09":"y"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"web-de":{"desktop-webmail":{"2022-09":"y"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-09":"y"},"android":{"2022-09":"y"}}},
|
||||
"notes":"This is a global note.",
|
||||
"notes_by_num":{"1":"Partial. Fixed attachment is not supported.","2":"Partial. Slash syntax values are not supported.","3":"Partial. Values containing background images are not supported.","4":"Buggy. For slash syntax values, it removes the slash character, making the value invalid.","5":"Partial. Seems to only support background colors."}
|
||||
"notes":null,
|
||||
"notes_by_num":null
|
||||
},
|
||||
|
||||
{
|
||||
@@ -3755,10 +3771,10 @@
|
||||
"category":"html",
|
||||
"tags":[],
|
||||
"keywords":null,
|
||||
"last_test_date":"2019-06-24",
|
||||
"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/od5IYQtx8yIbIUbeRyQXnP0yzFKEm2E9CKa3FU4BcEXFv/list",
|
||||
"stats":{"apple-mail":{"macos":{"10.3":"y","16.0":"y"},"ios":{"10.3":"y","12.3":"y","16.2":"y"}},"gmail":{"desktop-webmail":{"2019-06":"a #1","2023-01":"a #1"},"ios":{"2019-06":"a #1 #2","2023-01":"a #1 #2"},"android":{"2019-06":"a #1 #2"},"mobile-webmail":{"2020-02":"n","2023-01":"n"}},"orange":{"desktop-webmail":{"2019-06":"y","2021-03":"y","2023-02":"y"},"ios":{"2019-06":"y","2023-02":"y"},"android":{"2019-06":"n","2019-08":"y","2023-02":"y"}},"outlook":{"windows":{"2003":"y","2007":"a #4","2010":"a #4","2013":"a #4","2016":"a #4","2019":"a #4"},"windows-mail":{"2020-01":"a #4","2023-01":"a #4"},"macos":{"2011":"y","2016":"y","2019":"y","2021":"y"},"outlook-com":{"2019-06":"y","2023-01":"y"},"ios":{"2019-06":"y","2023-01":"y"},"android":{"2019-06":"y"}},"samsung-email":{"android":{"5.0.10.2":"y","6.0":"y"}},"sfr":{"desktop-webmail":{"2019-06":"y","2023-02":"y"},"ios":{"2019-06":"n","2023-02":"n"},"android":{"2019-06":"n","2023-02":"n"}},"thunderbird":{"macos":{"60.3":"y","102.7":"y"},"windows":{"102.7":"y"}},"aol":{"desktop-webmail":{"2020-01":"y","2023-01":"y"},"ios":{"2020-01":"y","2023-01":"y"},"android":{"2020-01":"y"}},"yahoo":{"desktop-webmail":{"2019-06":"y #5","2023-01":"y"},"ios":{"2019-06":"y","2023-01":"y"},"android":{"2019-06":"a #3","2023-01":"a #3"}},"protonmail":{"desktop-webmail":{"2020-03":"n","2022-02":"y","2023-01":"y"},"ios":{"2020-03":"n","2023-01":"a #1"},"android":{"2020-03":"a #1"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-09":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"},"ios":{"2023-02":"n"},"android":{"2023-02":"n"}},"gmx":{"desktop-webmail":{"2022-11":"y"},"ios":{"2022-11":"y"},"android":{"2022-11":"y"}},"web-de":{"desktop-webmail":{"2022-11":"y"},"ios":{"2022-11":"y"},"android":{"2022-11":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"y"},"android":{"2022-11":"y"}},"rainloop":{"desktop-webmail":{"2023-02":"n"}}},
|
||||
"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"},"outlook-com":{"2019-06":"y","2023-01":"y"},"ios":{"2019-06":"y","2023-01":"y"},"android":{"2019-06":"y"}},"samsung-email":{"android":{"5.0.10.2":"y","6.0":"y"}},"sfr":{"desktop-webmail":{"2019-06":"y","2023-02":"y"},"ios":{"2019-06":"n","2023-02":"n"},"android":{"2019-06":"n","2023-02":"n"}},"thunderbird":{"macos":{"60.3":"y","102.7":"y"},"windows":{"102.7":"y"}},"aol":{"desktop-webmail":{"2020-01":"y","2023-01":"y"},"ios":{"2020-01":"y","2023-01":"y"},"android":{"2020-01":"y"}},"yahoo":{"desktop-webmail":{"2019-06":"y #5","2023-01":"y"},"ios":{"2019-06":"y","2023-01":"y"},"android":{"2019-06":"a #3","2023-01":"a #3"}},"protonmail":{"desktop-webmail":{"2020-03":"n","2022-02":"y","2023-01":"y"},"ios":{"2020-03":"n","2023-01":"a #1"},"android":{"2020-03":"a #1"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-09":"y"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"},"ios":{"2023-02":"n"},"android":{"2023-02":"n"}},"gmx":{"desktop-webmail":{"2022-11":"y","2023-07":"n"},"ios":{"2022-11":"y"},"android":{"2022-11":"y"}},"web-de":{"desktop-webmail":{"2022-11":"y"},"ios":{"2022-11":"y"},"android":{"2022-11":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-11":"y"},"android":{"2022-11":"y"}},"rainloop":{"desktop-webmail":{"2023-02":"n"}}},
|
||||
"notes":"",
|
||||
"notes_by_num":{"1":"Partial. Not supported inside the `<body>`.","2":"Partial. Not supported with Non Gmail Accounts.","3":"Buggy. The first `<head>` in the HTML is removed, so `<style>` elements need to be in a second `<head>` element.","4":"Buggy. `<style>` elements need to be declared before their rules are used.","5":"A CSS rule following a CSS comment is ignored. (See [email-bugs#25](https://github.com/hteumeuleu/email-bugs/issues/25).)"}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user