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/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 {
-
+
+
+
+
+
+
+ This will permanently delete {{ formatNumber(items.length) }} messages.
+
+
+
+
+
+
@@ -949,6 +1045,30 @@ export default {
+
+
+
+
+
+
+ This will mark {{ formatNumber(unreadInSearch) }} messages as read.
+
+
+
+
+
+
diff --git a/storage/database.go b/storage/database.go
index 5b50932..192f149 100644
--- a/storage/database.go
+++ b/storage/database.go
@@ -117,10 +117,6 @@ type DBMailSummary struct {
To []*mail.Address
Cc []*mail.Address
Bcc []*mail.Address
- // Subject string
- // Size int
- // Inline int
- // Attachments int
}
// InitDB will initialise the database
@@ -255,7 +251,16 @@ func Store(body []byte) (string, error) {
return "", err
}
- tagData := findTags(&body)
+ // extract tags from body matches based on --tag
+ tagStr := findTagsInRawMessage(&body)
+
+ // extract tags from X-Tags header
+ headerTags := strings.TrimSpace(env.Root.Header.Get("X-Tags"))
+ if headerTags != "" {
+ tagStr += "," + headerTags
+ }
+
+ tagData := uniqueTagsFromString(tagStr)
tagJSON, err := json.Marshal(tagData)
if err != nil {
@@ -376,7 +381,7 @@ func List(start, limit int) ([]MessageSummary, error) {
}
// Search will search a mailbox for search terms.
-// The search is broken up by segments (exact phrases can be quoted), and interprits specific terms such as:
+// The search is broken up by segments (exact phrases can be quoted), and interprets specific terms such as:
// is:read, is:unread, has:attachment, to:, from: & subject:
// Negative searches also also included by prefixing the search term with a `-` or `!`
func Search(search string, start, limit int) ([]MessageSummary, error) {
@@ -886,7 +891,7 @@ func IsUnread(id string) bool {
return unread == 1
}
-// MessageIDExists blaah
+// MessageIDExists checks whether a Message-ID exists in the DB
func MessageIDExists(id string) bool {
var total int
diff --git a/storage/tags.go b/storage/tags.go
index e304335..4c47506 100644
--- a/storage/tags.go
+++ b/storage/tags.go
@@ -3,23 +3,21 @@ package storage
import (
"context"
"encoding/json"
- "regexp"
"sort"
"strings"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/utils/logger"
+ "github.com/axllent/mailpit/utils/tools"
"github.com/leporo/sqlf"
)
// SetTags will set the tags for a given database ID, used via API
func SetTags(id string, tags []string) error {
applyTags := []string{}
- reg := regexp.MustCompile(`\s+`)
for _, t := range tags {
- t = strings.TrimSpace(reg.ReplaceAllString(t, " "))
-
- if t != "" && config.TagRegexp.MatchString(t) && !inArray(t, applyTags) {
+ t = tools.CleanTag(t)
+ if t != "" && config.ValidTagRegexp.MatchString(t) && !inArray(t, applyTags) {
applyTags = append(applyTags, t)
}
}
@@ -42,23 +40,22 @@ func SetTags(id string, tags []string) error {
return err
}
-// Used to auto-apply tags to new messages
-func findTags(message *[]byte) []string {
- tags := []string{}
+// Find tags set via --tags in raw message.
+// Returns a comma-separated string.
+func findTagsInRawMessage(message *[]byte) string {
+ tagStr := ""
if len(config.SMTPTags) == 0 {
- return tags
+ return tagStr
}
str := strings.ToLower(string(*message))
for _, t := range config.SMTPTags {
- if !inArray(t.Tag, tags) && strings.Contains(str, t.Match) {
- tags = append(tags, t.Tag)
+ if strings.Contains(str, t.Match) {
+ tagStr += "," + t.Tag
}
}
- sort.Strings(tags)
-
- return tags
+ return tagStr
}
// Get message tags from the database for a given database ID
@@ -84,3 +81,31 @@ func getMessageTags(id string) []string {
return tags
}
+
+// UniqueTagsFromString will split a string with commas, and extract a unique slice of formatted tags
+func uniqueTagsFromString(s string) []string {
+ tags := []string{}
+
+ if s == "" {
+ return tags
+ }
+
+ parts := strings.Split(s, ",")
+ for _, p := range parts {
+ w := tools.CleanTag(p)
+ if w == "" {
+ continue
+ }
+ if config.ValidTagRegexp.MatchString(w) {
+ if !inArray(w, tags) {
+ tags = append(tags, w)
+ }
+ } else {
+ logger.Log().Debugf("[db] ignoring invalid tag: %s", w)
+ }
+ }
+
+ sort.Strings(tags)
+
+ return tags
+}
diff --git a/storage/utils.go b/storage/utils.go
index fb23bc7..2cff91e 100644
--- a/storage/utils.go
+++ b/storage/utils.go
@@ -75,7 +75,7 @@ func dbCron() {
time.Sleep(60 * time.Second)
start := time.Now()
- // check if database contains deleted data and has not beein in use
+ // check if database contains deleted data and has not been in use
// for 5 minutes, if so VACUUM
currentTime := time.Now()
diff := currentTime.Sub(dbLastAction)
@@ -167,6 +167,7 @@ func isFile(path string) bool {
return true
}
+// InArray tests if a string in within an array. It is not case sensitive.
func inArray(k string, arr []string) bool {
k = strings.ToLower(k)
for _, v := range arr {
diff --git a/utils/tools/message.go b/utils/tools/message.go
index bd78243..5af68b1 100644
--- a/utils/tools/message.go
+++ b/utils/tools/message.go
@@ -1,4 +1,4 @@
-// Package tools provides various methods for variouws things
+// Package tools provides various methods for various things
package tools
import (
@@ -23,7 +23,7 @@ func RemoveMessageHeaders(msg []byte, headers []string) ([]byte, error) {
reBlank := regexp.MustCompile(`^\s+`)
for _, hdr := range headers {
- // case-insentitive
+ // case-insensitive
reHdr := regexp.MustCompile(`(?i)^` + regexp.QuoteMeta(hdr+":"))
// header := []byte(hdr + ":")
diff --git a/utils/tools/tags.go b/utils/tools/tags.go
new file mode 100644
index 0000000..660501e
--- /dev/null
+++ b/utils/tools/tags.go
@@ -0,0 +1,25 @@
+package tools
+
+import (
+ "regexp"
+ "strings"
+)
+
+var (
+ // Invalid tag characters regex
+ tagsInvalidChars = regexp.MustCompile(`[^a-zA-Z0-9\-\ \_]`)
+
+ // Regex to catch multiple spaces
+ multiSpaceRe = regexp.MustCompile(`(\s+)`)
+)
+
+// CleanTag returns a clean tag, removing whitespace and invalid characters
+func CleanTag(s string) string {
+ s = strings.TrimSpace(
+ multiSpaceRe.ReplaceAllString(
+ tagsInvalidChars.ReplaceAllString(s, " "),
+ " ",
+ ),
+ )
+ return s
+}