diff --git a/.chglog/CHANGELOG.tpl.md b/.chglog/CHANGELOG.tpl.md new file mode 100755 index 0000000..5abaff8 --- /dev/null +++ b/.chglog/CHANGELOG.tpl.md @@ -0,0 +1,48 @@ +# Changelog + +Notable changes to Mailpit will be documented in this file. + + +{{ if .Versions -}} +{{ if .Unreleased.CommitGroups -}} +## [Unreleased] + +{{ if .Unreleased.CommitGroups -}} +{{ range .Unreleased.CommitGroups -}} +### {{ .Title }} +{{ range .Commits -}} +- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} +{{ end }} +{{ end }} +{{ end -}} +{{ end -}} +{{ end -}} + +{{ range .Versions }} +{{- if .CommitGroups -}} +## {{ .Tag.Name }} + +{{ range .CommitGroups -}} +### {{ .Title }} +{{ range .Commits -}} +- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} +{{ end }} +{{ end }} +{{ end -}} + +{{- if .MergeCommits -}} +### Pull Requests +{{ range .MergeCommits -}} +- {{ .Header }} +{{ end }} +{{ end -}} + +{{- if .NoteGroups -}} +{{ range .NoteGroups -}} +### {{ .Title }} +{{ range .Notes }} +{{ .Body }} +{{ end }} +{{ end -}} +{{ end -}} +{{ end -}} diff --git a/.chglog/RELEASE.tpl.md b/.chglog/RELEASE.tpl.md new file mode 100755 index 0000000..a8ce8ae --- /dev/null +++ b/.chglog/RELEASE.tpl.md @@ -0,0 +1,12 @@ +{{ if .Versions -}} +{{ range .Versions }} +{{- if .CommitGroups -}} +{{ range .CommitGroups -}} +### {{ .Title }} +{{ range .Commits -}} +- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} +{{ end }} +{{ end -}} +{{ end -}} +{{ end -}} +{{ end -}} diff --git a/.chglog/config.yml b/.chglog/config.yml new file mode 100755 index 0000000..65597c4 --- /dev/null +++ b/.chglog/config.yml @@ -0,0 +1,28 @@ +style: github +template: CHANGELOG.tpl.md +info: + title: CHANGELOG + repository_url: https://github.com/axllent/mailpit +options: + commits: + # filters: + # Type: + # - feat + # - fix + # - perf + # - refactor + commit_groups: + title_maps: + feature: Feature + fix: Fix + # perf: Performance Improvements + # refactor: Code Refactoring + header: + pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$" + pattern_maps: + - Type + - Scope + - Subject + notes: + keywords: + - BREAKING CHANGE diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 839d03f..5dfbd6a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,6 +16,14 @@ jobs: with: go-version: ${{ matrix.go-version }} - uses: actions/checkout@v3 + - uses: actions/cache@v3 + with: + path: | + ~/.cache/go-build + ~/go + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- - run: go test ./storage -v - run: go test ./storage -bench=. diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8a8ec..c816416 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,34 @@ # Changelog -## [0.0.3] - -- Bugfix: Update to clover-v2.0.0-alpha.2 to fix sorting +Notable changes to Mailpit will be documented in this file. -## [0.0.2] +## 0.0.4 -- Unread message statistics & updates +### Bugfix +- Update to clover-v2.0.0-alpha.2 to fix sorting + +### Tests +- Add search tests + +### UI +- Add date to console log +- Add space in To fields +- Cater for messages without From email address +- Minor UI & logging changes +- Add space in To fields +- cater for messages without From email address -## [0.0.1-beta] +## 0.0.3 + +### Bugfix +- Update to clover-v2.0.0-alpha.2 to fix sorting + + +## 0.0.2 + +### Feature +- Unread statistics + -- First release diff --git a/logger/logger.go b/logger/logger.go index 1133213..aa0e30f 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -28,7 +28,7 @@ func Log() *logrus.Logger { log.Out = os.Stdout log.SetFormatter(&logrus.TextFormatter{ FullTimestamp: true, - TimestampFormat: "15:04:05", + TimestampFormat: "2006/01/02 15:04:05", ForceColors: true, }) } diff --git a/server/ui-src/App.vue b/server/ui-src/App.vue index 7de51a1..139076e 100644 --- a/server/ui-src/App.vue +++ b/server/ui-src/App.vue @@ -408,7 +408,7 @@ export default { - diff --git a/server/ui-src/templates/Message.vue b/server/ui-src/templates/Message.vue index bee7fd1..77b2f3a 100644 --- a/server/ui-src/templates/Message.vue +++ b/server/ui-src/templates/Message.vue @@ -80,7 +80,8 @@ export default { From - {{ message.From.Name + " <" + message.From.Address +">" }} + {{ message.From.Name + " " }} + <{{ message.From.Address }}> [ Unknown ] diff --git a/storage/database.go b/storage/database.go index 78845b2..edc299a 100644 --- a/storage/database.go +++ b/storage/database.go @@ -185,6 +185,8 @@ func Store(mailbox string, b []byte) (string, error) { fromData := addressToSlice(env, "From") if len(fromData) > 0 { from = fromData[0] + } else if env.GetHeader("From") != "" { + from = &mail.Address{Name: env.GetHeader("From")} } obj := CloverStore{ @@ -224,7 +226,7 @@ func Store(mailbox string, b []byte) (string, error) { count++ if count%100 == 0 { - logger.Log().Infof("%d messages added (%s per 100)", count, time.Since(per100start)) + logger.Log().Infof("100 messages added in %s", time.Since(per100start)) per100start = time.Now() } @@ -311,7 +313,7 @@ func List(mailbox string, start, limit int) ([]data.Summary, error) { // Search returns a summary of items mathing a search. It searched the SearchText field. func Search(mailbox, search string, start, limit int) ([]data.Summary, error) { - sq := fmt.Sprintf("(?i)%s", regexp.QuoteMeta(search)) + sq := fmt.Sprintf("(?i)%s", cleanString(regexp.QuoteMeta(search))) q, err := db.FindAll(clover.NewQuery(mailbox). Skip(start). Limit(limit). @@ -393,6 +395,8 @@ func GetMessage(mailbox, id string) (*data.Message, error) { fromData := addressToSlice(env, "From") if len(fromData) > 0 { from = fromData[0] + } else if env.GetHeader("From") != "" { + from = &mail.Address{Name: env.GetHeader("From")} } date, err := env.Date() diff --git a/storage/database_test.go b/storage/database_test.go index 9d473bc..c2152ff 100644 --- a/storage/database_test.go +++ b/storage/database_test.go @@ -1,12 +1,15 @@ package storage import ( + "bytes" "fmt" "io/ioutil" + "math/rand" "testing" "time" "github.com/axllent/mailpit/config" + "github.com/jhillyerd/enmime" ) var ( @@ -123,6 +126,78 @@ func TestRetrieveMimeEmail(t *testing.T) { assertEqual(t, len(attachmentData.Content), msg.Attachments[0].Size, "attachment size does not match") inlineData, err := GetAttachmentPart(DefaultMailbox, id, msg.Inline[0].PartID) assertEqual(t, len(inlineData.Content), msg.Inline[0].Size, "inline attachment size does not match") + + db.Close() +} + +func TestSearch(t *testing.T) { + setup() + + for i := 0; i < 1000; 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 .", 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(DefaultMailbox, buf.Bytes()); err != nil { + t.Log("error ", err) + t.Fail() + } + } + + for i := 1; i < 101; i++ { + // search a random something that will return a single result + searchIndx := rand.Intn(4) + 1 + var search string + switch searchIndx { + 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(DefaultMailbox, search, 0, 200) + 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 rsults + summaries, err := Search(DefaultMailbox, "This is the email body", 0, 200) + if err != nil { + t.Log("error ", err) + t.Fail() + } + assertEqual(t, len(summaries), 200, "200 search results expected") + + db.Close() } func BenchmarkImportText(b *testing.B) { diff --git a/storage/utils.go b/storage/utils.go index 8cfca72..f2bbb18 100644 --- a/storage/utils.go +++ b/storage/utils.go @@ -42,17 +42,20 @@ func createSearchText(env *enmime.Envelope) string { b.WriteString(a.FileName + " ") } - d := b.String() - - // remove/replace new lines - re := regexp.MustCompile(`(\r?\n|\t|>|<|"|:|\,|;)`) - d = re.ReplaceAllString(d, " ") - // remove duplicate whitespace and trim - d = strings.ToLower(strings.Join(strings.Fields(strings.TrimSpace(d)), " ")) + d := cleanString(b.String()) return d } +// cleanString removed unwanted characters from stored search text and search queries +func cleanString(str string) string { + // remove/replace new lines + re := regexp.MustCompile(`(\r?\n|\t|>|<|"|:|\,|;)`) + str = re.ReplaceAllString(str, " ") + // remove duplicate whitespace and trim + return strings.ToLower(strings.Join(strings.Fields(strings.TrimSpace(str)), " ")) +} + // Auto-prune runs every 5 minutes to automatically delete oldest messages // if total is greater than the threshold func pruneCron() {