-- IMAP (75.2): UIDs per folder, IMAP flags, the original message text -- UIDVALIDITY: never changes for a folder; UIDNEXT: the next UID to give out ALTER TABLE folders ADD COLUMN uid_validity BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM now())::BIGINT); ALTER TABLE folders ADD COLUMN uid_next BIGINT NOT NULL DEFAULT 1; -- UID inside the folder; strictly increasing — a message moved into a folder gets a new, larger UID ALTER TABLE messages ADD COLUMN uid BIGINT; ALTER TABLE messages ADD COLUMN is_answered BOOLEAN NOT NULL DEFAULT FALSE; ALTER TABLE messages ADD COLUMN is_deleted BOOLEAN NOT NULL DEFAULT FALSE; UPDATE messages m SET uid = numbered.n FROM (SELECT id, ROW_NUMBER() OVER (PARTITION BY folder_id ORDER BY received_at, id) AS n FROM messages) numbered WHERE m.id = numbered.id; UPDATE folders f SET uid_next = COALESCE((SELECT MAX(uid) FROM messages WHERE folder_id = f.id), 0) + 1; ALTER TABLE messages ALTER COLUMN uid SET NOT NULL; CREATE UNIQUE INDEX idx_messages_folder_uid ON messages(folder_id, uid); -- The UID is given out by the database, so every path that stores or moves a message (SMTP, web client, IMAP) -- gets it right without knowing about IMAP CREATE FUNCTION assign_message_uid() RETURNS trigger AS $$ BEGIN UPDATE folders SET uid_next = uid_next + 1 WHERE id = NEW.folder_id RETURNING uid_next - 1 INTO NEW.uid; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER messages_uid_on_insert BEFORE INSERT ON messages FOR EACH ROW EXECUTE FUNCTION assign_message_uid(); -- The web client saves the whole row (folder_id included) — only a real move gets a new UID CREATE TRIGGER messages_uid_on_move BEFORE UPDATE OF folder_id ON messages FOR EACH ROW WHEN (NEW.folder_id IS DISTINCT FROM OLD.folder_id) EXECUTE FUNCTION assign_message_uid(); -- The message exactly as received (SMTP) or appended (IMAP APPEND); for messages without it the server builds -- the MIME text once from the stored fields and keeps it here, so sizes and bytes never change between fetches. -- Removed when a draft changes. CREATE TABLE message_sources ( message_id BIGINT PRIMARY KEY REFERENCES messages(id) ON DELETE CASCADE, raw BYTEA NOT NULL );