Compare commits

..

19 Commits

Author SHA1 Message Date
Ralph Slooten
26c6f9d965 Merge branch 'release/v1.9.2' 2023-09-24 19:16:23 +13:00
Ralph Slooten
76a261bf06 Release v1.9.2 2023-09-24 19:16:22 +13:00
Ralph Slooten
86a3bea300 Libs: Update node modules 2023-09-24 19:14:46 +13:00
Ralph Slooten
5fa6b20a53 Update tag test message 2023-09-24 19:10:41 +13:00
Ralph Slooten
3ad62769a6 Tests: Add message tag tests 2023-09-24 19:08:47 +13:00
Ralph Slooten
a63952aee6 Tests: Add search delete tests 2023-09-24 17:29:27 +13:00
Ralph Slooten
de95910539 Change recipients <name>2@example.com 2023-09-24 17:27:02 +13:00
Ralph Slooten
60a41ce3ca Fix: Delete all messages matching search when more than 1000 results 2023-09-24 13:07:16 +13:00
Ralph Slooten
898b36ce0b UI: Reset pagination when returning to inbox from search 2023-09-24 12:24:52 +13:00
Ralph Slooten
b4a4d44492 Merge tag 'v1.9.1' into develop
Release v1.9.1
2023-09-23 22:58:14 +12:00
Ralph Slooten
64e4e4240a Merge branch 'release/v1.9.1' 2023-09-23 22:58:11 +12:00
Ralph Slooten
0477c6573f Release v1.9.1 2023-09-23 22:58:10 +12:00
Ralph Slooten
28ac6d2099 UI: Set 404 page when loading a non-existent message 2023-09-23 15:49:43 +12:00
Ralph Slooten
43a1dbe3f0 Chore: Update caniemail data 2023-09-23 14:56:57 +12:00
Ralph Slooten
aa3f860540 Libs: Update Go modules 2023-09-23 11:51:29 +12:00
Ralph Slooten
f54a2187ac UI: Link email addresses in message summary to search 2023-09-23 11:48:06 +12:00
Ralph Slooten
063eab2c6a UI: Better support for mobile screen sizes 2023-09-23 09:31:02 +12:00
Ralph Slooten
b282e6663b Remove redundant Read status from message (always true) 2023-09-22 21:31:35 +12:00
Ralph Slooten
df777c6e90 Merge tag 'v1.9.0' into develop
Release v1.9.0
2023-09-22 16:40:51 +12:00
19 changed files with 444 additions and 222 deletions

View File

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

View File

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

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

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

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

View File

@@ -228,6 +228,12 @@
}
}
.messageHeaders {
th {
vertical-align: top;
}
}
.list-group-item.message:first-child {
border-top: 0;
}

View File

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

View File

@@ -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">
&lt;{{ message.From.Address }}&gt;
&lt;<a :href="searchURI(message.From.Address)" class="text-body">
{{ message.From.Address }}
</a>&gt;
</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 + " &lt;" + t.Address + "&gt;" }}</span>
<span>
{{ t.Name }}
&lt;<a :href="searchURI(t.Address)" class="text-body">
{{ t.Address }}
</a>&gt;
</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 + " &lt;" + t.Address + "&gt;" }} </span>
{{ t.Name }}
&lt;<a :href="searchURI(t.Address)" class="text-body">
{{ t.Address }}
</a>&gt;
</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 + " &lt;" + t.Address + "&gt;" }}
{{ t.Name }}
&lt;<a :href="searchURI(t.Address)" class="text-body">
{{ t.Address }}
</a>&gt;
</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 + " &lt;" + t.Address + "&gt;" }} </span>
{{ t.Name }}
&lt;<a :href="searchURI(t.Address)" class="text-body-secondary">
{{ t.Address }}
</a>&gt;
</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">&lt;{{ message.ReturnPath }}&gt;</td>
<td class="privacy text-body-secondary text-break">
&lt;<a :href="searchURI(message.ReturnPath)" class="text-body-secondary">
{{ message.ReturnPath }}
</a>&gt;
</td>
</tr>
<tr>
<th class="small">Subject</th>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {}}` doesnt 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).)"}
},