diff --git a/CHANGELOG.md b/CHANGELOG.md index 90c6402..270e4f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ Notable changes to Mailpit will be documented in this file. +## [v1.6.14] + +### Feature +- Add ability to delete or mark search results read +- Set tags via X-Tags message header + +### Libs +- Update node modules + + ## [v1.6.13] ### Feature diff --git a/config/config.go b/config/config.go index 26b32f1..744b3ff 100644 --- a/config/config.go +++ b/config/config.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/axllent/mailpit/utils/logger" + "github.com/axllent/mailpit/utils/tools" "github.com/mattn/go-shellwords" "github.com/tg123/go-htpasswd" "gopkg.in/yaml.v3" @@ -41,7 +42,7 @@ var ( // UIAuthFile for basic authentication UIAuthFile string - // UIAuth used for euthentication + // UIAuth used for authentication UIAuth *htpasswd.File // Webroot to define the base path for the UI and API @@ -71,8 +72,8 @@ var ( // SMTPCLITags is used to map the CLI args SMTPCLITags string - // TagRegexp is the allowed tag characters - TagRegexp = regexp.MustCompile(`^([a-zA-Z0-9\-\ \_]){3,}$`) + // ValidTagRegexp represents a valid tag + ValidTagRegexp = regexp.MustCompile(`^([a-zA-Z0-9\-\ \_]){3,}$`) // SMTPTags are expressions to apply tags to new mail SMTPTags []AutoTag @@ -86,7 +87,7 @@ var ( // ReleaseEnabled is whether message releases are enabled, requires a valid SMTPRelayConfigFile ReleaseEnabled = false - // SMTPRelayAllIncoming is whether to relay all incoming messages via preconfgured SMTP server. + // SMTPRelayAllIncoming is whether to relay all incoming messages via pre-configured SMTP server. // Use with extreme caution! SMTPRelayAllIncoming = false @@ -219,8 +220,8 @@ func VerifyConfig() error { for _, a := range args { t := strings.Split(a, "=") if len(t) > 1 { - tag := strings.TrimSpace(t[0]) - if !TagRegexp.MatchString(tag) || len(tag) == 0 { + tag := tools.CleanTag(t[0]) + if !ValidTagRegexp.MatchString(tag) || len(tag) == 0 { return fmt.Errorf("Invalid tag (%s) - can only contain spaces, letters, numbers, - & _", tag) } match := strings.TrimSpace(strings.ToLower(strings.Join(t[1:], "="))) diff --git a/docs/apiv1/Message.md b/docs/apiv1/Message.md index d40c36e..28f14eb 100644 --- a/docs/apiv1/Message.md +++ b/docs/apiv1/Message.md @@ -15,6 +15,7 @@ Returns a JSON summary of the message and attachments. ```json { "ID": "d7a5543b-96dd-478b-9b60-2b465c9884de", + "MessageID": "12345.67890@localhost", "Read": true, "From": { "Name": "John Doe", @@ -31,6 +32,7 @@ Returns a JSON summary of the message and attachments. "ReplyTo": [], "Subject": "Message subject", "Date": "2016-09-07T16:46:00+13:00", + "Tags": ["test"], "Text": "Plain text MIME part of the email", "HTML": "HTML MIME part (if exists)", "Size": 79499, diff --git a/docs/apiv1/Messages.md b/docs/apiv1/Messages.md index 9d72129..709f9c5 100644 --- a/docs/apiv1/Messages.md +++ b/docs/apiv1/Messages.md @@ -31,9 +31,11 @@ List messages in the mailbox. Messages are returned in the order of latest recei "unread": 500, "count": 50, "start": 0, + "tags": ["test"], "messages": [ { "ID": "1c575821-70ba-466f-8cee-2e1cf0fcdd0f", + "MessageID": "12345.67890@localhost", "Read": false, "From": { "Name": "John Doe", @@ -54,6 +56,7 @@ List messages in the mailbox. Messages are returned in the order of latest recei "Bcc": [], "Subject": "Message subject", "Created": "2022-10-03T21:35:32.228605299+13:00", + "Tags": ["test"], "Size": 6144, "Attachments": 0 }, diff --git a/docs/apiv1/Search.md b/docs/apiv1/Search.md index fdf6d02..038e55f 100644 --- a/docs/apiv1/Search.md +++ b/docs/apiv1/Search.md @@ -30,6 +30,7 @@ Matching messages are returned in the order of latest received to oldest. "messages": [ { "ID": "1c575821-70ba-466f-8cee-2e1cf0fcdd0f", + "MessageID": "12345.67890@localhost", "Read": false, "From": { "Name": "John Doe", diff --git a/package-lock.json b/package-lock.json index 2c7252c..5549f6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,9 +35,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.21.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.9.tgz", - "integrity": "sha512-q5PNg/Bi1OpGgx5jYlvWZwAorZepEudDMCLtj967aeS7WMont7dUZI46M2XwcIQqvUlMxWfdLFu4S/qSxeUu5g==", + "version": "7.22.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.4.tgz", + "integrity": "sha512-VLLsx06XkEYqBtE5YGPwfSGwfrjnyPP5oiGty3S8pQLFDFLaS8VwWSIxkTXpcvr5zeYLE6+MBNl2npl/YnfofA==", "bin": { "parser": "bin/babel-parser.js" }, @@ -46,11 +46,11 @@ } }, "node_modules/@babel/runtime-corejs3": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.21.5.tgz", - "integrity": "sha512-FRqFlFKNazWYykft5zvzuEl1YyTDGsIRrjV9rvxvYkUC7W/ueBng1X68Xd6uRMzAaJ0xMKn08/wem5YS1lpX8w==", + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.22.3.tgz", + "integrity": "sha512-6bdmknScYKmt8I9VjsJuKKGr+TwUb555FTf6tT1P/ANlCjTHCiYLhiQ4X/O7J731w5NOqu8c1aYHEVuOwPz7jA==", "dependencies": { - "core-js-pure": "^3.25.1", + "core-js-pure": "^3.30.2", "regenerator-runtime": "^0.13.11" }, "engines": { @@ -428,9 +428,9 @@ } }, "node_modules/@popperjs/core": { - "version": "2.11.7", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz", - "integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -1029,9 +1029,9 @@ } }, "node_modules/bootstrap": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.3.tgz", - "integrity": "sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.0.tgz", + "integrity": "sha512-UnBV3E3v4STVNQdms6jSGO2CvOkjUMdDAVR2V5N4uCMdaIkaQjbcEAMqRimDHIs4uqBYzDAKCQwCB+97tJgHQw==", "funding": [ { "type": "github", @@ -1043,7 +1043,7 @@ } ], "peerDependencies": { - "@popperjs/core": "^2.11.6" + "@popperjs/core": "^2.11.7" } }, "node_modules/bootstrap-icons": { @@ -1965,9 +1965,9 @@ } }, "node_modules/postcss": { - "version": "8.4.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", - "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", + "version": "8.4.24", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", + "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", "funding": [ { "type": "opencollective", diff --git a/server/ui-src/App.vue b/server/ui-src/App.vue index 4bfd9a5..1432b01 100644 --- a/server/ui-src/App.vue +++ b/server/ui-src/App.vue @@ -74,6 +74,13 @@ export default { }, canNext: function () { return this.total > (this.start + this.count); + }, + unreadInSearch: function () { + if (!this.searching) { + return false; + } + + return this.items.filter(i => !i.Read).length; } }, @@ -304,6 +311,24 @@ export default { }); }, + // delete messages displayed in current search + deleteSearch: function () { + let ids = this.items.map(item => item.ID); + + if (!ids.length) { + return false; + } + + let self = this; + let uri = 'api/v1/messages'; + self.delete(uri, { 'ids': ids }, function (response) { + window.location.hash = ""; + self.scrollInPlace = true; + self.loadMessages(); + }); + }, + + // delete all messages from mailbox deleteAll: function () { let self = this; let uri = 'api/v1/messages'; @@ -313,6 +338,7 @@ export default { }); }, + // mark current message as read markUnread: function () { let self = this; if (!self.message) { @@ -326,6 +352,7 @@ export default { }); }, + // mark all messages in mailbox as read markAllRead: function () { let self = this; let uri = 'api/v1/messages' @@ -336,6 +363,24 @@ export default { }); }, + // mark messages in current search as read + markSearchRead: function () { + let ids = this.items.map(item => item.ID); + + if (!ids.length) { + return false; + } + + let self = this; + let uri = 'api/v1/messages'; + self.put(uri, { 'read': true, 'ids': ids }, function (response) { + window.location.hash = ""; + self.scrollInPlace = true; + self.loadMessages(); + }); + }, + + // mark selected messages as read markSelectedRead: function () { let self = this; if (!self.selected.length) { @@ -349,6 +394,7 @@ export default { }); }, + // mark selected messages as unread markSelectedUnread: function () { let self = this; if (!self.selected.length) { @@ -362,7 +408,7 @@ export default { }); }, - // test of any selected emails are unread + // test if any selected emails are unread selectedHasUnread: function () { if (!this.selected.length) { return false; @@ -709,7 +755,8 @@ export default { Mailpit
- +
@@ -720,14 +767,29 @@ export default {
- + + + + +