Старый IMAP (выключен, .block() на сетевом потоке) удалён, написан заново:
- ImapServer: декодер целых команд с литералами ({n} после "+ Ready", {n+}),
команды по одной на рабочем потоке; STARTTLS (без выполнения открытого текста
после него) и неявный TLS; IDLE с опросом раз в 5 с до DONE
- ImapSession: IMAP4rev1 + LITERAL+, SASL-IR, IDLE, UIDPLUS, MOVE, UNSELECT,
NAMESPACE, SPECIAL-USE, ID; FETCH (ENVELOPE, BODYSTRUCTURE, BODY[section]<partial>),
SEARCH, STORE, COPY/MOVE с COPYUID, APPEND с APPENDUID, EXPUNGE/UID EXPUNGE
- MimeTree: части письма по байтам — размеры BODYSTRUCTURE совпадают с BODY[]
- V4: UID на папку выдаёт триггер (перенос из веба тоже получает новый UID),
флаги \Answered/\Deleted, message_sources — письмо как пришло (SMTP, APPEND)
или собранное один раз (MimeSource); правка черновика сбрасывает его
- тесты: настоящий клиент Angus IMAP, сырые литералы/IDLE/подмешивание (47 всего)
- живая проверка: обновление V3 -> V4 на PostgreSQL с письмами, Python imaplib:
STARTTLS, 993, папки, вложения, поиск, APPEND/COPY/MOVE/EXPUNGE, IDLE — 0 ошибок
α
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
44 lines
2.1 KiB
PL/PgSQL
44 lines
2.1 KiB
PL/PgSQL
-- 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
|
|
);
|