Skip to content

Commit 0f24496

Browse files
committed
Merge branch 'release/v1.21.1'
2 parents 10c20dd + 2743e2e commit 0f24496

File tree

7 files changed

+138
-11
lines changed

7 files changed

+138
-11
lines changed

CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
Notable changes to Mailpit will be documented in this file.
44

5+
## [v1.21.1]
6+
7+
### Feature
8+
- Add ability to search by size smaller or larger than a value (eg: `larger:1M` / `smaller:2.5M`)
9+
- Add ability to search for messages containing inline images (`has:inline`)
10+
11+
### Chore
12+
- Update Go dependencies
13+
- Separate attachments and inline images in download nav and badges ([#379](https://github.com/axllent/mailpit/issues/379))
14+
15+
516
## [v1.21.0]
617

718
### Feature

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ require (
2121
github.com/sirupsen/logrus v1.9.3
2222
github.com/spf13/cobra v1.8.1
2323
github.com/spf13/pflag v1.0.5
24-
github.com/tg123/go-htpasswd v1.2.2
24+
github.com/tg123/go-htpasswd v1.2.3
2525
github.com/vanng822/go-premailer v1.22.0
2626
golang.org/x/net v0.30.0
2727
golang.org/x/text v0.19.0

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
111111
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
112112
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
113113
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
114-
github.com/tg123/go-htpasswd v1.2.2 h1:tmNccDsQ+wYsoRfiONzIhDm5OkVHQzN3w4FOBAlN6BY=
115-
github.com/tg123/go-htpasswd v1.2.2/go.mod h1:FcIrK0J+6zptgVwK1JDlqyajW/1B4PtuJ/FLWl7nx8A=
114+
github.com/tg123/go-htpasswd v1.2.3 h1:ALR6ZBIc2m9u70m+eAWUFt5p43ISbIvAvRFYzZPTOY8=
115+
github.com/tg123/go-htpasswd v1.2.3/go.mod h1:FcIrK0J+6zptgVwK1JDlqyajW/1B4PtuJ/FLWl7nx8A=
116116
github.com/unrolled/render v1.7.0/go.mod h1:LwQSeDhjml8NLjIO9GJO1/1qpFJxtfVIpzxXKjfVkoI=
117117
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
118118
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=

internal/storage/search.go

+60
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"context"
55
"database/sql"
66
"encoding/json"
7+
"fmt"
78
"regexp"
9+
"strconv"
810
"strings"
911
"time"
1012

@@ -355,6 +357,12 @@ func searchQueryBuilder(searchString, timezone string) *sqlf.Stmt {
355357
} else {
356358
q.Where(`m.ID IN (SELECT DISTINCT mt.ID FROM ` + tenant("message_tags") + ` mt JOIN tags t ON mt.TagID = t.ID)`)
357359
}
360+
} else if lw == "has:inline" || lw == "has:inlines" {
361+
if exclude {
362+
q.Where("Inline = 0")
363+
} else {
364+
q.Where("Inline > 0")
365+
}
358366
} else if lw == "has:attachment" || lw == "has:attachments" {
359367
if exclude {
360368
q.Where("Attachments = 0")
@@ -391,6 +399,22 @@ func searchQueryBuilder(searchString, timezone string) *sqlf.Stmt {
391399
}
392400
}
393401
}
402+
} else if strings.HasPrefix(lw, "larger:") && sizeToBytes(cleanString(w[7:])) > 0 {
403+
w = cleanString(w[7:])
404+
size := sizeToBytes(w)
405+
if exclude {
406+
q.Where("Size < ?", size)
407+
} else {
408+
q.Where("Size > ?", size)
409+
}
410+
} else if strings.HasPrefix(lw, "smaller:") && sizeToBytes(cleanString(w[8:])) > 0 {
411+
w = cleanString(w[8:])
412+
size := sizeToBytes(w)
413+
if exclude {
414+
q.Where("Size > ?", size)
415+
} else {
416+
q.Where("Size < ?", size)
417+
}
394418
} else {
395419
// search text
396420
if exclude {
@@ -403,3 +427,39 @@ func searchQueryBuilder(searchString, timezone string) *sqlf.Stmt {
403427

404428
return q
405429
}
430+
431+
// Simple function to return a size in bytes, eg 2kb, 4MB or 1.5m.
432+
//
433+
// K, k, Kb, KB, kB and kb are treated as Kilobytes.
434+
// M, m, Mb, MB and mb are treated as Megabytes.
435+
func sizeToBytes(v string) int64 {
436+
v = strings.ToLower(v)
437+
re := regexp.MustCompile(`^(\d+)(\.\d+)?\s?([a-z]{1,2})?$`)
438+
439+
m := re.FindAllStringSubmatch(v, -1)
440+
if len(m) == 0 {
441+
return 0
442+
}
443+
444+
val := fmt.Sprintf("%s%s", m[0][1], m[0][2])
445+
unit := m[0][3]
446+
447+
i, err := strconv.ParseFloat(strings.TrimSpace(val), 64)
448+
if err != nil {
449+
return 0
450+
}
451+
452+
if unit == "" {
453+
return int64(i)
454+
}
455+
456+
if unit == "k" || unit == "kb" {
457+
return int64(i * 1024)
458+
}
459+
460+
if unit == "m" || unit == "mb" {
461+
return int64(i * 1024 * 1024)
462+
}
463+
464+
return 0
465+
}

internal/storage/search_test.go

+22
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,25 @@ func TestEscPercentChar(t *testing.T) {
201201
assertEqual(t, res, expected, "no match")
202202
}
203203
}
204+
205+
func TestSizeToBytes(t *testing.T) {
206+
tests := map[string]int64{}
207+
tests["1m"] = 1048576
208+
tests["1mb"] = 1048576
209+
tests["1 M"] = 1048576
210+
tests["1 MB"] = 1048576
211+
tests["1k"] = 1024
212+
tests["1kb"] = 1024
213+
tests["1 K"] = 1024
214+
tests["1 kB"] = 1024
215+
tests["1.5M"] = 1572864
216+
tests["1234567890"] = 1234567890
217+
tests["invalid"] = 0
218+
tests["1.2.3"] = 0
219+
tests["1.2.3M"] = 0
220+
221+
for search, expected := range tests {
222+
res := sizeToBytes(search)
223+
assertEqual(t, res, expected, "size does not match")
224+
}
225+
}

server/ui-src/components/message/Message.vue

+14-5
Original file line numberDiff line numberDiff line change
@@ -457,11 +457,20 @@ export default {
457457
</tbody>
458458
</table>
459459
</div>
460-
<div class="col-md-auto d-none d-md-block text-end mt-md-3">
461-
<div class="mt-2 mt-md-0" v-if="allAttachments(message)">
462-
<span class="badge rounded-pill text-bg-secondary p-2">
463-
Attachment<span v-if="allAttachments(message).length > 1">s</span>
464-
({{ allAttachments(message).length }})
460+
<div class="col-md-auto d-none d-md-block text-end mt-md-3"
461+
v-if="message.Attachments && message.Attachments.length || message.Inline && message.Inline.length">
462+
<div class="mt-2 mt-md-0">
463+
<template v-if="message.Attachments.length">
464+
<span class="badge rounded-pill text-bg-secondary p-2 mb-2" title="Attachments in this message">
465+
Attachment<span v-if="message.Attachments.length > 1">s</span>
466+
({{ message.Attachments.length }})
467+
</span>
468+
<br>
469+
</template>
470+
<span class="badge rounded-pill text-bg-secondary p-2" v-if="message.Inline.length"
471+
title="Inline images in this message">
472+
Inline image<span v-if="message.Inline.length > 1">s</span>
473+
({{ message.Inline.length }})
465474
</span>
466475
</div>
467476
</div>

server/ui-src/views/MessageView.vue

+28-3
Original file line numberDiff line numberDiff line change
@@ -504,16 +504,41 @@ export default {
504504
Text body
505505
</button>
506506
</li>
507-
<template v-if="allAttachments(message).length">
507+
<template v-if="message.Attachments && message.Attachments.length">
508508
<li>
509509
<hr class="dropdown-divider">
510510
</li>
511511
<li>
512512
<h6 class="dropdown-header">
513-
Attachment<template v-if="allAttachments(message).length > 1">s</template>
513+
Attachments
514514
</h6>
515515
</li>
516-
<li v-for="part in allAttachments(message)">
516+
<li v-for="part in message.Attachments">
517+
<RouterLink :to="'/api/v1/message/' + message.ID + '/part/' + part.PartID"
518+
class="row m-0 dropdown-item d-flex" target="_blank"
519+
:title="part.FileName != '' ? part.FileName : '[ unknown ]'" style="min-width: 350px">
520+
<div class="col-auto p-0 pe-1">
521+
<i class="bi" :class="attachmentIcon(part)"></i>
522+
</div>
523+
<div class="col text-truncate p-0 pe-1">
524+
{{ part.FileName != '' ? part.FileName : '[ unknown ]' }}
525+
</div>
526+
<div class="col-auto text-muted small p-0">
527+
{{ getFileSize(part.Size) }}
528+
</div>
529+
</RouterLink>
530+
</li>
531+
</template>
532+
<template v-if="message.Inline && message.Inline.length">
533+
<li>
534+
<hr class="dropdown-divider">
535+
</li>
536+
<li>
537+
<h6 class="dropdown-header">
538+
Inline image<span v-if="message.Inline.length > 1">s</span>
539+
</h6>
540+
</li>
541+
<li v-for="part in message.Inline">
517542
<RouterLink :to="'/api/v1/message/' + message.ID + '/part/' + part.PartID"
518543
class="row m-0 dropdown-item d-flex" target="_blank"
519544
:title="part.FileName != '' ? part.FileName : '[ unknown ]'" style="min-width: 350px">

0 commit comments

Comments
 (0)