diff --git a/CHANGELOG.md b/CHANGELOG.md index beb13e76cf..ccff7d0141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ Notable changes to Mailpit will be documented in this file. +## [v1.21.1] + +### Feature +- Add ability to search by size smaller or larger than a value (eg: `larger:1M` / `smaller:2.5M`) +- Add ability to search for messages containing inline images (`has:inline`) + +### Chore +- Update Go dependencies +- Separate attachments and inline images in download nav and badges ([#379](https://github.com/axllent/mailpit/issues/379)) + + ## [v1.21.0] ### Feature diff --git a/go.mod b/go.mod index 7d616b5749..88565dd199 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 - github.com/tg123/go-htpasswd v1.2.2 + github.com/tg123/go-htpasswd v1.2.3 github.com/vanng822/go-premailer v1.22.0 golang.org/x/net v0.30.0 golang.org/x/text v0.19.0 diff --git a/go.sum b/go.sum index 97b3b98ca3..da752b99cb 100644 --- a/go.sum +++ b/go.sum @@ -111,8 +111,8 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tg123/go-htpasswd v1.2.2 h1:tmNccDsQ+wYsoRfiONzIhDm5OkVHQzN3w4FOBAlN6BY= -github.com/tg123/go-htpasswd v1.2.2/go.mod h1:FcIrK0J+6zptgVwK1JDlqyajW/1B4PtuJ/FLWl7nx8A= +github.com/tg123/go-htpasswd v1.2.3 h1:ALR6ZBIc2m9u70m+eAWUFt5p43ISbIvAvRFYzZPTOY8= +github.com/tg123/go-htpasswd v1.2.3/go.mod h1:FcIrK0J+6zptgVwK1JDlqyajW/1B4PtuJ/FLWl7nx8A= github.com/unrolled/render v1.7.0/go.mod h1:LwQSeDhjml8NLjIO9GJO1/1qpFJxtfVIpzxXKjfVkoI= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= diff --git a/internal/storage/search.go b/internal/storage/search.go index c69a6a6120..1380798545 100644 --- a/internal/storage/search.go +++ b/internal/storage/search.go @@ -4,7 +4,9 @@ import ( "context" "database/sql" "encoding/json" + "fmt" "regexp" + "strconv" "strings" "time" @@ -355,6 +357,12 @@ func searchQueryBuilder(searchString, timezone string) *sqlf.Stmt { } else { q.Where(`m.ID IN (SELECT DISTINCT mt.ID FROM ` + tenant("message_tags") + ` mt JOIN tags t ON mt.TagID = t.ID)`) } + } else if lw == "has:inline" || lw == "has:inlines" { + if exclude { + q.Where("Inline = 0") + } else { + q.Where("Inline > 0") + } } else if lw == "has:attachment" || lw == "has:attachments" { if exclude { q.Where("Attachments = 0") @@ -391,6 +399,22 @@ func searchQueryBuilder(searchString, timezone string) *sqlf.Stmt { } } } + } else if strings.HasPrefix(lw, "larger:") && sizeToBytes(cleanString(w[7:])) > 0 { + w = cleanString(w[7:]) + size := sizeToBytes(w) + if exclude { + q.Where("Size < ?", size) + } else { + q.Where("Size > ?", size) + } + } else if strings.HasPrefix(lw, "smaller:") && sizeToBytes(cleanString(w[8:])) > 0 { + w = cleanString(w[8:]) + size := sizeToBytes(w) + if exclude { + q.Where("Size > ?", size) + } else { + q.Where("Size < ?", size) + } } else { // search text if exclude { @@ -403,3 +427,39 @@ func searchQueryBuilder(searchString, timezone string) *sqlf.Stmt { return q } + +// Simple function to return a size in bytes, eg 2kb, 4MB or 1.5m. +// +// K, k, Kb, KB, kB and kb are treated as Kilobytes. +// M, m, Mb, MB and mb are treated as Megabytes. +func sizeToBytes(v string) int64 { + v = strings.ToLower(v) + re := regexp.MustCompile(`^(\d+)(\.\d+)?\s?([a-z]{1,2})?$`) + + m := re.FindAllStringSubmatch(v, -1) + if len(m) == 0 { + return 0 + } + + val := fmt.Sprintf("%s%s", m[0][1], m[0][2]) + unit := m[0][3] + + i, err := strconv.ParseFloat(strings.TrimSpace(val), 64) + if err != nil { + return 0 + } + + if unit == "" { + return int64(i) + } + + if unit == "k" || unit == "kb" { + return int64(i * 1024) + } + + if unit == "m" || unit == "mb" { + return int64(i * 1024 * 1024) + } + + return 0 +} diff --git a/internal/storage/search_test.go b/internal/storage/search_test.go index 100786ad84..affd1a6faf 100644 --- a/internal/storage/search_test.go +++ b/internal/storage/search_test.go @@ -201,3 +201,25 @@ func TestEscPercentChar(t *testing.T) { assertEqual(t, res, expected, "no match") } } + +func TestSizeToBytes(t *testing.T) { + tests := map[string]int64{} + tests["1m"] = 1048576 + tests["1mb"] = 1048576 + tests["1 M"] = 1048576 + tests["1 MB"] = 1048576 + tests["1k"] = 1024 + tests["1kb"] = 1024 + tests["1 K"] = 1024 + tests["1 kB"] = 1024 + tests["1.5M"] = 1572864 + tests["1234567890"] = 1234567890 + tests["invalid"] = 0 + tests["1.2.3"] = 0 + tests["1.2.3M"] = 0 + + for search, expected := range tests { + res := sizeToBytes(search) + assertEqual(t, res, expected, "size does not match") + } +} diff --git a/server/ui-src/components/message/Message.vue b/server/ui-src/components/message/Message.vue index 159a043c36..edb7ea7922 100644 --- a/server/ui-src/components/message/Message.vue +++ b/server/ui-src/components/message/Message.vue @@ -457,11 +457,20 @@ export default { -
-
- - Attachments - ({{ allAttachments(message).length }}) +
+
+ + + Inline images + ({{ message.Inline.length }})
diff --git a/server/ui-src/views/MessageView.vue b/server/ui-src/views/MessageView.vue index d8040d3b7c..50d9c12b6c 100644 --- a/server/ui-src/views/MessageView.vue +++ b/server/ui-src/views/MessageView.vue @@ -504,16 +504,41 @@ export default { Text body -