mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 00:14:18 +00:00
A draft saved offline now stays on the device under a stand-in id and goes to the server by itself when it's back. Until now every save was refused and the composer showed "not saved". - `lib/store/drafts`: one pending create per draft, however often it's saved. A draft from the server edited offline is replaced by create, then destroy, so a failure leaves a copy. Each create has a fixed Message-ID, looked up in Drafts after a lost response. A refused draft stays on the device with its error and can be opened. Changed on another device: both are kept and the user is told. - `JMAPClient`: stand-in ids in Email/get and Email/set resolve like calendar's, also when answering from the device. A file attached offline stays readable, and is uploaded before a save that names it. - Held files and kept mail files are stored in 1 MB chunks. Orphans are dropped at the end of a flush, counting what drafts need. - `lib/store/mail/attachments.ts`: attachments, inline images and raw messages per scope, newest first, after everything else in a pass, with eviction that spares pinned mail and never sees held files. - `mail.rawBlob` and `mail.rawStatus` for an offline export. Changed test assertions: `outbox.heldFile` and `mail.getBlob` return `blob` instead of `data`, so a file isn't held whole in memory. The attachment tests ignore raw downloads, and mail-sync runs with attachments off, because raw messages add a commit per message.
114 lines
3.9 KiB
TypeScript
114 lines
3.9 KiB
TypeScript
import type { Migration } from '../storage';
|
|
|
|
export const MAIL_MIGRATIONS: readonly Migration[] = [
|
|
{
|
|
id: 100,
|
|
name: 'mail',
|
|
sql: `
|
|
CREATE TABLE email (
|
|
rowid INTEGER PRIMARY KEY,
|
|
jmap TEXT NOT NULL,
|
|
id TEXT NOT NULL,
|
|
thread_id TEXT,
|
|
received_at INTEGER NOT NULL,
|
|
sent_at INTEGER,
|
|
size INTEGER NOT NULL DEFAULT 0,
|
|
has_attachment INTEGER NOT NULL DEFAULT 0,
|
|
subject_sort TEXT,
|
|
from_sort TEXT,
|
|
to_sort TEXT,
|
|
json TEXT NOT NULL,
|
|
-- 0 no body yet, 1 kept, 2 the server had none to give
|
|
body_state INTEGER NOT NULL DEFAULT 0,
|
|
gen INTEGER NOT NULL DEFAULT 0,
|
|
UNIQUE (jmap, id)
|
|
);
|
|
CREATE INDEX email_received ON email (jmap, received_at DESC, rowid DESC);
|
|
CREATE INDEX email_thread ON email (jmap, thread_id);
|
|
CREATE INDEX email_backfill ON email (jmap, body_state, received_at DESC);
|
|
CREATE TABLE email_mailbox (
|
|
email_rowid INTEGER NOT NULL REFERENCES email (rowid) ON DELETE CASCADE,
|
|
jmap TEXT NOT NULL,
|
|
mailbox_id TEXT NOT NULL,
|
|
received_at INTEGER NOT NULL,
|
|
PRIMARY KEY (email_rowid, mailbox_id)
|
|
) WITHOUT ROWID;
|
|
CREATE INDEX email_mailbox_list ON email_mailbox (jmap, mailbox_id, received_at DESC, email_rowid DESC);
|
|
CREATE TABLE email_keyword (
|
|
email_rowid INTEGER NOT NULL REFERENCES email (rowid) ON DELETE CASCADE,
|
|
keyword TEXT NOT NULL,
|
|
PRIMARY KEY (email_rowid, keyword)
|
|
) WITHOUT ROWID;
|
|
CREATE INDEX email_keyword_lookup ON email_keyword (keyword, email_rowid);
|
|
CREATE TABLE email_body (
|
|
email_rowid INTEGER PRIMARY KEY REFERENCES email (rowid) ON DELETE CASCADE,
|
|
json TEXT NOT NULL
|
|
);
|
|
CREATE TABLE thread (
|
|
jmap TEXT NOT NULL,
|
|
id TEXT NOT NULL,
|
|
email_ids TEXT NOT NULL,
|
|
PRIMARY KEY (jmap, id)
|
|
) WITHOUT ROWID;
|
|
CREATE VIRTUAL TABLE email_fts_word USING fts5(
|
|
subject, from_addr, to_addr, body,
|
|
content = '', contentless_delete = 1,
|
|
tokenize = 'unicode61 remove_diacritics 2'
|
|
);
|
|
CREATE VIRTUAL TABLE email_fts_tri USING fts5(
|
|
subject, from_addr, to_addr, body,
|
|
content = '', contentless_delete = 1,
|
|
tokenize = 'trigram remove_diacritics 1'
|
|
);
|
|
`,
|
|
},
|
|
{
|
|
id: 101,
|
|
name: 'mail attachments',
|
|
sql: `
|
|
-- The attachments and inline images of kept messages.
|
|
CREATE TABLE email_part (
|
|
jmap TEXT NOT NULL,
|
|
blob_id TEXT NOT NULL,
|
|
email_rowid INTEGER NOT NULL,
|
|
received_at INTEGER NOT NULL,
|
|
size INTEGER NOT NULL DEFAULT 0,
|
|
type TEXT,
|
|
name TEXT,
|
|
-- 1 when the server wouldn't give it: not asked for again
|
|
refused INTEGER NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (jmap, blob_id, email_rowid)
|
|
) WITHOUT ROWID;
|
|
CREATE INDEX email_part_message ON email_part (email_rowid);
|
|
CREATE INDEX email_part_newest ON email_part (jmap, received_at DESC);
|
|
CREATE TABLE blob (
|
|
jmap TEXT NOT NULL,
|
|
blob_id TEXT NOT NULL,
|
|
type TEXT,
|
|
size INTEGER NOT NULL,
|
|
at INTEGER NOT NULL,
|
|
data BLOB NOT NULL,
|
|
PRIMARY KEY (jmap, blob_id)
|
|
) WITHOUT ROWID;
|
|
`,
|
|
},
|
|
// In the drafts range: this came with drafts and attachments.
|
|
{
|
|
id: 410,
|
|
name: 'kept files in chunks, raw messages',
|
|
sql: `
|
|
-- Binding a whole file grows the WASM heap by its size, and that never shrinks.
|
|
-- A file counts as kept once its row in blob exists.
|
|
CREATE TABLE blob_chunk (
|
|
jmap TEXT NOT NULL,
|
|
blob_id TEXT NOT NULL,
|
|
seq INTEGER NOT NULL,
|
|
data BLOB NOT NULL,
|
|
PRIMARY KEY (jmap, blob_id, seq)
|
|
);
|
|
-- 1 for the whole message (Email.blobId), for an export.
|
|
ALTER TABLE email_part ADD COLUMN raw INTEGER NOT NULL DEFAULT 0;
|
|
`,
|
|
},
|
|
];
|