#18: implement SMTP Submission (587/465), SASL PLAIN/LOGIN auth and preflight diagnostics

This commit is contained in:
Lucas Rayan committed 2026-08-01 22:49:02 -03:00
1 parent 7249419efc
commit d56f29d084
70 files changed
+5163 -659

No files matched your search

@@ -5,26 +5,50 @@ import (
"time"
)
func TestCalculateNextBackoff_SchedulesCorrectDelays(t *testing.T) {
// PLAN.md section 6.3 defines the wait *before the next* attempt: after
// attempt 1 fails the next try is +5 min, after attempt 3 it is +1 h together
// with the delay DSN, and from the fifth try on the delay carries +/-20%
// jitter. A zero delay after the first failure would hammer the destination in
// a tight loop.
func TestCalculateNextBackoff_FollowsRetryTable(t *testing.T) {
now := time.Now()
expiresAt := now.Add(5 * 24 * time.Hour)
// Attempt 2 -> +5 min
next, delayDsn, perm := CalculateNextBackoff(now, 2, expiresAt)
if perm || delayDsn || next.Sub(now) < 4*time.Minute {
t.Errorf("attempt 2 backoff mismatch: next=%v, delayDsn=%v, perm=%v", next, delayDsn, perm)
cases := []struct {
failedAttempt int
wantMin time.Duration
wantMax time.Duration
wantDelayDsn bool
}{
{1, 5 * time.Minute, 5 * time.Minute, false},
{2, 15 * time.Minute, 15 * time.Minute, false},
{3, time.Hour, time.Hour, true},
{4, 96 * time.Minute, 144 * time.Minute, false}, // 2 h +/- 20%
{5, 192 * time.Minute, 288 * time.Minute, false}, // 4 h +/- 20%
{6, 384 * time.Minute, 576 * time.Minute, false}, // 8 h +/- 20%
{7, 576 * time.Minute, 864 * time.Minute, false}, // 12 h +/- 20%
{20, 576 * time.Minute, 864 * time.Minute, false}, // steady 12 h
}
// Attempt 4 -> +1 hr + delayDsn=true
next, delayDsn, perm = CalculateNextBackoff(now, 4, expiresAt)
if perm || !delayDsn || next.Sub(now) < 55*time.Minute {
t.Errorf("attempt 4 backoff mismatch: next=%v, delayDsn=%v, perm=%v", next, delayDsn, perm)
for _, tc := range cases {
next, delayDsn, perm := CalculateNextBackoff(now, tc.failedAttempt, expiresAt)
if perm {
t.Errorf("attempt %d: unexpected permanent failure", tc.failedAttempt)
continue
}
got := next.Sub(now)
if got < tc.wantMin || got > tc.wantMax {
t.Errorf("attempt %d: delay = %v, want between %v and %v", tc.failedAttempt, got, tc.wantMin, tc.wantMax)
}
if delayDsn != tc.wantDelayDsn {
t.Errorf("attempt %d: delayDsn = %v, want %v", tc.failedAttempt, delayDsn, tc.wantDelayDsn)
}
}
}
// Expired -> permanent failure
expiredTime := now.Add(-1 * time.Hour)
_, _, perm = CalculateNextBackoff(now, 5, expiredTime)
if !perm {
t.Errorf("expected permanent failure for expired job")
func TestCalculateNextBackoff_ExpiredJobIsPermanent(t *testing.T) {
now := time.Now()
if _, _, perm := CalculateNextBackoff(now, 5, now.Add(-time.Hour)); !perm {
t.Error("expected permanent failure for an expired job")
}
}