From f8efda01496fe2043170f92ebf35b5ff51b215cf Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Thu, 17 Oct 2024 22:30:58 +1300 Subject: [PATCH 1/5] Fix: SQL error deleting a tag while using tenant-id (#374) --- internal/storage/tags.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/storage/tags.go b/internal/storage/tags.go index 37a8c83df..4e2e72800 100644 --- a/internal/storage/tags.go +++ b/internal/storage/tags.go @@ -130,7 +130,7 @@ func addMessageTag(id, name string) (string, error) { func deleteMessageTag(id, name string) error { if _, err := sqlf.DeleteFrom(tenant("message_tags")). Where(tenant("message_tags.ID")+" = ?", id). - Where(tenant("message_tags.Key")+` IN (SELECT Key FROM `+tenant("message_tags")+` LEFT JOIN tags ON `+tenant("TagID")+"="+tenant("tags.ID")+` WHERE Name = ?)`, name). + Where(tenant("message_tags.Key")+` IN (SELECT Key FROM `+tenant("message_tags")+` LEFT JOIN tags ON TagID=tags.ID WHERE Name = ?)`, name). ExecAndClose(context.TODO(), db); err != nil { return err } From 882adeebe3f1d89d18585f6340f339edfb921ee4 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Thu, 17 Oct 2024 22:41:41 +1300 Subject: [PATCH 2/5] SQL error deleting a tag while using tenant-id (take 2) --- internal/storage/tags.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/storage/tags.go b/internal/storage/tags.go index 4e2e72800..9605bc5ba 100644 --- a/internal/storage/tags.go +++ b/internal/storage/tags.go @@ -130,7 +130,7 @@ func addMessageTag(id, name string) (string, error) { func deleteMessageTag(id, name string) error { if _, err := sqlf.DeleteFrom(tenant("message_tags")). Where(tenant("message_tags.ID")+" = ?", id). - Where(tenant("message_tags.Key")+` IN (SELECT Key FROM `+tenant("message_tags")+` LEFT JOIN tags ON TagID=tags.ID WHERE Name = ?)`, name). + Where(tenant("message_tags.Key")+` IN (SELECT Key FROM `+tenant("message_tags")+` LEFT JOIN `+tenant("tags")+` ON TagID=`+tenant("tags.ID")+` WHERE Name = ?)`, name). ExecAndClose(context.TODO(), db); err != nil { return err } From 405babda7b29763d4d83ab70825f79aab6cb1482 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Fri, 18 Oct 2024 17:55:46 +1300 Subject: [PATCH 3/5] Testing: Add tenantIDs to tests --- config/config.go | 21 +++- internal/storage/messages_test.go | 185 +++++++++++++++------------ internal/storage/search_test.go | 203 ++++++++++++++++-------------- internal/storage/tags_test.go | 201 +++++++++++++++-------------- internal/storage/testing.go | 3 +- 5 files changed, 343 insertions(+), 270 deletions(-) diff --git a/config/config.go b/config/config.go index 60eeea8ec..c9a2d7c7a 100644 --- a/config/config.go +++ b/config/config.go @@ -233,14 +233,9 @@ func VerifyConfig() error { return err } - TenantID = tools.Normalize(TenantID) + TenantID = DBTenantID(TenantID) if TenantID != "" { logger.Log().Infof("[db] using tenant \"%s\"", TenantID) - re := regexp.MustCompile(`[^a-zA-Z0-9\_]`) - TenantID = re.ReplaceAllString(TenantID, "_") - if !strings.HasSuffix(TenantID, "_") { - TenantID = TenantID + "_" - } } re := regexp.MustCompile(`.*:\d+$`) @@ -632,3 +627,17 @@ func isValidURL(s string) bool { return strings.HasPrefix(u.Scheme, "http") } + +// DBTenantID converts a tenant ID to a DB-friendly value if set +func DBTenantID(s string) string { + s = tools.Normalize(s) + if s != "" { + re := regexp.MustCompile(`[^a-zA-Z0-9\_]`) + s = re.ReplaceAllString(s, "_") + if !strings.HasSuffix(s, "_") { + s = s + "_" + } + } + + return s +} diff --git a/internal/storage/messages_test.go b/internal/storage/messages_test.go index 95f3525e1..8e9c6e281 100644 --- a/internal/storage/messages_test.go +++ b/internal/storage/messages_test.go @@ -3,10 +3,12 @@ package storage import ( "testing" "time" + + "github.com/axllent/mailpit/config" ) func TestTextEmailInserts(t *testing.T) { - setup() + setup("") defer Close() t.Log("Testing text email storage") @@ -38,113 +40,140 @@ func TestTextEmailInserts(t *testing.T) { } func TestMimeEmailInserts(t *testing.T) { - setup() - defer Close() + for _, tenantID := range []string{"", "MyServer 3", "host.example.com"} { + tenantID = config.DBTenantID(tenantID) - t.Log("Testing mime email storage") + setup(tenantID) - start := time.Now() + if tenantID == "" { + t.Log("Testing mime email storage") + } else { + t.Logf("Testing mime email storage (tenant %s)", tenantID) + } - for i := 0; i < testRuns; i++ { - if _, err := Store(&testMimeEmail); err != nil { + start := time.Now() + + for i := 0; i < testRuns; i++ { + if _, err := Store(&testMimeEmail); err != nil { + t.Log("error ", err) + t.Fail() + } + } + + assertEqual(t, CountTotal(), float64(testRuns), "Incorrect number of mime emails stored") + + t.Logf("Inserted %d text emails in %s", testRuns, time.Since(start)) + + delStart := time.Now() + if err := DeleteAllMessages(); err != nil { t.Log("error ", err) t.Fail() } - } - assertEqual(t, CountTotal(), float64(testRuns), "Incorrect number of mime emails stored") + assertEqual(t, CountTotal(), float64(0), "incorrect number of mime emails deleted") - t.Logf("Inserted %d text emails in %s", testRuns, time.Since(start)) + t.Logf("Deleted %d mime emails in %s", testRuns, time.Since(delStart)) - delStart := time.Now() - if err := DeleteAllMessages(); err != nil { - t.Log("error ", err) - t.Fail() + Close() } - - assertEqual(t, CountTotal(), float64(0), "incorrect number of mime emails deleted") - - t.Logf("Deleted %d mime emails in %s", testRuns, time.Since(delStart)) } func TestRetrieveMimeEmail(t *testing.T) { - setup() - defer Close() + for _, tenantID := range []string{"", "MyServer 3", "host.example.com"} { + tenantID = config.DBTenantID(tenantID) - t.Log("Testing mime email retrieval") + setup(tenantID) - id, err := Store(&testMimeEmail) - if err != nil { - t.Log("error ", err) - t.Fail() - } + if tenantID == "" { + t.Log("Testing mime email retrieval") + } else { + t.Logf("Testing mime email retrieval (tenant %s)", tenantID) + } - msg, err := GetMessage(id) - if err != nil { - t.Log("error ", err) - t.Fail() - } + id, err := Store(&testMimeEmail) + if err != nil { + t.Log("error ", err) + t.Fail() + } - assertEqual(t, msg.From.Name, "Sender Smith", "\"From\" name does not match") - assertEqual(t, msg.From.Address, "sender2@example.com", "\"From\" address does not match") - assertEqual(t, msg.Subject, "inline + attachment", "subject does not match") - assertEqual(t, len(msg.To), 1, "incorrect number of recipients") - assertEqual(t, msg.To[0].Name, "Recipient Ross", "\"To\" name does not match") - assertEqual(t, msg.To[0].Address, "recipient2@example.com", "\"To\" address does not match") - assertEqual(t, len(msg.Attachments), 1, "incorrect number of attachments") - assertEqual(t, msg.Attachments[0].FileName, "Sample PDF.pdf", "attachment filename does not match") - assertEqual(t, len(msg.Inline), 1, "incorrect number of inline attachments") - assertEqual(t, msg.Inline[0].FileName, "inline-image.jpg", "inline attachment filename does not match") - - attachmentData, err := GetAttachmentPart(id, msg.Attachments[0].PartID) - if err != nil { - t.Log("error ", err) - t.Fail() - } - assertEqual(t, float64(len(attachmentData.Content)), msg.Attachments[0].Size, "attachment size does not match") + msg, err := GetMessage(id) + if err != nil { + t.Log("error ", err) + t.Fail() + } - inlineData, err := GetAttachmentPart(id, msg.Inline[0].PartID) - if err != nil { - t.Log("error ", err) - t.Fail() + assertEqual(t, msg.From.Name, "Sender Smith", "\"From\" name does not match") + assertEqual(t, msg.From.Address, "sender2@example.com", "\"From\" address does not match") + assertEqual(t, msg.Subject, "inline + attachment", "subject does not match") + assertEqual(t, len(msg.To), 1, "incorrect number of recipients") + assertEqual(t, msg.To[0].Name, "Recipient Ross", "\"To\" name does not match") + assertEqual(t, msg.To[0].Address, "recipient2@example.com", "\"To\" address does not match") + assertEqual(t, len(msg.Attachments), 1, "incorrect number of attachments") + assertEqual(t, msg.Attachments[0].FileName, "Sample PDF.pdf", "attachment filename does not match") + assertEqual(t, len(msg.Inline), 1, "incorrect number of inline attachments") + assertEqual(t, msg.Inline[0].FileName, "inline-image.jpg", "inline attachment filename does not match") + + attachmentData, err := GetAttachmentPart(id, msg.Attachments[0].PartID) + if err != nil { + t.Log("error ", err) + t.Fail() + } + assertEqual(t, float64(len(attachmentData.Content)), msg.Attachments[0].Size, "attachment size does not match") + + inlineData, err := GetAttachmentPart(id, msg.Inline[0].PartID) + if err != nil { + t.Log("error ", err) + t.Fail() + } + assertEqual(t, float64(len(inlineData.Content)), msg.Inline[0].Size, "inline attachment size does not match") + + Close() } - assertEqual(t, float64(len(inlineData.Content)), msg.Inline[0].Size, "inline attachment size does not match") } func TestMessageSummary(t *testing.T) { - setup() - defer Close() + for _, tenantID := range []string{"", "MyServer 3", "host.example.com"} { + tenantID = config.DBTenantID(tenantID) - t.Log("Testing message summary") + setup(tenantID) - if _, err := Store(&testMimeEmail); err != nil { - t.Log("error ", err) - t.Fail() - } + if tenantID == "" { + t.Log("Testing message summary") + } else { + t.Logf("Testing message summary (tenant %s)", tenantID) + } - summaries, err := List(0, 0, 1) - if err != nil { - t.Log("error ", err) - t.Fail() - } + if _, err := Store(&testMimeEmail); err != nil { + t.Log("error ", err) + t.Fail() + } + + summaries, err := List(0, 0, 1) + if err != nil { + t.Log("error ", err) + t.Fail() + } - assertEqual(t, len(summaries), 1, "Expected 1 result") + assertEqual(t, len(summaries), 1, "Expected 1 result") - msg := summaries[0] + msg := summaries[0] - assertEqual(t, msg.From.Name, "Sender Smith", "\"From\" name does not match") - assertEqual(t, msg.From.Address, "sender2@example.com", "\"From\" address does not match") - assertEqual(t, msg.Subject, "inline + attachment", "subject does not match") - assertEqual(t, len(msg.To), 1, "incorrect number of recipients") - assertEqual(t, msg.To[0].Name, "Recipient Ross", "\"To\" name does not match") - assertEqual(t, msg.To[0].Address, "recipient2@example.com", "\"To\" address does not match") - assertEqual(t, msg.Snippet, "Message with inline image and attachment:", "\"Snippet\" does does not match") - assertEqual(t, msg.Attachments, 1, "Expected 1 attachment") - assertEqual(t, msg.MessageID, "33af2ac1-c33d-9738-35e3-a6daf90bbd89@gmail.com", "\"MessageID\" does not match") + assertEqual(t, msg.From.Name, "Sender Smith", "\"From\" name does not match") + assertEqual(t, msg.From.Address, "sender2@example.com", "\"From\" address does not match") + assertEqual(t, msg.Subject, "inline + attachment", "subject does not match") + assertEqual(t, len(msg.To), 1, "incorrect number of recipients") + assertEqual(t, msg.To[0].Name, "Recipient Ross", "\"To\" name does not match") + assertEqual(t, msg.To[0].Address, "recipient2@example.com", "\"To\" address does not match") + assertEqual(t, msg.Snippet, "Message with inline image and attachment:", "\"Snippet\" does does not match") + assertEqual(t, msg.Attachments, 1, "Expected 1 attachment") + assertEqual(t, msg.MessageID, "33af2ac1-c33d-9738-35e3-a6daf90bbd89@gmail.com", "\"MessageID\" does not match") + + Close() + } } func BenchmarkImportText(b *testing.B) { - setup() + setup("") defer Close() for i := 0; i < b.N; i++ { @@ -156,7 +185,7 @@ func BenchmarkImportText(b *testing.B) { } func BenchmarkImportMime(b *testing.B) { - setup() + setup("") defer Close() for i := 0; i < b.N; i++ { diff --git a/internal/storage/search_test.go b/internal/storage/search_test.go index 74065a942..100786ad8 100644 --- a/internal/storage/search_test.go +++ b/internal/storage/search_test.go @@ -6,133 +6,154 @@ import ( "math/rand" "testing" + "github.com/axllent/mailpit/config" "github.com/jhillyerd/enmime" ) func TestSearch(t *testing.T) { - setup() - defer Close() + for _, tenantID := range []string{"", "MyServer 3", "host.example.com"} { + tenantID = config.DBTenantID(tenantID) - t.Log("Testing search") - 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("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 { - t.Log("error ", err) - t.Fail() - } + setup(tenantID) - buf := new(bytes.Buffer) + if tenantID == "" { + t.Log("Testing search") + } else { + t.Logf("Testing search (tenant %s)", tenantID) + } - if err := env.Encode(buf); err != nil { - t.Log("error ", err) - t.Fail() + 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("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 { + t.Log("error ", err) + t.Fail() + } + + buf := new(bytes.Buffer) + + if err := env.Encode(buf); err != nil { + t.Log("error ", err) + t.Fail() + } + + bufBytes := buf.Bytes() + + if _, err := Store(&bufBytes); err != nil { + t.Log("error ", err) + t.Fail() + } } - bufBytes := buf.Bytes() + for i := 1; i < 51; i++ { + // search a random something that will return a single result + 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, 0, 100) + if err != nil { + t.Log("error ", err) + t.Fail() + } + + 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") + 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") + } - if _, err := Store(&bufBytes); err != nil { + // search something that will return 200 results + summaries, _, err := Search("This is the email body", "", 0, 0, testRuns) + if err != nil { t.Log("error ", err) t.Fail() } + assertEqual(t, len(summaries), testRuns, "search results expected") + + Close() } +} - for i := 1; i < 51; i++ { - // search a random something that will return a single result - 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), +func TestSearchDelete100(t *testing.T) { + for _, tenantID := range []string{"", "MyServer 3", "host.example.com"} { + tenantID = config.DBTenantID(tenantID) + + setup(tenantID) + + if tenantID == "" { + t.Log("Testing search delete of 100 messages") + } else { + t.Logf("Testing search delete of 100 messages (tenant %s)", tenantID) } - searchIdx := rand.Intn(len(uniqueSearches)) - search := uniqueSearches[searchIdx] + for i := 0; i < 100; i++ { + if _, err := Store(&testTextEmail); err != nil { + t.Log("error ", err) + t.Fail() + } + if _, err := Store(&testMimeEmail); err != nil { + t.Log("error ", err) + t.Fail() + } + } - summaries, _, err := Search(search, "", 0, 0, 100) + _, total, err := Search("from:sender@example.com", "", 0, 0, 100) if err != nil { t.Log("error ", err) t.Fail() } - 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") - 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 results - summaries, _, err := Search("This is the email body", "", 0, 0, testRuns) - if err != nil { - t.Log("error ", err) - t.Fail() - } - assertEqual(t, len(summaries), testRuns, "search results expected") -} - -func TestSearchDelete100(t *testing.T) { - setup() - defer Close() + assertEqual(t, total, 100, "100 search results expected") - t.Log("Testing search delete of 100 messages") - for i := 0; i < 100; i++ { - if _, err := Store(&testTextEmail); err != nil { + if err := DeleteSearch("from:sender@example.com", ""); err != nil { t.Log("error ", err) t.Fail() } - if _, err := Store(&testMimeEmail); err != nil { + + _, total, err = Search("from:sender@example.com", "", 0, 0, 100) + if err != nil { t.Log("error ", err) t.Fail() } - } - - _, total, err := Search("from:sender@example.com", "", 0, 0, 100) - if err != nil { - t.Log("error ", err) - t.Fail() - } - - assertEqual(t, total, 100, "100 search results expected") - if err := DeleteSearch("from:sender@example.com", ""); err != nil { - t.Log("error ", err) - t.Fail() - } + assertEqual(t, total, 0, "0 search results expected") - _, total, err = Search("from:sender@example.com", "", 0, 0, 100) - if err != nil { - t.Log("error ", err) - t.Fail() + Close() } - - assertEqual(t, total, 0, "0 search results expected") } func TestSearchDelete1100(t *testing.T) { - setup() + setup("") defer Close() t.Log("Testing search delete of 1100 messages") diff --git a/internal/storage/tags_test.go b/internal/storage/tags_test.go index f9ae285bf..98548e58e 100644 --- a/internal/storage/tags_test.go +++ b/internal/storage/tags_test.go @@ -4,127 +4,140 @@ import ( "fmt" "strings" "testing" + + "github.com/axllent/mailpit/config" ) func TestTags(t *testing.T) { - setup() - defer Close() - t.Log("Testing tags") + for _, tenantID := range []string{"", "MyServer 3", "host.example.com"} { + tenantID = config.DBTenantID(tenantID) + + setup(tenantID) + + if tenantID == "" { + t.Log("Testing tags") + } else { + t.Logf("Testing tags (tenant %s)", tenantID) + } + + ids := []string{} + + for i := 0; i < 10; i++ { + id, err := Store(&testMimeEmail) + if err != nil { + t.Log("error ", err) + t.Fail() + } + ids = append(ids, id) + } - ids := []string{} + for i := 0; i < 10; i++ { + if _, err := SetMessageTags(ids[i], []string{fmt.Sprintf("Tag-%d", i)}); err != nil { + t.Log("error ", err) + t.Fail() + } + } + + for i := 0; i < 10; i++ { + message, err := GetMessage(ids[i]) + if err != nil { + t.Log("error ", err) + t.Fail() + } + + if len(message.Tags) != 1 || message.Tags[0] != fmt.Sprintf("Tag-%d", i) { + t.Fatal("Message tags do not match") + } + } + + if err := DeleteAllMessages(); err != nil { + t.Log("error ", err) + t.Fail() + } - for i := 0; i < 10; i++ { + // test 20 tags id, err := Store(&testMimeEmail) if err != nil { t.Log("error ", err) t.Fail() } - ids = append(ids, id) - } - - for i := 0; i < 10; i++ { - if _, err := SetMessageTags(ids[i], []string{fmt.Sprintf("Tag-%d", i)}); err != nil { + newTags := []string{} + for i := 0; i < 20; i++ { + // pad number with 0 to ensure they are returned alphabetically + newTags = append(newTags, fmt.Sprintf("AnotherTag %02d", i)) + } + if _, err := SetMessageTags(id, newTags); err != nil { t.Log("error ", err) t.Fail() } - } + returnedTags := getMessageTags(id) + assertEqual(t, strings.Join(newTags, "|"), strings.Join(returnedTags, "|"), "Message tags do not match") - for i := 0; i < 10; i++ { - message, err := GetMessage(ids[i]) - if err != nil { + // remove first tag + if err := deleteMessageTag(id, newTags[0]); err != nil { t.Log("error ", err) t.Fail() } + returnedTags = getMessageTags(id) + assertEqual(t, strings.Join(newTags[1:], "|"), strings.Join(returnedTags, "|"), "Message tags do not match after deleting 1") - if len(message.Tags) != 1 || message.Tags[0] != fmt.Sprintf("Tag-%d", i) { - t.Fatal("Message tags do not match") + // remove all tags + if err := DeleteAllMessageTags(id); err != nil { + t.Log("error ", err) + t.Fail() } - } + returnedTags = getMessageTags(id) + assertEqual(t, "", strings.Join(returnedTags, "|"), "Message tags should be empty") - if err := DeleteAllMessages(); err != nil { - t.Log("error ", err) - t.Fail() - } - - // test 20 tags - id, err := Store(&testMimeEmail) - if err != nil { - t.Log("error ", err) - t.Fail() - } - newTags := []string{} - for i := 0; i < 20; i++ { - // pad number with 0 to ensure they are returned alphabetically - newTags = append(newTags, fmt.Sprintf("AnotherTag %02d", i)) - } - if _, err := SetMessageTags(id, newTags); err != nil { - t.Log("error ", err) - t.Fail() - } - returnedTags := getMessageTags(id) - assertEqual(t, strings.Join(newTags, "|"), strings.Join(returnedTags, "|"), "Message tags do not match") - - // remove first tag - if err := deleteMessageTag(id, newTags[0]); err != nil { - t.Log("error ", err) - t.Fail() - } - returnedTags = getMessageTags(id) - assertEqual(t, strings.Join(newTags[1:], "|"), strings.Join(returnedTags, "|"), "Message tags do not match after deleting 1") + // apply the same tag twice + if _, err := SetMessageTags(id, []string{"Duplicate Tag", "Duplicate Tag"}); err != nil { + t.Log("error ", err) + t.Fail() + } + returnedTags = getMessageTags(id) + assertEqual(t, "Duplicate Tag", strings.Join(returnedTags, "|"), "Message tags should be duplicated") + if err := DeleteAllMessageTags(id); err != nil { + t.Log("error ", err) + t.Fail() + } - // remove all tags - if err := DeleteAllMessageTags(id); err != nil { - t.Log("error ", err) - t.Fail() - } - returnedTags = getMessageTags(id) - assertEqual(t, "", strings.Join(returnedTags, "|"), "Message tags should be empty") + // apply tag with invalid characters + if _, err := SetMessageTags(id, []string{"Dirty! \"Tag\""}); err != nil { + t.Log("error ", err) + t.Fail() + } + returnedTags = getMessageTags(id) + assertEqual(t, "Dirty Tag", strings.Join(returnedTags, "|"), "Dirty message tag did not clean as expected") + if err := DeleteAllMessageTags(id); err != nil { + t.Log("error ", err) + t.Fail() + } - // apply the same tag twice - if _, err := SetMessageTags(id, []string{"Duplicate Tag", "Duplicate Tag"}); err != nil { - t.Log("error ", err) - t.Fail() - } - returnedTags = getMessageTags(id) - assertEqual(t, "Duplicate Tag", strings.Join(returnedTags, "|"), "Message tags should be duplicated") - if err := DeleteAllMessageTags(id); err != nil { - t.Log("error ", err) - t.Fail() - } + // Check deleted message tags also prune the tags database + allTags := GetAllTags() + assertEqual(t, "", strings.Join(allTags, "|"), "Tags did not delete as expected") - // apply tag with invalid characters - if _, err := SetMessageTags(id, []string{"Dirty! \"Tag\""}); err != nil { - t.Log("error ", err) - t.Fail() - } - returnedTags = getMessageTags(id) - assertEqual(t, "Dirty Tag", strings.Join(returnedTags, "|"), "Dirty message tag did not clean as expected") - if err := DeleteAllMessageTags(id); err != nil { - t.Log("error ", err) - t.Fail() - } + if err := DeleteAllMessages(); err != nil { + t.Log("error ", err) + t.Fail() + } - // Check deleted message tags also prune the tags database - allTags := GetAllTags() - assertEqual(t, "", strings.Join(allTags, "|"), "Tags did not delete as expected") + // test 20 tags + id, err = Store(&testTagEmail) + if err != nil { + t.Log("error ", err) + t.Fail() + } - if err := DeleteAllMessages(); err != nil { - t.Log("error ", err) - t.Fail() - } + returnedTags = getMessageTags(id) + assertEqual(t, "BccTag|CcTag|FromFag|ToTag|X-tag1|X-tag2", strings.Join(returnedTags, "|"), "Tags not detected correctly") + if err := DeleteAllMessageTags(id); err != nil { + t.Log("error ", err) + t.Fail() + } - // test 20 tags - id, err = Store(&testTagEmail) - if err != nil { - t.Log("error ", err) - t.Fail() + Close() } - returnedTags = getMessageTags(id) - assertEqual(t, "BccTag|CcTag|FromFag|ToTag|X-tag1|X-tag2", strings.Join(returnedTags, "|"), "Tags not detected correctly") - if err := DeleteAllMessageTags(id); err != nil { - t.Log("error ", err) - t.Fail() - } } diff --git a/internal/storage/testing.go b/internal/storage/testing.go index a8d3f12be..75267935a 100644 --- a/internal/storage/testing.go +++ b/internal/storage/testing.go @@ -16,10 +16,11 @@ var ( testRuns = 100 ) -func setup() { +func setup(tenantID string) { logger.NoLogging = true config.MaxMessages = 0 config.Database = os.Getenv("MP_DATABASE") + config.TenantID = config.DBTenantID(tenantID) if err := InitDB(); err != nil { panic(err) From d2070e1ee1e92d9151cdb86ab0063ad571b91ca8 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Fri, 18 Oct 2024 18:03:25 +1300 Subject: [PATCH 4/5] Chore: Update caniemail database --- internal/htmlcheck/caniemail-data.json | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/internal/htmlcheck/caniemail-data.json b/internal/htmlcheck/caniemail-data.json index a236a8c01..38edbd9f4 100644 --- a/internal/htmlcheck/caniemail-data.json +++ b/internal/htmlcheck/caniemail-data.json @@ -1,6 +1,6 @@ { "api_version":"1.0.4", - "last_update_date":"2024-08-31 16:00:28 +0000", + "last_update_date":"2024-10-09 08:12:03 +0000", "nicenames":{"family":{"gmail":"Gmail","outlook":"Outlook","yahoo":"Yahoo! Mail","apple-mail":"Apple Mail","aol":"AOL","thunderbird":"Mozilla Thunderbird","microsoft":"Microsoft","samsung-email":"Samsung Email","sfr":"SFR","orange":"Orange","protonmail":"ProtonMail","hey":"HEY","mail-ru":"Mail.ru","fastmail":"Fastmail","laposte":"LaPoste.net","t-online-de":"T-online.de","free-fr":"Free.fr","gmx":"GMX","web-de":"WEB.DE","ionos-1and1":"1&1","rainloop":"RainLoop","wp-pl":"WP.pl"},"platform":{"desktop-app":"Desktop","desktop-webmail":"Desktop Webmail","mobile-webmail":"Mobile Webmail","webmail":"Webmail","ios":"iOS","android":"Android","windows":"Windows","macos":"macOS","windows-mail":"Windows Mail","outlook-com":"Outlook.com"},"support":{"supported":"Supported","mitigated":"Partially supported","unsupported":"Not supported","unknown":"Support unknown","mixed":"Mixed support"},"category":{"html":"HTML","css":"CSS","image":"Image formats","others":"Others"}}, "data":[ { @@ -1406,7 +1406,7 @@ "last_test_date":"2019-10-01", "test_url":"https://www.caniemail.com/tests/css-margin.html", "test_results_url":"https://app.emailonacid.com/app/acidtest/UmR6V6XenYY9bQiABuLGZRRrdP3fj2ZraiJjEyi4WKBho/list", - "stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.4":"y"}},"gmail":{"desktop-webmail":{"2019-10":"a #1"},"ios":{"2019-10":"a #1"},"android":{"2019-10":"a #1"},"mobile-webmail":{"2020-02":"a #1"}},"orange":{"desktop-webmail":{"2019-10":"y","2021-03":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"outlook":{"windows":{"2003":"y","2007":"a #1 #2 #3 #4","2010":"a #1 #2 #3 #4","2013":"a #1 #2 #3 #4","2016":"a #1 #2 #3 #4","2019":"a #1 #2 #3 #4"},"windows-mail":{"2019-10":"a #1 #2 #3"},"macos":{"2011":"y","2016":"y","16.80":"a #1"},"outlook-com":{"2019-10":"a #1","2023-12":"a #1"},"ios":{"2.51.1":"y","4.3.1":"a #1"},"android":{"2019-10":"a #1"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"thunderbird":{"macos":{"60.3":"y"}},"aol":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"yahoo":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"a #1"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"y"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"y"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}}, + "stats":{"apple-mail":{"macos":{"12.4":"y"},"ios":{"12.4":"y"}},"gmail":{"desktop-webmail":{"2019-10":"a #1"},"ios":{"2019-10":"a #1"},"android":{"2019-10":"a #1"},"mobile-webmail":{"2020-02":"a #1"}},"orange":{"desktop-webmail":{"2019-10":"y","2021-03":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"outlook":{"windows":{"2003":"y","2007":"a #1 #2 #3 #4","2010":"a #1 #2 #3 #4","2013":"a #1 #2 #3 #4","2016":"a #1 #2 #3 #4","2019":"a #1 #2 #3 #4"},"windows-mail":{"2019-10":"a #1 #2 #3"},"macos":{"2011":"y","2016":"y","16.80":"a #1"},"outlook-com":{"2019-10":"a #1","2023-12":"a #1"},"ios":{"2.51.1":"y","4.3.1":"a #1"},"android":{"2019-10":"a #1"}},"samsung-email":{"android":{"6.0":"y"}},"sfr":{"desktop-webmail":{"2019-10":"y"},"ios":{"2019-10":"y"},"android":{"2019-10":"y"}},"thunderbird":{"macos":{"60.3":"y"}},"aol":{"desktop-webmail":{"2019-10":"y","2024-10":"a #1"},"ios":{"2019-10":"y","2024-10":"a #1"},"android":{"2019-10":"y","2024-10":"a #1"}},"yahoo":{"desktop-webmail":{"2019-10":"y","2024-10":"a #1"},"ios":{"2019-10":"y","2024-10":"a #1"},"android":{"2019-10":"y","2024-10":"a #1"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"a #1"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-06":"y"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"web-de":{"desktop-webmail":{"2022-06":"y"},"ios":{"2022-06":"y"},"android":{"2022-06":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-06":"y"},"android":{"2022-06":"y"}}}, "notes":null, "notes_by_num":{"1":"Partial. Negative values are not supported.","2":"Partial. Not supported on `` and `` elements.","3":"Buggy. `background-color` is included inside the `margin`.","4":"Partial. `auto` value is not supported."} }, @@ -3219,6 +3219,22 @@ "notes_by_num":{"1":"Buggy. `visibility:collapse` applied to a `` only hides content and does not \"remove\" it from layout.","2":"Partially supported. `visibility:collapse` is not supported."} }, + { + "slug":"css-white-space-collapse", + "title":"white-space-collapse", + "description":"Controls how white space inside an element is collapsed.", + "url":"https://www.caniemail.com/features/css-white-space-collapse/", + "category":"css", + "tags":[], + "keywords":"break, space, collapse, hide", + "last_test_date":"2024-09-04", + "test_url":"https://www.caniemail.com/tests/css-white-space-collapse.html", + "test_results_url":"https://testi.at/proj/e6y4s3zytp5kty7kcg", + "stats":{"apple-mail":{"macos":{"10":"n","11":"n","12":"n","13":"n","14":"y #1"},"ios":{"15":"y #1"}},"gmail":{"desktop-webmail":{"2024-09":"n"},"ios":{"2024-09":"n"},"android":{"2024-09":"n"},"mobile-webmail":{"2024-09":"n"}},"orange":{"desktop-webmail":{"2024-09":"u"},"ios":{"2024-09":"u"},"android":{"2024-09":"u"}},"outlook":{"windows":{"2013":"n","2016":"n","2019":"n","2021":"n"},"windows-mail":{"2024-09":"n"},"macos":{"2024-09":"n"},"outlook-com":{"2024-09":"n"},"ios":{"2024-09":"n"},"android":{"2024-09":"n"}},"yahoo":{"desktop-webmail":{"2024-09":"n"},"ios":{"2024-09":"n"},"android":{"2024-09":"n"}},"aol":{"desktop-webmail":{"2024-09":"n"},"ios":{"2024-09":"n"},"android":{"2024-09":"n"}},"samsung-email":{"android":{"2024-09":"y #1"}},"sfr":{"desktop-webmail":{"2024-03":"u"},"ios":{"2024-03":"u"},"android":{"2024-03":"u"}},"protonmail":{"desktop-webmail":{"2024-09":"u"},"ios":{"2024-09":"u"},"android":{"2024-09":"u"}},"hey":{"desktop-webmail":{"2024-09":"u"}},"mail-ru":{"desktop-webmail":{"2024-09":"y #1"}},"fastmail":{"desktop-webmail":{"2024-09":"u"}}}, + "notes":null, + "notes_by_num":{"1":"Partial. `preserve-spaces` value works only on Firefox."} + }, + { "slug":"css-white-space", "title":"white-space", @@ -4734,7 +4750,7 @@ "last_test_date":"2023-01-15", "test_url":"https://www.caniemail.com/tests/images.html", "test_results_url":"https://app.emailonacid.com/app/acidtest/xm1T5nQ1MKtHpVSJidhagmt3Z53CjqbkMhorlvuM0Gz57/list", - "stats":{"apple-mail":{"macos":{"13":"n","14":"y"},"ios":{"13":"n","14":"n","15":"y"}},"gmail":{"desktop-webmail":{"2020-02":"n","2023-01":"n","2024-07":"n"},"ios":{"2020-02":"a #1","2023-01":"a #1"},"android":{"2020-02":"a #1","2023-01":"a #1"},"mobile-webmail":{"2020-02":"n","2023-01":"n"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"n"},"windows-mail":{"2020-02":"y"},"macos":{"2016":"n","13.1":"y","16.80":"y"},"outlook-com":{"2020-02":"y","2024-01":"y"},"ios":{"2020-02":"n","2023-01":"y"},"android":{"2020-02":"y"}},"samsung-email":{"android":{"9.0":"y"}},"thunderbird":{"windows":{"2020-02":"y"},"macos":{"68.4":"y"}},"aol":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"yahoo":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2020-02":"y","2021-03":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"sfr":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"n","2023-01":"n"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-09":"y"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"web-de":{"desktop-webmail":{"2022-09":"y"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-09":"y"},"android":{"2022-09":"y"}}}, + "stats":{"apple-mail":{"macos":{"13":"n","14":"y"},"ios":{"13":"n","14":"n","15":"y"}},"gmail":{"desktop-webmail":{"2020-02":"n","2023-01":"n","2024-07":"n"},"ios":{"2020-02":"a #1","2023-01":"a #1"},"android":{"2020-02":"a #1","2023-01":"a #1"},"mobile-webmail":{"2020-02":"n","2023-01":"n"}},"outlook":{"windows":{"2007":"n","2010":"n","2013":"n","2016":"n","2019":"y"},"windows-mail":{"2020-02":"y"},"macos":{"2016":"n","13.1":"y","16.80":"y"},"outlook-com":{"2020-02":"y","2024-01":"y"},"ios":{"2020-02":"n","2023-01":"y"},"android":{"2020-02":"y"}},"samsung-email":{"android":{"9.0":"y"}},"thunderbird":{"windows":{"2020-02":"y"},"macos":{"68.4":"y"}},"aol":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"yahoo":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"orange":{"desktop-webmail":{"2020-02":"y","2021-03":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"sfr":{"desktop-webmail":{"2020-02":"y"},"ios":{"2020-02":"y"},"android":{"2020-02":"y"}},"protonmail":{"desktop-webmail":{"2020-03":"y"},"ios":{"2020-03":"y"},"android":{"2020-03":"y"}},"hey":{"desktop-webmail":{"2020-06":"y"}},"mail-ru":{"desktop-webmail":{"2020-10":"n","2023-01":"n"}},"fastmail":{"desktop-webmail":{"2021-07":"y"}},"laposte":{"desktop-webmail":{"2021-08":"y"}},"gmx":{"desktop-webmail":{"2022-09":"y"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"web-de":{"desktop-webmail":{"2022-09":"y"},"ios":{"2022-09":"y"},"android":{"2022-09":"y"}},"ionos-1and1":{"desktop-webmail":{"2022-09":"y"},"android":{"2022-09":"y"}}}, "notes":null, "notes_by_num":{"1":"Partially supported. Only works with non Google accounts."} }, From 5131b6a0ccd78998f0d55b75ce91512dee682b5a Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Sat, 19 Oct 2024 23:43:16 +1300 Subject: [PATCH 5/5] Release v1.20.7 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcd59482e..c8c299e5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ Notable changes to Mailpit will be documented in this file. +## [v1.20.7] + +### Chore +- Update caniemail database + +### Fix +- SQL error deleting a tag while using tenant-id ([#374](https://github.com/axllent/mailpit/issues/374)) + +### Testing +- Add tenantIDs to tests + + ## [v1.20.6] ### Chore