Files
mailgoserver/internal/db/crud_relay_queue.go
T
2026-08-21 04:46:46 +01:00

118 lines
4.4 KiB
Go

package db
import (
"encoding/json"
"time"
)
// RelayQueueRecipient is one entry of a RelayQueueItem's recipients_json — mirrors
// relay.Result's Recipient/RecipientType shape closely enough to round-trip without
// internal/db needing to import internal/relay (which would be a cycle: relay already
// imports db).
type RelayQueueRecipient struct {
Recipient string `json:"recipient"`
Type string `json:"type"`
}
// RelayQueueItem is one recipient-domain-group of in-flight outbound relay work — see
// schema.go's esrv_relay_queue comment.
type RelayQueueItem struct {
ID int64
EmailLogID int64
MailFrom string
Domain string
Recipients []RelayQueueRecipient
Content string
Status string
Attempts int
NextAttemptAt time.Time
LastError string
CreatedAt time.Time
}
// InsertRelayQueueItem queues one recipient-domain-group for delivery, due at
// nextAttemptAt (normally time.Now() for a fresh message — the worker's next tick
// picks it up promptly).
func (d *DB) InsertRelayQueueItem(emailLogID int64, mailFrom, domain string, recipients []RelayQueueRecipient, content string, nextAttemptAt time.Time) (int64, error) {
recipientsJSON, err := json.Marshal(recipients)
if err != nil {
return 0, err
}
res, err := d.Exec(`INSERT INTO esrv_relay_queue
(email_log_id, mail_from, domain, recipients_json, content, next_attempt_at)
VALUES (?, ?, ?, ?, ?, ?)`,
emailLogID, mailFrom, domain, string(recipientsJSON), content, nextAttemptAt)
if err != nil {
return 0, err
}
return res.LastInsertId()
}
// ClaimDueRelayQueueItems selects up to limit rows that are due (status='pending' and
// next_attempt_at in the past) and immediately flips them to status='sending' so a
// second worker tick can't pick the same row up again — a plain select-then-update,
// not a single atomic statement, but safe here: sqlDB.SetMaxOpenConns(1) (schema.go)
// already serializes every DB access app-wide through one connection, so there's no
// real cross-goroutine race to guard against, only "don't process the same row twice
// within one process," which this already prevents.
func (d *DB) ClaimDueRelayQueueItems(limit int) ([]RelayQueueItem, error) {
rows, err := d.Query(`SELECT id, email_log_id, mail_from, domain, recipients_json, content, attempts, next_attempt_at, created_at
FROM esrv_relay_queue WHERE status = 'pending' AND next_attempt_at <= ? ORDER BY next_attempt_at LIMIT ?`,
time.Now(), limit)
if err != nil {
return nil, err
}
var items []RelayQueueItem
for rows.Next() {
var it RelayQueueItem
var recipientsJSON, nextAttemptAt, createdAt string
if err := rows.Scan(&it.ID, &it.EmailLogID, &it.MailFrom, &it.Domain, &recipientsJSON, &it.Content, &it.Attempts, &nextAttemptAt, &createdAt); err != nil {
rows.Close()
return nil, err
}
if err := json.Unmarshal([]byte(recipientsJSON), &it.Recipients); err != nil {
rows.Close()
return nil, err
}
it.NextAttemptAt, _ = parseTime(nextAttemptAt)
it.CreatedAt, _ = parseTime(createdAt)
it.Status = "sending"
items = append(items, it)
}
if err := rows.Err(); err != nil {
rows.Close()
return nil, err
}
rows.Close()
for _, it := range items {
if _, err := d.Exec(`UPDATE esrv_relay_queue SET status = 'sending' WHERE id = ?`, it.ID); err != nil {
return nil, err
}
}
return items, nil
}
// RescheduleRelayQueueItem records a failed delivery attempt and puts the row back to
// 'pending' at the given next attempt time (retry backoff — see
// internal/relay/queue.go's retrySchedule).
func (d *DB) RescheduleRelayQueueItem(id int64, nextAttemptAt time.Time, lastError string) error {
_, err := d.Exec(`UPDATE esrv_relay_queue SET status = 'pending', attempts = attempts + 1, next_attempt_at = ?, last_error = ? WHERE id = ?`,
nextAttemptAt, lastError, id)
return err
}
func (d *DB) DeleteRelayQueueItem(id int64) error {
_, err := d.Exec(`DELETE FROM esrv_relay_queue WHERE id = ?`, id)
return err
}
// CountPendingRelayQueueItemsForEmailLog reports how many domain-groups for one
// message are still unresolved — the worker uses this to know when it's the one
// finalizing an email_log's overall status (all groups either delivered or given up).
func (d *DB) CountPendingRelayQueueItemsForEmailLog(emailLogID int64) (int, error) {
var n int
err := d.QueryRow(`SELECT COUNT(*) FROM esrv_relay_queue WHERE email_log_id = ?`, emailLogID).Scan(&n)
return n, err
}