refactor(mail): one definition of the standard folders, and reconcile the drift

The console created Reports, the CLI that makes the first administrator did not,
so that account had no folder for DMARC and TLS-RPT reports. The list now lives
in one SQL function, provision_default_folders (migration 0014), which both call.

The same migration reconciles what the two paths had left wrong in existing
mailboxes: folder counters and the mailbox's used bytes are recomputed from the
rows (only where they differ, so it is safe to run again).

create-admin can now be tested: fail() throws instead of exiting, and the command
runs only when invoked as one. The new test fails on the old code (no Reports).
This commit is contained in:
Lucas Rayan committed 2026-10-16 13:19:55 -03:00
1 parent caf52c0225
commit a8941a6191
6 files changed
+310 -37

No files matched your search

@@ -0,0 +1,77 @@
-- One definition of "a mailbox's standard folders", and a reconciliation of the
-- cached numbers that two code paths used to keep on their own.
--
-- 1. provision_default_folders
--
-- The standard folders were created in two places that had already diverged:
-- the console's createMailbox added Reports, the CLI that creates the first
-- administrator did not, so that account had no folder for DMARC and TLS-RPT
-- reports and they fell into its INBOX. Both now call this function, so the
-- list lives here and nowhere else. It is idempotent: it adds only what is
-- missing, matching by name without regard to case.
CREATE OR REPLACE FUNCTION provision_default_folders(p_mailbox_id UUID)
RETURNS VOID
LANGUAGE sql
AS $$
INSERT INTO folders (mailbox_id, name, special_use)
SELECT p_mailbox_id, f.name, f.special_use
FROM (VALUES ('INBOX', 'inbox'),
('Sent', 'sent'),
('Drafts', 'drafts'),
('Trash', 'trash'),
('Junk', 'junk'),
('Archive', 'archive'),
-- Where delivered DMARC and TLS-RPT reports are filed. IMAP has no
-- special-use role for these, so it is matched by name.
('Reports', NULL)) AS f(name, special_use)
WHERE NOT EXISTS (
SELECT 1 FROM folders x
WHERE x.mailbox_id = p_mailbox_id
AND x.parent_id IS NULL
AND LOWER(x.name) = LOWER(f.name));
$$;
-- Mailboxes created before the function that lack a standard folder get it now.
SELECT provision_default_folders(id) FROM mailboxes;
-- 2. Reconcile the cached numbers with the rows.
--
-- Folder counters, and the mailbox's used bytes, are denormalised caches. IMAP
-- and the webmail each maintained them and each got something wrong (IMAP never
-- moved the unread counter; the webmail never gave the quota back), so existing
-- mailboxes carry the drift. Every path now goes through one MailStore that
-- keeps them true; this corrects what is already stored, once. It only touches
-- rows that are actually wrong, so it is safe to run again.
UPDATE folders f
SET unread_count = c.unread,
total_count = c.total
FROM (
SELECT f2.id,
COUNT(e.id) FILTER (
WHERE NOT EXISTS (
SELECT 1 FROM message_flags mf
WHERE mf.message_id = e.id
AND mf.received_at = e.received_at
AND mf.flag = '\Seen')
)::int AS unread,
COUNT(e.id)::int AS total
FROM folders f2
LEFT JOIN email_messages e
ON e.folder_id = f2.id AND e.expunged_at IS NULL
GROUP BY f2.id
) AS c
WHERE f.id = c.id
AND (f.unread_count IS DISTINCT FROM c.unread
OR f.total_count IS DISTINCT FROM c.total);
UPDATE mailboxes m
SET used_bytes = COALESCE(s.total, 0)
FROM mailboxes m2
LEFT JOIN (
SELECT mailbox_id, SUM(size_bytes)::bigint AS total
FROM email_messages
WHERE expunged_at IS NULL
GROUP BY mailbox_id
) AS s ON s.mailbox_id = m2.id
WHERE m.id = m2.id
AND m.used_bytes IS DISTINCT FROM COALESCE(s.total, 0);