mirror of
https://github.com/neilalexander/yggmail.git
synced 2026-05-24 20:06:28 +03:00
Queue messages for re-send
This commit is contained in:
@@ -126,7 +126,7 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
queues := smtpsender.NewQueues(cfg, log, transport)
|
queues := smtpsender.NewQueues(cfg, log, transport, storage)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
package smtpsender
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type fifoQueue struct {
|
|
||||||
frames []interface{}
|
|
||||||
count int
|
|
||||||
mutex sync.Mutex
|
|
||||||
notifs chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newFIFOQueue() *fifoQueue {
|
|
||||||
q := &fifoQueue{
|
|
||||||
notifs: make(chan struct{}),
|
|
||||||
}
|
|
||||||
return q
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *fifoQueue) push(frame interface{}) bool {
|
|
||||||
q.mutex.Lock()
|
|
||||||
defer q.mutex.Unlock()
|
|
||||||
q.frames = append(q.frames, frame)
|
|
||||||
q.count++
|
|
||||||
select {
|
|
||||||
case q.notifs <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *fifoQueue) pop() (interface{}, bool) {
|
|
||||||
q.mutex.Lock()
|
|
||||||
defer q.mutex.Unlock()
|
|
||||||
if q.count == 0 {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
frame := q.frames[0]
|
|
||||||
q.frames[0] = nil
|
|
||||||
q.frames = q.frames[1:]
|
|
||||||
q.count--
|
|
||||||
if q.count == 0 {
|
|
||||||
q.frames = nil
|
|
||||||
}
|
|
||||||
return frame, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *fifoQueue) wait() <-chan struct{} {
|
|
||||||
q.mutex.Lock()
|
|
||||||
defer q.mutex.Unlock()
|
|
||||||
if q.count > 0 {
|
|
||||||
ch := make(chan struct{})
|
|
||||||
close(ch)
|
|
||||||
return ch
|
|
||||||
}
|
|
||||||
return q.notifs
|
|
||||||
}
|
|
||||||
@@ -4,13 +4,15 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"net/mail"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
"github.com/neilalexander/yggmail/internal/config"
|
"github.com/neilalexander/yggmail/internal/config"
|
||||||
|
"github.com/neilalexander/yggmail/internal/storage"
|
||||||
"github.com/neilalexander/yggmail/internal/transport"
|
"github.com/neilalexander/yggmail/internal/transport"
|
||||||
|
"github.com/neilalexander/yggmail/internal/utils"
|
||||||
"go.uber.org/atomic"
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,22 +20,63 @@ type Queues struct {
|
|||||||
Config *config.Config
|
Config *config.Config
|
||||||
Log *log.Logger
|
Log *log.Logger
|
||||||
Transport transport.Transport
|
Transport transport.Transport
|
||||||
|
Storage storage.Storage
|
||||||
queues sync.Map // servername -> *Queue
|
queues sync.Map // servername -> *Queue
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewQueues(config *config.Config, log *log.Logger, transport transport.Transport) *Queues {
|
func NewQueues(config *config.Config, log *log.Logger, transport transport.Transport, storage storage.Storage) *Queues {
|
||||||
return &Queues{
|
qs := &Queues{
|
||||||
Config: config,
|
Config: config,
|
||||||
Log: log,
|
Log: log,
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
|
Storage: storage,
|
||||||
}
|
}
|
||||||
|
go qs.manager()
|
||||||
|
return qs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qs *Queues) QueueFor(server string) (*Queue, error) {
|
func (qs *Queues) manager() {
|
||||||
|
destinations, err := qs.Storage.QueueListDestinations()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, destination := range destinations {
|
||||||
|
_, _ = qs.queueFor(destination)
|
||||||
|
}
|
||||||
|
time.AfterFunc(time.Minute*5, qs.manager)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qs *Queues) QueueFor(from string, rcpts []string, content []byte) error {
|
||||||
|
pid, err := qs.Storage.MailCreate("Outbox", content)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("q.queues.Storage.MailCreate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rcpt := range rcpts {
|
||||||
|
addr, err := mail.ParseAddress(rcpt)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("mail.ParseAddress: %w", err)
|
||||||
|
}
|
||||||
|
pk, err := utils.ParseAddress(addr.Address)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parseAddress: %w", err)
|
||||||
|
}
|
||||||
|
host := hex.EncodeToString(pk)
|
||||||
|
|
||||||
|
if err := qs.Storage.QueueInsertDestinationForID(host, pid, from, rcpt); err != nil {
|
||||||
|
return fmt.Errorf("qs.Storage.QueueInsertDestinationForID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = qs.queueFor(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qs *Queues) queueFor(server string) (*Queue, error) {
|
||||||
v, _ := qs.queues.LoadOrStore(server, &Queue{
|
v, _ := qs.queues.LoadOrStore(server, &Queue{
|
||||||
queues: qs,
|
queues: qs,
|
||||||
destination: server,
|
destination: server,
|
||||||
fifo: newFIFOQueue(),
|
|
||||||
})
|
})
|
||||||
q, ok := v.(*Queue)
|
q, ok := v.(*Queue)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -49,40 +92,32 @@ type Queue struct {
|
|||||||
queues *Queues
|
queues *Queues
|
||||||
destination string
|
destination string
|
||||||
running atomic.Bool
|
running atomic.Bool
|
||||||
backoff atomic.Int64
|
|
||||||
fifo *fifoQueue
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queue) Queue(mail *QueuedMail) error {
|
|
||||||
q.fifo.push(mail)
|
|
||||||
if q.running.CAS(false, true) {
|
|
||||||
go q.run()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queue) run() {
|
func (q *Queue) run() {
|
||||||
defer q.running.Store(false)
|
defer q.running.Store(false)
|
||||||
for {
|
defer q.queues.Storage.MailExpunge("Outbox") // nolint:errcheck
|
||||||
select {
|
|
||||||
case <-q.fifo.wait():
|
refs, err := q.queues.Storage.QueueMailIDsForDestination(q.destination)
|
||||||
case <-time.After(time.Second * 10):
|
if err != nil {
|
||||||
return
|
q.queues.Log.Println("Error with queue:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
q.queues.Log.Println("There are", len(refs), "mail(s) queued for", q.destination)
|
||||||
|
|
||||||
|
for _, ref := range refs {
|
||||||
|
_, mail, err := q.queues.Storage.MailSelect("Outbox", ref.ID)
|
||||||
|
if err != nil {
|
||||||
|
q.queues.Log.Println("Failed to get mail", ref.ID, "due to error:", err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
item, ok := q.fifo.pop()
|
q.queues.Log.Println("Sending mail from", ref.From, "to", q.destination)
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mail, ok := item.(*QueuedMail)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
q.queues.Log.Println("Processing mail from", mail.From, "to", mail.Destination)
|
|
||||||
|
|
||||||
if err := func() error {
|
if err := func() error {
|
||||||
conn, err := q.queues.Transport.Dial(q.destination)
|
conn, err := q.queues.Transport.Dial(q.destination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
q.queues.Log.Println("Failed to dial destination", q.destination, "due to error:", err)
|
||||||
return fmt.Errorf("q.queues.Transport.Dial: %w", err)
|
return fmt.Errorf("q.queues.Transport.Dial: %w", err)
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
@@ -98,14 +133,12 @@ func (q *Queue) run() {
|
|||||||
return fmt.Errorf("client.Hello: %w", err)
|
return fmt.Errorf("client.Hello: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
q.backoff.Store(0)
|
if err := client.Mail(ref.From, nil); err != nil {
|
||||||
|
|
||||||
if err := client.Mail(mail.From, nil); err != nil {
|
|
||||||
q.queues.Log.Println("Remote server", q.destination, "did not accept MAIL:", err)
|
q.queues.Log.Println("Remote server", q.destination, "did not accept MAIL:", err)
|
||||||
return fmt.Errorf("client.Mail: %w", err)
|
return fmt.Errorf("client.Mail: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := client.Rcpt(mail.Rcpt); err != nil {
|
if err := client.Rcpt(ref.Rcpt); err != nil {
|
||||||
q.queues.Log.Println("Remote server", q.destination, "did not accept RCPT:", err)
|
q.queues.Log.Println("Remote server", q.destination, "did not accept RCPT:", err)
|
||||||
return fmt.Errorf("client.Rcpt: %w", err)
|
return fmt.Errorf("client.Rcpt: %w", err)
|
||||||
}
|
}
|
||||||
@@ -116,24 +149,26 @@ func (q *Queue) run() {
|
|||||||
}
|
}
|
||||||
defer writer.Close()
|
defer writer.Close()
|
||||||
|
|
||||||
if _, err := writer.Write(mail.Content); err != nil {
|
if _, err := writer.Write(mail.Mail); err != nil {
|
||||||
return fmt.Errorf("writer.Write: %w", err)
|
return fmt.Errorf("writer.Write: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := q.queues.Storage.QueueDeleteDestinationForID(q.destination, ref.ID); err != nil {
|
||||||
|
return fmt.Errorf("q.queues.Storage.QueueDeleteDestinationForID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if remaining, err := q.queues.Storage.QueueSelectIsMessagePendingSend("Outbox", ref.ID); err != nil {
|
||||||
|
return fmt.Errorf("q.queues.Storage.QueueSelectIsMessagePendingSend: %w", err)
|
||||||
|
} else if !remaining {
|
||||||
|
return q.queues.Storage.MailDelete("Outbox", ref.ID)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}(); err != nil {
|
}(); err != nil {
|
||||||
retry := time.Second * time.Duration(math.Exp2(float64(q.backoff.Inc())))
|
q.queues.Log.Println("Failed to send message:", err, "- will retry")
|
||||||
q.queues.Log.Println("Queue error:", err, "- will retry in", retry)
|
// TODO: Send a mail to the inbox on the first instance?
|
||||||
time.Sleep(retry)
|
|
||||||
} else {
|
} else {
|
||||||
q.queues.Log.Println("Sent mail from", mail.From, "to", mail.Destination)
|
q.queues.Log.Println("Sent mail from", ref.From, "to", q.destination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type QueuedMail struct {
|
|
||||||
From string // mail address
|
|
||||||
Rcpt string // mail addresses
|
|
||||||
Destination string // server name
|
|
||||||
Content []byte
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/emersion/go-message"
|
"github.com/emersion/go-message"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
"github.com/neilalexander/yggmail/internal/smtpsender"
|
|
||||||
"github.com/neilalexander/yggmail/internal/utils"
|
"github.com/neilalexander/yggmail/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,55 +54,17 @@ func (s *SessionLocal) Data(r io.Reader) error {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
servers := make(map[string]struct{})
|
var b bytes.Buffer
|
||||||
|
if err := m.WriteTo(&b); err != nil {
|
||||||
for _, rcpt := range s.rcpt {
|
return fmt.Errorf("m.WriteTo: %w", err)
|
||||||
pk, err := utils.ParseAddress(rcpt)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parseAddress: %w", err)
|
|
||||||
}
|
|
||||||
host := hex.EncodeToString(pk)
|
|
||||||
|
|
||||||
if _, ok := servers[host]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
servers[host] = struct{}{}
|
|
||||||
|
|
||||||
if pk.Equal(s.backend.Config.PublicKey) {
|
|
||||||
var b bytes.Buffer
|
|
||||||
if err := m.WriteTo(&b); err != nil {
|
|
||||||
return fmt.Errorf("m.WriteTo: %w", err)
|
|
||||||
}
|
|
||||||
if _, err := s.backend.Storage.MailCreate("INBOX", b.Bytes()); err != nil {
|
|
||||||
return fmt.Errorf("s.backend.Storage.StoreMessageFor: %w", err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
queue, err := s.backend.Queues.QueueFor(host)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("s.backend.Queues.QueueFor: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mail := &smtpsender.QueuedMail{
|
|
||||||
From: s.from,
|
|
||||||
Rcpt: rcpt,
|
|
||||||
Destination: host,
|
|
||||||
}
|
|
||||||
|
|
||||||
var b bytes.Buffer
|
|
||||||
if err := m.WriteTo(&b); err != nil {
|
|
||||||
return fmt.Errorf("m.WriteTo: %w", err)
|
|
||||||
}
|
|
||||||
mail.Content = b.Bytes()
|
|
||||||
|
|
||||||
if err := queue.Queue(mail); err != nil {
|
|
||||||
return fmt.Errorf("queue.Queue: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.backend.Log.Println("Queued mail for", mail.Destination)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.backend.Queues.QueueFor(s.from, s.rcpt, b.Bytes()); err != nil {
|
||||||
|
return fmt.Errorf("s.backend.Queues.QueueFor: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.backend.Log.Println("Queued mail for", s.rcpt)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type SQLite3Storage struct {
|
|||||||
*TableConfig
|
*TableConfig
|
||||||
*TableMailboxes
|
*TableMailboxes
|
||||||
*TableMails
|
*TableMails
|
||||||
|
*TableQueue
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSQLite3StorageStorage(filename string) (*SQLite3Storage, error) {
|
func NewSQLite3StorageStorage(filename string) (*SQLite3Storage, error) {
|
||||||
@@ -31,5 +32,9 @@ func NewSQLite3StorageStorage(filename string) (*SQLite3Storage, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("NewTableMails: %w", err)
|
return nil, fmt.Errorf("NewTableMails: %w", err)
|
||||||
}
|
}
|
||||||
|
s.TableQueue, err = NewTableQueue(db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("NewTableQueue: %w", err)
|
||||||
|
}
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,11 +163,13 @@ func (t *TableMails) MailCreate(mailbox string, data []byte) (int, error) {
|
|||||||
|
|
||||||
func (t *TableMails) MailSelect(mailbox string, id int) (int, *types.Mail, error) {
|
func (t *TableMails) MailSelect(mailbox string, id int) (int, *types.Mail, error) {
|
||||||
var seq int
|
var seq int
|
||||||
|
var datetime int64
|
||||||
mail := &types.Mail{}
|
mail := &types.Mail{}
|
||||||
err := t.selectMail.QueryRow(mailbox, id).Scan(
|
err := t.selectMail.QueryRow(mailbox, id).Scan(
|
||||||
&seq, &mail.ID, &mail.Mail, &mail.Date,
|
&seq, &mail.ID, &mail.Mail, &datetime,
|
||||||
&mail.Seen, &mail.Answered, &mail.Flagged, &mail.Deleted,
|
&mail.Seen, &mail.Answered, &mail.Flagged, &mail.Deleted,
|
||||||
)
|
)
|
||||||
|
mail.Date = time.Unix(datetime, 0)
|
||||||
return seq, mail, err
|
return seq, mail, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +213,7 @@ func (t *TableMails) MailUpdateFlags(mailbox string, id int, seen, answered, fla
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TableMails) MailDelete(mailbox, id string) error {
|
func (t *TableMails) MailDelete(mailbox string, id int) error {
|
||||||
_, err := t.deleteMail.Exec(mailbox, id)
|
_, err := t.deleteMail.Exec(mailbox, id)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
150
internal/storage/sqlite3/table_queue.go
Normal file
150
internal/storage/sqlite3/table_queue.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package sqlite3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/neilalexander/yggmail/internal/storage/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TableQueue struct {
|
||||||
|
db *sql.DB
|
||||||
|
queueSelectDestinations *sql.Stmt
|
||||||
|
queueSelectIDsForDestination *sql.Stmt
|
||||||
|
queueInsertDestinationForID *sql.Stmt
|
||||||
|
queueDeleteIDForDestination *sql.Stmt
|
||||||
|
queueSelectIsMessagePendingSend *sql.Stmt
|
||||||
|
}
|
||||||
|
|
||||||
|
const queueSchema = `
|
||||||
|
CREATE TABLE IF NOT EXISTS queue (
|
||||||
|
destination TEXT NOT NULL,
|
||||||
|
mailbox TEXT NOT NULL,
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
mail TEXT NOT NULL,
|
||||||
|
rcpt TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (destination, mailbox, id),
|
||||||
|
FOREIGN KEY (mailbox, id) REFERENCES mails(mailbox, id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
`
|
||||||
|
|
||||||
|
const queueSelectDestinationsStmt = `
|
||||||
|
SELECT DISTINCT destination FROM queue
|
||||||
|
`
|
||||||
|
|
||||||
|
const queueSelectIDsForDestinationStmt = `
|
||||||
|
SELECT id, mail, rcpt FROM queue WHERE destination = $1
|
||||||
|
ORDER BY id DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
const queueInsertDestinationForIDStmt = `
|
||||||
|
INSERT INTO queue (destination, mailbox, id, mail, rcpt) VALUES($1, $2, $3, $4, $5)
|
||||||
|
`
|
||||||
|
|
||||||
|
const deleteDestinationForIDStmt = `
|
||||||
|
DELETE FROM queue WHERE destination = $1 AND mailbox = $2 AND id = $3
|
||||||
|
`
|
||||||
|
|
||||||
|
const queueSelectIsMessagePendingSendStmt = `
|
||||||
|
SELECT COUNT(*) FROM queue WHERE mailbox = $1 AND id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
func NewTableQueue(db *sql.DB) (*TableQueue, error) {
|
||||||
|
t := &TableQueue{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
_, err := db.Exec(queueSchema)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("db.Exec: %w", err)
|
||||||
|
}
|
||||||
|
t.queueSelectDestinations, err = db.Prepare(queueSelectDestinationsStmt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("db.Prepare(queueSelectDestinationsStmt): %w", err)
|
||||||
|
}
|
||||||
|
t.queueSelectIDsForDestination, err = db.Prepare(queueSelectIDsForDestinationStmt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("db.Prepare(queueSelectIDsForDestinationStmt): %w", err)
|
||||||
|
}
|
||||||
|
t.queueInsertDestinationForID, err = db.Prepare(queueInsertDestinationForIDStmt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("db.Prepare(queueInsertDestinationForIDStmt): %w", err)
|
||||||
|
}
|
||||||
|
t.queueDeleteIDForDestination, err = db.Prepare(deleteDestinationForIDStmt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("db.Prepare(deleteDestinationForIDStmt): %w", err)
|
||||||
|
}
|
||||||
|
t.queueSelectIsMessagePendingSend, err = db.Prepare(queueSelectIsMessagePendingSendStmt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("db.Prepare(queueSelectIsMessagePendingSendStmt): %w", err)
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TableQueue) QueueListDestinations() ([]string, error) {
|
||||||
|
rows, err := t.queueSelectDestinations.Query()
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("t.queueSelectDestinations.Query: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var destinations []string
|
||||||
|
for rows.Next() {
|
||||||
|
var destination string
|
||||||
|
if err := rows.Scan(&destination); err != nil {
|
||||||
|
return nil, fmt.Errorf("rows.Scan: %w", err)
|
||||||
|
}
|
||||||
|
destinations = append(destinations, destination)
|
||||||
|
}
|
||||||
|
return destinations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TableQueue) QueueMailIDsForDestination(destination string) ([]types.QueuedMail, error) {
|
||||||
|
rows, err := t.queueSelectIDsForDestination.Query(destination)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("t.queueSelectDestinations.Query: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var ids []types.QueuedMail
|
||||||
|
for rows.Next() {
|
||||||
|
var id int
|
||||||
|
var from, rcpt string
|
||||||
|
if err := rows.Scan(&id, &from, &rcpt); err != nil {
|
||||||
|
return nil, fmt.Errorf("rows.Scan: %w", err)
|
||||||
|
}
|
||||||
|
ids = append(ids, types.QueuedMail{
|
||||||
|
ID: id,
|
||||||
|
From: from,
|
||||||
|
Rcpt: rcpt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TableQueue) QueueInsertDestinationForID(destination string, id int, from, rcpt string) error {
|
||||||
|
_, err := t.queueInsertDestinationForID.Exec(destination, "Outbox", id, from, rcpt)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TableQueue) QueueDeleteDestinationForID(destination string, id int) error {
|
||||||
|
_, err := t.queueDeleteIDForDestination.Exec(destination, "Outbox", id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TableQueue) QueueSelectIsMessagePendingSend(mailbox string, id int) (bool, error) {
|
||||||
|
row := t.queueSelectIsMessagePendingSend.QueryRow(mailbox, id)
|
||||||
|
if err := row.Err(); err != nil && err != sql.ErrNoRows {
|
||||||
|
return false, fmt.Errorf("row.Err: %w", err)
|
||||||
|
} else if err == sql.ErrNoRows {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
var count int
|
||||||
|
if err := row.Scan(&count); err != nil {
|
||||||
|
return false, fmt.Errorf("row.Scan: %w", err)
|
||||||
|
}
|
||||||
|
return count > 0, nil
|
||||||
|
}
|
||||||
@@ -22,7 +22,13 @@ type Storage interface {
|
|||||||
MailSelect(mailbox string, id int) (int, *types.Mail, error)
|
MailSelect(mailbox string, id int) (int, *types.Mail, error)
|
||||||
MailSearch(mailbox string) ([]uint32, error)
|
MailSearch(mailbox string) ([]uint32, error)
|
||||||
MailUpdateFlags(mailbox string, id int, seen, answered, flagged, deleted bool) error
|
MailUpdateFlags(mailbox string, id int, seen, answered, flagged, deleted bool) error
|
||||||
MailDelete(mailbox, id string) error
|
MailDelete(mailbox string, id int) error
|
||||||
MailExpunge(mailbox string) error
|
MailExpunge(mailbox string) error
|
||||||
MailCount(mailbox string) (int, error)
|
MailCount(mailbox string) (int, error)
|
||||||
|
|
||||||
|
QueueListDestinations() ([]string, error)
|
||||||
|
QueueMailIDsForDestination(destination string) ([]types.QueuedMail, error)
|
||||||
|
QueueInsertDestinationForID(destination string, id int, from, rcpt string) error
|
||||||
|
QueueDeleteDestinationForID(destination string, id int) error
|
||||||
|
QueueSelectIsMessagePendingSend(mailbox string, id int) (bool, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,3 +12,9 @@ type Mail struct {
|
|||||||
Flagged bool
|
Flagged bool
|
||||||
Deleted bool
|
Deleted bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QueuedMail struct {
|
||||||
|
ID int
|
||||||
|
From string
|
||||||
|
Rcpt string
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user