refactor(mail): one MailStore changes message state for every path
Flags, expunge, move, copy and folder deletion each existed twice: once for IMAP (FlagRepository, ExpungeRepository, CopyRepository) and once for the webmail (WebmailRepository). They kept the cached numbers true on their own and drifted: - reading a message over IMAP never moved the folder's unread counter - deleting in the webmail, or deleting a folder, never gave the quota back - an IMAP expunge could push a counter below zero MailStore now holds each state change once, in one transaction, with the same rules everywhere: counters never go negative, a change that alters what a client can observe raises the folder's modseq (one that alters nothing does not), the quota follows the bytes, and a message is located by folder. The four repositories are thin adapters that only translate how each path names a message. The contract test added before this change (which failed on 8 of its cases) now passes, as does the full suite.
This commit is contained in:
1 parent
e63b68acb2
commit
caf52c0225
5 files changed
+550
-593
No files matched your search
@@ -2,95 +2,23 @@ package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"lambdamail/protocols/internal/application/port"
|
||||
)
|
||||
|
||||
// FlagRepository implements port.FlagRepository against Postgres.
|
||||
// FlagRepository implements port.FlagRepository (IMAP STORE). The work is done
|
||||
// by MailStore, which every path shares; this only names the message the way
|
||||
// IMAP does, by folder and UID.
|
||||
type FlagRepository struct {
|
||||
pool *pgxpool.Pool
|
||||
store *MailStore
|
||||
}
|
||||
|
||||
func NewFlagRepository(pool *pgxpool.Pool) *FlagRepository {
|
||||
return &FlagRepository{pool: pool}
|
||||
return &FlagRepository{store: NewMailStore(pool)}
|
||||
}
|
||||
|
||||
func (r *FlagRepository) SetFlags(ctx context.Context, folderID string, uid uint32, op port.FlagOp, flags []string, unchangedSince uint64) (bool, error) {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("begin transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
var currentModSeq uint64
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT modseq FROM email_messages WHERE folder_id = $1 AND uid = $2 AND expunged_at IS NULL
|
||||
`, folderID, uid).Scan(¤tModSeq); err != nil {
|
||||
return false, fmt.Errorf("find message folder=%s uid=%d: %w", folderID, uid, err)
|
||||
}
|
||||
|
||||
if unchangedSince > 0 && currentModSeq > unchangedSince {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var newModSeq uint64
|
||||
if err := tx.QueryRow(ctx, `
|
||||
UPDATE folders SET highest_modseq = highest_modseq + 1 WHERE id = $1 RETURNING highest_modseq
|
||||
`, folderID).Scan(&newModSeq); err != nil {
|
||||
return false, fmt.Errorf("advance highest_modseq for folder %s: %w", folderID, err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE email_messages SET modseq = $1 WHERE folder_id = $2 AND uid = $3 AND expunged_at IS NULL
|
||||
`, newModSeq, folderID, uid); err != nil {
|
||||
return false, fmt.Errorf("update message modseq: %w", err)
|
||||
}
|
||||
|
||||
switch op {
|
||||
case port.FlagOpAdd:
|
||||
for _, flag := range flags {
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO message_flags (message_id, received_at, flag)
|
||||
SELECT id, received_at, $3 FROM email_messages WHERE folder_id = $1 AND uid = $2 AND expunged_at IS NULL
|
||||
ON CONFLICT DO NOTHING
|
||||
`, folderID, uid, flag); err != nil {
|
||||
return false, fmt.Errorf("add flag %q: %w", flag, err)
|
||||
}
|
||||
}
|
||||
case port.FlagOpDel:
|
||||
for _, flag := range flags {
|
||||
if _, err := tx.Exec(ctx, `
|
||||
DELETE FROM message_flags WHERE (message_id, received_at) IN (
|
||||
SELECT id, received_at FROM email_messages WHERE folder_id = $1 AND uid = $2 AND expunged_at IS NULL
|
||||
) AND flag = $3
|
||||
`, folderID, uid, flag); err != nil {
|
||||
return false, fmt.Errorf("delete flag %q: %w", flag, err)
|
||||
}
|
||||
}
|
||||
case port.FlagOpSet:
|
||||
if _, err := tx.Exec(ctx, `
|
||||
DELETE FROM message_flags WHERE (message_id, received_at) IN (
|
||||
SELECT id, received_at FROM email_messages WHERE folder_id = $1 AND uid = $2 AND expunged_at IS NULL
|
||||
)
|
||||
`, folderID, uid); err != nil {
|
||||
return false, fmt.Errorf("clear flags before set: %w", err)
|
||||
}
|
||||
for _, flag := range flags {
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO message_flags (message_id, received_at, flag)
|
||||
SELECT id, received_at, $3 FROM email_messages WHERE folder_id = $1 AND uid = $2 AND expunged_at IS NULL
|
||||
ON CONFLICT DO NOTHING
|
||||
`, folderID, uid, flag); err != nil {
|
||||
return false, fmt.Errorf("set flag %q: %w", flag, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return false, fmt.Errorf("commit: %w", err)
|
||||
}
|
||||
return true, nil
|
||||
return r.store.SetFlags(ctx, folderID, uid, op, flags, unchangedSince)
|
||||
}
|
||||
Reference in new issue
Block a user