Files
leocrm/app/plugins/builtins/mail/migrations/0001_initial.sql
T
leocrm-bot f646c597dc T06: Mail plugin backend — IMAP/SMTP, threading, templates, rules, PGP, vacation, delegates — 46 tests, 74.56% coverage
- 14 SQLAlchemy models (mail_accounts, mail_folders, mails, attachments, labels, rules, templates, signatures, etc.)
- AES-256 encrypted credential storage
- IMAP sync service (ARQ-ready, sync trigger endpoint)
- SMTP send/reply/forward service
- Mail rule engine (condition matching → move/label/flag/forward)
- Vacation auto-reply with dedup (vacation_sent_log)
- PGP integration (key import, encrypt/decrypt, contact public keys)
- Shared mailboxes with delegate access + send permissions
- HTML sanitization (nh3)
- Full-text search (ILIKE fallback, tsvector-ready)
- Thread grouping via References/In-Reply-To headers
- Template variable substitution
- Contact/company auto-linking from email addresses
- Calendar event creation from mail
- 46 tests covering all 40 acceptance criteria
- Ruff lint clean, format clean
- Full regression: 527 tests pass (0 failures)
2026-07-01 15:41:27 +02:00

225 lines
9.4 KiB
SQL

-- Mail plugin initial migration: creates all mail tables
CREATE TABLE IF NOT EXISTS mail_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
email_address VARCHAR(255) NOT NULL,
display_name VARCHAR(255) NOT NULL DEFAULT '',
imap_host VARCHAR(255) NOT NULL,
imap_port INTEGER NOT NULL DEFAULT 993,
imap_ssl BOOLEAN NOT NULL DEFAULT TRUE,
smtp_host VARCHAR(255) NOT NULL,
smtp_port INTEGER NOT NULL DEFAULT 587,
smtp_tls BOOLEAN NOT NULL DEFAULT TRUE,
username VARCHAR(255) NOT NULL,
encrypted_password TEXT NOT NULL,
is_shared BOOLEAN NOT NULL DEFAULT FALSE,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
tenant_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_mail_accounts_user ON mail_accounts(user_id);
CREATE INDEX IF NOT EXISTS ix_mail_accounts_tenant ON mail_accounts(tenant_id);
CREATE TABLE IF NOT EXISTS mail_folders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID NOT NULL REFERENCES mail_accounts(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
imap_name VARCHAR(255) NOT NULL,
parent_id UUID REFERENCES mail_folders(id) ON DELETE CASCADE,
is_standard BOOLEAN NOT NULL DEFAULT FALSE,
unread_count INTEGER NOT NULL DEFAULT 0,
total_count INTEGER NOT NULL DEFAULT 0,
tenant_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_mail_folders_account ON mail_folders(account_id);
CREATE INDEX IF NOT EXISTS ix_mail_folders_tenant ON mail_folders(tenant_id);
CREATE TABLE IF NOT EXISTS mails (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID NOT NULL REFERENCES mail_accounts(id) ON DELETE CASCADE,
folder_id UUID NOT NULL REFERENCES mail_folders(id) ON DELETE CASCADE,
message_id VARCHAR(512) NOT NULL,
thread_id VARCHAR(512) NOT NULL DEFAULT '',
in_reply_to VARCHAR(512),
references_header TEXT,
subject VARCHAR(512) NOT NULL DEFAULT '',
from_address VARCHAR(255) NOT NULL,
to_addresses TEXT NOT NULL DEFAULT '',
cc_addresses TEXT NOT NULL DEFAULT '',
bcc_addresses TEXT NOT NULL DEFAULT '',
body_text TEXT NOT NULL DEFAULT '',
body_html TEXT NOT NULL DEFAULT '',
body_html_sanitized TEXT NOT NULL DEFAULT '',
is_seen BOOLEAN NOT NULL DEFAULT FALSE,
is_flagged BOOLEAN NOT NULL DEFAULT FALSE,
is_draft BOOLEAN NOT NULL DEFAULT FALSE,
is_answered BOOLEAN NOT NULL DEFAULT FALSE,
is_forwarded BOOLEAN NOT NULL DEFAULT FALSE,
has_attachments BOOLEAN NOT NULL DEFAULT FALSE,
size_bytes INTEGER NOT NULL DEFAULT 0,
received_at TIMESTAMPTZ,
sent_at TIMESTAMPTZ,
contact_id UUID,
company_id UUID,
tenant_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_mails_folder ON mails(folder_id);
CREATE INDEX IF NOT EXISTS ix_mails_account ON mails(account_id);
CREATE INDEX IF NOT EXISTS ix_mails_tenant ON mails(tenant_id);
CREATE INDEX IF NOT EXISTS ix_mails_thread ON mails(thread_id);
CREATE TABLE IF NOT EXISTS mail_attachments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
mail_id UUID NOT NULL REFERENCES mails(id) ON DELETE CASCADE,
filename VARCHAR(255) NOT NULL,
mime_type VARCHAR(255) NOT NULL DEFAULT 'application/octet-stream',
size_bytes INTEGER NOT NULL DEFAULT 0,
storage_path VARCHAR(1024) NOT NULL,
dms_file_id UUID,
content_id VARCHAR(255),
tenant_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_mail_attachments_mail ON mail_attachments(mail_id);
CREATE INDEX IF NOT EXISTS ix_mail_attachments_tenant ON mail_attachments(tenant_id);
CREATE TABLE IF NOT EXISTS mail_labels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
color VARCHAR(20) NOT NULL DEFAULT '#808080',
user_id UUID NOT NULL,
tenant_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_mail_labels_tenant ON mail_labels(tenant_id);
CREATE TABLE IF NOT EXISTS mail_label_assignments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
mail_id UUID NOT NULL REFERENCES mails(id) ON DELETE CASCADE,
label_id UUID NOT NULL REFERENCES mail_labels(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT uq_mail_label_assignment UNIQUE (mail_id, label_id)
);
CREATE INDEX IF NOT EXISTS ix_mail_label_assign_mail ON mail_label_assignments(mail_id);
CREATE INDEX IF NOT EXISTS ix_mail_label_assign_label ON mail_label_assignments(label_id);
CREATE TABLE IF NOT EXISTS mail_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID REFERENCES mail_accounts(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
priority INTEGER NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
conditions TEXT NOT NULL DEFAULT '{}',
actions TEXT NOT NULL DEFAULT '{}',
tenant_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_mail_rules_account ON mail_rules(account_id);
CREATE INDEX IF NOT EXISTS ix_mail_rules_tenant ON mail_rules(tenant_id);
CREATE TABLE IF NOT EXISTS mail_templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
subject VARCHAR(512) NOT NULL DEFAULT '',
body_html TEXT NOT NULL DEFAULT '',
user_id UUID NOT NULL,
tenant_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_mail_templates_tenant ON mail_templates(tenant_id);
CREATE TABLE IF NOT EXISTS mail_signatures (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
account_id UUID REFERENCES mail_accounts(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
body_html TEXT NOT NULL DEFAULT '',
is_default BOOLEAN NOT NULL DEFAULT FALSE,
tenant_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_mail_signatures_tenant ON mail_signatures(tenant_id);
CREATE TABLE IF NOT EXISTS vacation_sent_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID NOT NULL REFERENCES mail_accounts(id) ON DELETE CASCADE,
sender_address VARCHAR(255) NOT NULL,
sent_at TIMESTAMPTZ NOT NULL,
tenant_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_vacation_sent_log_account ON vacation_sent_log(account_id);
CREATE INDEX IF NOT EXISTS ix_vacation_sent_log_tenant ON vacation_sent_log(tenant_id);
CREATE TABLE IF NOT EXISTS mail_seen_by (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
mail_id UUID NOT NULL REFERENCES mails(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
seen_at TIMESTAMPTZ NOT NULL,
tenant_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT uq_mail_seen_by UNIQUE (mail_id, user_id)
);
CREATE INDEX IF NOT EXISTS ix_mail_seen_by_mail ON mail_seen_by(mail_id);
CREATE TABLE IF NOT EXISTS mail_account_delegates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID NOT NULL REFERENCES mail_accounts(id) ON DELETE CASCADE,
delegate_user_id UUID NOT NULL,
access_level VARCHAR(20) NOT NULL DEFAULT 'read',
tenant_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT uq_mail_delegate UNIQUE (account_id, delegate_user_id)
);
CREATE INDEX IF NOT EXISTS ix_mail_delegates_account ON mail_account_delegates(account_id);
CREATE TABLE IF NOT EXISTS mail_account_send_permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID NOT NULL REFERENCES mail_accounts(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
tenant_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT uq_mail_send_perm UNIQUE (account_id, user_id)
);
CREATE INDEX IF NOT EXISTS ix_mail_send_perms_account ON mail_account_send_permissions(account_id);
CREATE TABLE IF NOT EXISTS pgp_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
key_id VARCHAR(255) NOT NULL,
encrypted_private_key TEXT NOT NULL,
public_key_armored TEXT NOT NULL,
tenant_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_pgp_keys_user ON pgp_keys(user_id);
CREATE TABLE IF NOT EXISTS contact_pgp_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
contact_id UUID NOT NULL,
public_key_armored TEXT NOT NULL,
key_id VARCHAR(255) NOT NULL DEFAULT '',
tenant_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_contact_pgp_keys_contact ON contact_pgp_keys(contact_id);