diff --git a/internal/storage/database.go b/internal/storage/database.go index 7558ffb..b3c0189 100644 --- a/internal/storage/database.go +++ b/internal/storage/database.go @@ -146,15 +146,15 @@ func Store(body *[]byte) (string, error) { from = &mail.Address{Name: env.GetHeader("From")} } - messageID := strings.Trim(env.Root.Header.Get("Message-ID"), "<>") - obj := DBMailSummary{ - From: from, - To: addressToSlice(env, "To"), - Cc: addressToSlice(env, "Cc"), - Bcc: addressToSlice(env, "Bcc"), + From: from, + To: addressToSlice(env, "To"), + Cc: addressToSlice(env, "Cc"), + Bcc: addressToSlice(env, "Bcc"), + ReplyTo: addressToSlice(env, "Reply-To"), } + messageID := strings.Trim(env.Root.Header.Get("Message-ID"), "<>") created := time.Now() // use message date instead of created date @@ -294,6 +294,10 @@ func List(start, limit int) ([]MessageSummary, error) { em.Attachments = attachments em.Read = read == 1 em.Snippet = snippet + // artificially generate ReplyTo if legacy data is missing Reply-To field + if em.ReplyTo == nil { + em.ReplyTo = []*mail.Address{} + } results = append(results, em) }); err != nil { diff --git a/internal/storage/reindex.go b/internal/storage/reindex.go index 282f857..f099bb2 100644 --- a/internal/storage/reindex.go +++ b/internal/storage/reindex.go @@ -4,6 +4,8 @@ import ( "bytes" "context" "database/sql" + "encoding/json" + "net/mail" "os" "github.com/axllent/mailpit/internal/logger" @@ -43,6 +45,7 @@ func ReindexAll() { ID string SearchText string Snippet string + Metadata string } for _, ids := range chunks { @@ -63,6 +66,28 @@ func ReindexAll() { continue } + from := &mail.Address{} + fromJSON := addressToSlice(env, "From") + if len(fromJSON) > 0 { + from = fromJSON[0] + } else if env.GetHeader("From") != "" { + from = &mail.Address{Name: env.GetHeader("From")} + } + + obj := DBMailSummary{ + From: from, + To: addressToSlice(env, "To"), + Cc: addressToSlice(env, "Cc"), + Bcc: addressToSlice(env, "Bcc"), + ReplyTo: addressToSlice(env, "Reply-To"), + } + + MetadataJSON, err := json.Marshal(obj) + if err != nil { + logger.Log().Errorf("[message] %s", err.Error()) + continue + } + searchText := createSearchText(env) snippet := tools.CreateSnippet(env.Text, env.HTML) @@ -70,6 +95,7 @@ func ReindexAll() { u.ID = id u.SearchText = searchText u.Snippet = snippet + u.Metadata = string(MetadataJSON) updates = append(updates, u) } @@ -86,7 +112,7 @@ func ReindexAll() { // insert mail summary data for _, u := range updates { - _, err = tx.Exec("UPDATE mailbox SET SearchText = ?, Snippet = ? WHERE ID = ?", u.SearchText, u.Snippet, u.ID) + _, err = tx.Exec("UPDATE mailbox SET SearchText = ?, Snippet = ?, Metadata = ? WHERE ID = ?", u.SearchText, u.Snippet, u.Metadata, u.ID) if err != nil { logger.Log().Errorf("[db] %s", err.Error()) continue diff --git a/internal/storage/search.go b/internal/storage/search.go index 06b65ef..d06591d 100644 --- a/internal/storage/search.go +++ b/internal/storage/search.go @@ -42,7 +42,7 @@ func Search(search string, start, limit int) ([]MessageSummary, int, error) { var ignore string em := MessageSummary{} - if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &snippet, &ignore, &ignore, &ignore, &ignore); err != nil { + if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &snippet, &ignore, &ignore, &ignore, &ignore, &ignore); err != nil { logger.Log().Errorf("[db] %s", err.Error()) return } @@ -114,7 +114,7 @@ func DeleteSearch(search string) error { var snippet string var ignore string - if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &snippet, &ignore, &ignore, &ignore, &ignore); err != nil { + if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &snippet, &ignore, &ignore, &ignore, &ignore, &ignore); err != nil { logger.Log().Errorf("[db] %s", err.Error()) return } @@ -214,7 +214,8 @@ func searchQueryBuilder(searchString string) *sqlf.Stmt { IFNULL(json_extract(Metadata, '$.To'), '{}') as ToJSON, IFNULL(json_extract(Metadata, '$.From'), '{}') as FromJSON, IFNULL(json_extract(Metadata, '$.Cc'), '{}') as CcJSON, - IFNULL(json_extract(Metadata, '$.Bcc'), '{}') as BccJSON + IFNULL(json_extract(Metadata, '$.Bcc'), '{}') as BccJSON, + IFNULL(json_extract(Metadata, '$.ReplyTo'), '{}') as ReplyToJSON `). OrderBy("m.Created DESC") @@ -275,6 +276,15 @@ func searchQueryBuilder(searchString string) *sqlf.Stmt { q.Where("BccJSON LIKE ?", "%"+escPercentChar(w)+"%") } } + } else if strings.HasPrefix(lw, "reply-to:") { + w = cleanString(w[9:]) + if w != "" { + if exclude { + q.Where("ReplyToJSON NOT LIKE ?", "%"+escPercentChar(w)+"%") + } else { + q.Where("ReplyToJSON LIKE ?", "%"+escPercentChar(w)+"%") + } + } } else if strings.HasPrefix(lw, "subject:") { w = w[8:] if w != "" { diff --git a/internal/storage/search_test.go b/internal/storage/search_test.go index 9ce8ccd..744c4b0 100644 --- a/internal/storage/search_test.go +++ b/internal/storage/search_test.go @@ -17,9 +17,13 @@ func TestSearch(t *testing.T) { for i := 0; i < testRuns; i++ { msg := enmime.Builder(). From(fmt.Sprintf("From %d", i), fmt.Sprintf("from-%d@example.com", i)). + CC(fmt.Sprintf("CC %d", i), fmt.Sprintf("cc-%d@example.com", i)). + CC(fmt.Sprintf("CC2 %d", i), fmt.Sprintf("cc2-%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)) + To(fmt.Sprintf("To %d", i), fmt.Sprintf("to-%d@example.com", i)). + To(fmt.Sprintf("To2 %d", i), fmt.Sprintf("to2-%d@example.com", i)). + ReplyTo(fmt.Sprintf("Reply To %d", i), fmt.Sprintf("reply-to-%d@example.com", i)) env, err := msg.Build() if err != nil { @@ -44,18 +48,26 @@ func TestSearch(t *testing.T) { for i := 1; i < 51; i++ { // search a random something that will return a single result - searchIdx := rand.Intn(4) + 1 - var search string - switch searchIdx { - 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) + uniqueSearches := []string{ + fmt.Sprintf("from-%d@example.com", i), + fmt.Sprintf("from:from-%d@example.com", i), + fmt.Sprintf("to-%d@example.com", i), + fmt.Sprintf("to:to-%d@example.com", i), + fmt.Sprintf("to2-%d@example.com", i), + fmt.Sprintf("to:to2-%d@example.com", i), + fmt.Sprintf("cc-%d@example.com", i), + fmt.Sprintf("cc:cc-%d@example.com", i), + fmt.Sprintf("cc2-%d@example.com", i), + fmt.Sprintf("cc:cc2-%d@example.com", i), + fmt.Sprintf("reply-to-%d@example.com", i), + fmt.Sprintf("reply-to:\"reply-to-%d@example.com\"", i), + fmt.Sprintf("\"Subject line %d end\"", i), + fmt.Sprintf("subject:\"Subject line %d end\"", i), + fmt.Sprintf("\"the email body %d jdsauk dwqmdqw\"", i), } + searchIdx := rand.Intn(len(uniqueSearches)) + + search := uniqueSearches[searchIdx] summaries, _, err := Search(search, 0, 100) if err != nil { @@ -63,7 +75,7 @@ func TestSearch(t *testing.T) { t.Fail() } - assertEqual(t, len(summaries), 1, "1 search result expected") + assertEqual(t, len(summaries), 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") diff --git a/internal/storage/structs.go b/internal/storage/structs.go index ec92ec9..8503d73 100644 --- a/internal/storage/structs.go +++ b/internal/storage/structs.go @@ -82,6 +82,8 @@ type MessageSummary struct { Cc []*mail.Address // Bcc addresses Bcc []*mail.Address + // Reply-To address + ReplyTo []*mail.Address // Email subject Subject string // Created time @@ -105,10 +107,11 @@ type MailboxStats struct { // DBMailSummary struct for storing mail summary type DBMailSummary struct { - From *mail.Address - To []*mail.Address - Cc []*mail.Address - Bcc []*mail.Address + From *mail.Address + To []*mail.Address + Cc []*mail.Address + Bcc []*mail.Address + ReplyTo []*mail.Address } // AttachmentSummary returns a summary of the attachment without any binary data diff --git a/server/ui/api/v1/swagger.json b/server/ui/api/v1/swagger.json index aef41e8..8b99aab 100644 --- a/server/ui/api/v1/swagger.json +++ b/server/ui/api/v1/swagger.json @@ -1259,6 +1259,13 @@ "description": "Read status", "type": "boolean" }, + "ReplyTo": { + "description": "Reply-To address", + "type": "array", + "items": { + "$ref": "#/definitions/Address" + } + }, "Size": { "description": "Message size in bytes (total)", "type": "integer",