- Introduced a new sync pipeline for IMAP that integrates a rules engine and webhook execution. - Enhanced the `SyncWorker` to support attachment management and folder synchronization. - Added functionality to detect special folder types (Sent, Drafts, Trash, Archive, Spam) during sync. - Implemented a database schema for tracking rule executions and their outcomes. - Created unit tests for the new rules engine and webhook execution logic. - Updated migration scripts to accommodate new database structures for rule executions and folder states. - Enhanced error handling and logging throughout the sync process for better observability.
158 lines
4.0 KiB
Go
158 lines
4.0 KiB
Go
package imap
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestExtractAttachments_plainAttachment(t *testing.T) {
|
|
pdfData := []byte("%PDF-1.4\n")
|
|
raw := buildMultipartMessage(t, "mixed", []mimePart{
|
|
{
|
|
contentType: "text/plain",
|
|
body: []byte("Hello world"),
|
|
},
|
|
{
|
|
contentType: "application/pdf; name=\"doc.pdf\"",
|
|
disposition: "attachment; filename=\"doc.pdf\"",
|
|
body: pdfData,
|
|
transferEnc: "base64",
|
|
},
|
|
})
|
|
|
|
attachments, err := ExtractAttachments(raw)
|
|
if err != nil {
|
|
t.Fatalf("ExtractAttachments() error = %v", err)
|
|
}
|
|
if len(attachments) != 1 {
|
|
t.Fatalf("len(attachments) = %d, want 1", len(attachments))
|
|
}
|
|
|
|
att := attachments[0]
|
|
if att.Filename != "doc.pdf" {
|
|
t.Fatalf("Filename = %q, want doc.pdf", att.Filename)
|
|
}
|
|
if att.ContentType != "application/pdf" {
|
|
t.Fatalf("ContentType = %q, want application/pdf", att.ContentType)
|
|
}
|
|
if att.IsInline {
|
|
t.Fatal("IsInline = true, want false")
|
|
}
|
|
if att.ContentID != "" {
|
|
t.Fatalf("ContentID = %q, want empty", att.ContentID)
|
|
}
|
|
if string(att.Data) != string(pdfData) {
|
|
t.Fatalf("Data = %q, want %q", att.Data, pdfData)
|
|
}
|
|
}
|
|
|
|
func TestExtractAttachments_inlineWithCID(t *testing.T) {
|
|
pngData := []byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}
|
|
raw := buildMultipartMessage(t, "related", []mimePart{
|
|
{
|
|
contentType: "text/html",
|
|
body: []byte(`<html><body><img src="cid:logo@cid"></body></html>`),
|
|
},
|
|
{
|
|
contentType: "image/png; name=\"logo.png\"",
|
|
disposition: "inline; filename=\"logo.png\"",
|
|
contentID: "<logo@cid>",
|
|
body: pngData,
|
|
transferEnc: "base64",
|
|
},
|
|
})
|
|
|
|
attachments, err := ExtractAttachments(raw)
|
|
if err != nil {
|
|
t.Fatalf("ExtractAttachments() error = %v", err)
|
|
}
|
|
if len(attachments) != 1 {
|
|
t.Fatalf("len(attachments) = %d, want 1", len(attachments))
|
|
}
|
|
|
|
att := attachments[0]
|
|
if att.Filename != "logo.png" {
|
|
t.Fatalf("Filename = %q, want logo.png", att.Filename)
|
|
}
|
|
if att.ContentType != "image/png" {
|
|
t.Fatalf("ContentType = %q, want image/png", att.ContentType)
|
|
}
|
|
if !att.IsInline {
|
|
t.Fatal("IsInline = false, want true")
|
|
}
|
|
if att.ContentID != "logo@cid" {
|
|
t.Fatalf("ContentID = %q, want logo@cid", att.ContentID)
|
|
}
|
|
if string(att.Data) != string(pngData) {
|
|
t.Fatalf("Data = %q, want %q", att.Data, pngData)
|
|
}
|
|
}
|
|
|
|
func TestExtractAttachments_skipsBodyParts(t *testing.T) {
|
|
raw := buildMultipartMessage(t, "alternative", []mimePart{
|
|
{
|
|
contentType: "text/plain",
|
|
body: []byte("plain body"),
|
|
},
|
|
{
|
|
contentType: "text/html",
|
|
body: []byte("<p>html body</p>"),
|
|
},
|
|
})
|
|
|
|
attachments, err := ExtractAttachments(raw)
|
|
if err != nil {
|
|
t.Fatalf("ExtractAttachments() error = %v", err)
|
|
}
|
|
if len(attachments) != 0 {
|
|
t.Fatalf("len(attachments) = %d, want 0", len(attachments))
|
|
}
|
|
}
|
|
|
|
type mimePart struct {
|
|
contentType string
|
|
disposition string
|
|
contentID string
|
|
transferEnc string
|
|
body []byte
|
|
}
|
|
|
|
func buildMultipartMessage(t *testing.T, subtype string, parts []mimePart) []byte {
|
|
t.Helper()
|
|
|
|
const boundary = "test-boundary"
|
|
var b strings.Builder
|
|
b.WriteString("From: sender@example.com\r\n")
|
|
b.WriteString("To: recipient@example.com\r\n")
|
|
b.WriteString("Subject: attachment test\r\n")
|
|
b.WriteString("MIME-Version: 1.0\r\n")
|
|
b.WriteString("Content-Type: multipart/" + subtype + "; boundary=\"" + boundary + "\"\r\n")
|
|
b.WriteString("\r\n")
|
|
|
|
for _, part := range parts {
|
|
b.WriteString("--" + boundary + "\r\n")
|
|
b.WriteString("Content-Type: " + part.contentType + "\r\n")
|
|
if part.disposition != "" {
|
|
b.WriteString("Content-Disposition: " + part.disposition + "\r\n")
|
|
}
|
|
if part.contentID != "" {
|
|
b.WriteString("Content-ID: " + part.contentID + "\r\n")
|
|
}
|
|
if part.transferEnc != "" {
|
|
b.WriteString("Content-Transfer-Encoding: " + part.transferEnc + "\r\n")
|
|
}
|
|
b.WriteString("\r\n")
|
|
|
|
if part.transferEnc == "base64" {
|
|
b.WriteString(base64.StdEncoding.EncodeToString(part.body))
|
|
} else {
|
|
b.Write(part.body)
|
|
}
|
|
b.WriteString("\r\n")
|
|
}
|
|
b.WriteString("--" + boundary + "--\r\n")
|
|
|
|
return []byte(b.String())
|
|
}
|