#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

@@ -21,7 +21,7 @@ func NewAuthRepository(pool *pgxpool.Pool) *AuthRepository {
func (r *AuthRepository) FindByAddress(ctx context.Context, address string) (*port.MailboxAuth, error) {
row := r.pool.QueryRow(ctx, `
SELECT m.id, m.password_hash
SELECT m.id, m.password_hash, m.email_address, d.name, m.max_recipients_per_hour
FROM mailboxes m
JOIN domains d ON d.id = m.domain_id
WHERE m.email_address = $1
@@ -31,7 +31,7 @@ func (r *AuthRepository) FindByAddress(ctx context.Context, address string) (*po
var rec port.MailboxAuth
var id uuid.UUID
if err := row.Scan(&id, &rec.PasswordHash); err != nil {
if err := row.Scan(&id, &rec.PasswordHash, &rec.EmailAddress, &rec.DomainName, &rec.MaxRecipientsPerHour); err != nil {
if err == pgx.ErrNoRows {
return nil, nil
}
@@ -3,6 +3,9 @@ package postgres
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
@@ -37,8 +40,13 @@ func (r *OutboundRepository) FetchNextReady(ctx context.Context, workerID string
rows, err := r.pool.Query(ctx, `
WITH ready_jobs AS (
SELECT id FROM outbound_jobs
WHERE status IN ('QUEUED', 'DEFERRED')
AND next_attempt_at <= NOW()
WHERE (
(status IN ('QUEUED', 'DEFERRED') AND next_attempt_at <= NOW())
-- Reclaim jobs whose worker died mid-delivery. Without this
-- they stay SENDING forever and the message is silently
-- lost, which contradicts ADR-002's durability guarantee.
OR (status = 'SENDING' AND locked_until IS NOT NULL AND locked_until <= NOW())
)
ORDER BY next_attempt_at ASC
LIMIT $2
FOR UPDATE SKIP LOCKED
@@ -90,6 +98,22 @@ func (r *OutboundRepository) FetchNextReady(ctx context.Context, workerID string
return jobs, nil
}
// CountRecipientsSince counts queued recipients, not messages: one message to
// fifty addresses is fifty recipients against the limit, which is the number
// that matters for abuse.
func (r *OutboundRepository) CountRecipientsSince(ctx context.Context, mailboxID uuid.UUID, since time.Time) (int, error) {
var count int
err := r.pool.QueryRow(ctx, `
SELECT COUNT(*) FROM outbound_jobs
WHERE mailbox_id = $1 AND created_at >= $2
`, mailboxID, since).Scan(&count)
if err != nil {
return 0, fmt.Errorf("count recent recipients for mailbox %s: %w", mailboxID, err)
}
return count, nil
}
func (r *OutboundRepository) UpdateJob(ctx context.Context, job *entity.OutboundJob) error {
_, err := r.pool.Exec(ctx, `
UPDATE outbound_jobs