-- 邮件收发(ADR-013):账号(配置 + 密封凭证 + 游标 + 跨进程租约)、入站留痕、逐订阅方投递留痕、出站留痕。 -- -- 腾讯企业邮箱两条接入路(IMAP/SMTP · 企业微信邮件 API)都没有带内容的推送,只能轮询拉: -- 两个后端连同一个库时,一个信箱同一时刻只许一个进程在拉 —— `polling_until` 与 -- `sys.task.advancing_until` 同款(一条 SQL 判 + 占,带过期时间,进程崩了不锁死)。 -- 入站两把唯一键:重复拉到(时钟偏 / UIDVALIDITY 重锚 / wecom 十分钟重叠窗)一律静默跳过。 -- 入站 / 出站表对账号**不设外键**:账号删了留痕还在。 create table if not exists sys.mail_account ( id text primary key, name text not null, kind text not null check (kind in ('imap-smtp', 'wecom')), address text not null default '', display_name text not null default '', enabled boolean not null default true, is_default boolean not null default false, poll_interval_sec integer not null default 30 check (poll_interval_sec between 10 and 3600), channel_id text, backfill_from timestamptz, settings jsonb not null default '{}'::jsonb, secret_cipher text, cursor jsonb, last_polled_at timestamptz, last_poll_error text, last_inbound_at timestamptz, polling_until timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create unique index if not exists mail_account_name_key on sys.mail_account (name); -- 默认发件账号至多一个 —— 用例先把别的置 false,这道索引兜底 create unique index if not exists mail_account_one_default on sys.mail_account ((true)) where is_default; create index if not exists mail_account_due_idx on sys.mail_account (enabled, last_polled_at); create table if not exists sys.mail_inbound ( id text primary key, account_id text not null, message_id text not null, provider_ref text not null, in_reply_to text, reference_ids jsonb not null default '[]'::jsonb, from_addr text not null default '', from_name text not null default '', to_list jsonb not null default '[]'::jsonb, cc_list jsonb not null default '[]'::jsonb, subject text not null default '', text_body text, html_body text, sent_at timestamptz, received_at timestamptz not null, attachments jsonb not null default '[]'::jsonb, raw bytea, raw_note text not null default '', size integer not null default 0, created_at timestamptz not null default now() ); create unique index if not exists mail_inbound_message_key on sys.mail_inbound (account_id, message_id); create unique index if not exists mail_inbound_provider_key on sys.mail_inbound (account_id, provider_ref); create index if not exists mail_inbound_time_idx on sys.mail_inbound (received_at desc); create table if not exists sys.mail_inbound_delivery ( mail_id text not null references sys.mail_inbound(id) on delete cascade, subscriber text not null, state text not null check (state in ('dispatching', 'done', 'failed', 'skipped')), error text, started_at timestamptz not null default now(), finished_at timestamptz, primary key (mail_id, subscriber) ); create table if not exists sys.mail_outbound ( id text primary key, account_id text not null, message_id text not null, idempotency_key text, to_list jsonb not null, cc_list jsonb not null default '[]'::jsonb, bcc_list jsonb not null default '[]'::jsonb, subject text not null, text_body text, html_body text, in_reply_to text, reference_ids jsonb not null default '[]'::jsonb, attachments jsonb not null default '[]'::jsonb, state text not null check (state in ('sending', 'sent', 'failed')), error text, provider_ref text, requested_by jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), sent_at timestamptz ); create unique index if not exists mail_outbound_idem_key on sys.mail_outbound (idempotency_key) where idempotency_key is not null; create index if not exists mail_outbound_time_idx on sys.mail_outbound (created_at desc);