132 lines
4.6 KiB
Go
132 lines
4.6 KiB
Go
package mailingress
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"github.com/emersion/go-imap/backend/memory"
|
|
"github.com/emersion/go-imap/server"
|
|
"github.com/example/notify-gateway/internal/gateway"
|
|
"github.com/example/notify-gateway/internal/outbox"
|
|
"github.com/example/notify-gateway/internal/testutil"
|
|
"io"
|
|
"log"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/example/notify-gateway/internal/config"
|
|
)
|
|
|
|
func TestIMAPCheckpointsAndDurableAcceptance(t *testing.T) {
|
|
ctx := context.Background()
|
|
serverTLS, clientTLS := testutil.TLSConfigs()
|
|
listener, err := tls.Listen("tcp", "127.0.0.1:0", serverTLS)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
backend := memory.New()
|
|
srv := server.New(backend)
|
|
srv.ErrorLog = log.New(io.Discard, "", 0)
|
|
done := make(chan struct{})
|
|
go func() { defer close(done); srv.Serve(listener) }()
|
|
defer func() { srv.Close(); <-done }()
|
|
dir := t.TempDir()
|
|
cfg, _ := config.Open(filepath.Join(dir, "config.json"))
|
|
db, err := outbox.Open(filepath.Join(dir, "outbox.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
c := cfg.Get()
|
|
c.Mappings = []config.Mapping{{ID: "mail", Enabled: true, Source: "mail", Target: "news", TextTemplate: "{{.Message}}"}}
|
|
if err := cfg.Replace(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
q := &gateway.Queue{Store: db, Dispatcher: gateway.New(cfg, nil)}
|
|
p := &Poller{Config: cfg, Queue: q}
|
|
m := config.MailIngress{ID: "mail", Address: listener.Addr().String(), Username: "username", Password: "password", Folder: "INBOX", Channel: "mail"}
|
|
// Existing UID 6 is skipped on first setup; mailbox remains read-only.
|
|
if err := p.poll(ctx, m, clientTLS); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rows, _ := db.List(ctx, 50, 0)
|
|
if len(rows) != 0 {
|
|
t.Fatal("old mail imported unexpectedly")
|
|
}
|
|
cpID := gateway.ScopedKey("imap", m.ID, m.Address, m.Username, m.Folder)
|
|
old, _ := db.Checkpoint(ctx, cpID)
|
|
user, _ := backend.Login(nil, "username", "password")
|
|
mailbox, _ := user.GetMailbox("INBOX")
|
|
appendMail := func(body string) {
|
|
t.Helper()
|
|
if err := mailbox.CreateMessage(nil, time.Now(), strings.NewReader("From: sender@example.org\r\nSubject: Test\r\nContent-Type: text/plain\r\n\r\n"+body)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
appendMail("new mail")
|
|
if err := p.poll(ctx, m, clientTLS); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rows, _ = db.List(ctx, 50, 0)
|
|
if len(rows) != 1 {
|
|
t.Fatalf("new mail jobs=%d", len(rows))
|
|
}
|
|
// Simulate a crash after receipt commit but before checkpoint persistence.
|
|
if err := db.SetCheckpoint(ctx, cpID, old); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := p.poll(ctx, m, clientTLS); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rows, _ = db.List(ctx, 50, 0)
|
|
if len(rows) != 1 {
|
|
t.Fatal("duplicate IMAP delivery")
|
|
}
|
|
saved, _ := db.Checkpoint(ctx, cpID)
|
|
c.Mappings[0].Enabled = false
|
|
cfg.Replace(c)
|
|
appendMail("pending routing")
|
|
if err := p.poll(ctx, m, clientTLS); err == nil {
|
|
t.Fatal("missing mapping accepted")
|
|
}
|
|
after, _ := db.Checkpoint(ctx, cpID)
|
|
if after != saved {
|
|
t.Fatal("checkpoint advanced past unaccepted mail")
|
|
}
|
|
c.Mappings[0].Enabled = true
|
|
cfg.Replace(c)
|
|
if err := p.poll(ctx, m, clientTLS); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rows, _ = db.List(ctx, 50, 0)
|
|
if len(rows) != 2 {
|
|
t.Fatal("mail was lost")
|
|
}
|
|
if err := p.Poll(ctx, m); err == nil {
|
|
t.Fatal("untrusted IMAP certificate accepted")
|
|
}
|
|
}
|
|
|
|
func TestMIMEFiltersAndLoopProtection(t *testing.T) {
|
|
raw := "From: Sender <sender@example.org>\r\nTo: ops@example.org\r\nSubject: =?UTF-8?Q?Gr=C3=BC=C3=9Fe?=\r\nMIME-Version: 1.0\r\nContent-Type: multipart/mixed; boundary=test\r\n\r\n--test\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: base64\r\n\r\nSGFsbG8=\r\n--test\r\nContent-Type: application/octet-stream\r\nContent-Disposition: attachment; filename=test.txt\r\n\r\nnot forwarded\r\n--test--\r\n"
|
|
cfg := config.MailIngress{Channel: "ops", From: []string{"sender@example.org"}, To: []string{"ops@example.org"}}
|
|
msg, skip, err := Parse([]byte(raw), cfg)
|
|
if err != nil || skip || msg.Title != "Grüße" || msg.Message != "Hallo" || msg.Source != "mail" {
|
|
t.Fatalf("%+v %v %v", msg, skip, err)
|
|
}
|
|
if _, skip, err := Parse([]byte("X-Notify-Gateway: 1\r\n"+raw), cfg); err != nil || !skip {
|
|
t.Fatalf("loop: %v %v", skip, err)
|
|
}
|
|
cfg.From = []string{"other@example.org"}
|
|
if _, skip, err := Parse([]byte(raw), cfg); err != nil || !skip {
|
|
t.Fatalf("sender filter: %v %v", skip, err)
|
|
}
|
|
if _, _, err := Parse([]byte(strings.Repeat("x", maxMailBytes+1)), cfg); err == nil {
|
|
t.Fatal("oversize accepted")
|
|
}
|
|
if _, _, err := Parse([]byte("Content-Type: text/html\r\n\r\n<b>hello</b>"), config.MailIngress{}); err == nil {
|
|
t.Fatal("HTML-only mail should require attention")
|
|
}
|
|
}
|