updated layout for webmail
This commit is contained in:
+84
-3
@@ -316,6 +316,11 @@ CREATE TABLE IF NOT EXISTS esrv_mailbox_messages (
|
||||
-- other cached_* columns) so the folder list can show a preview snippet without
|
||||
-- decrypting the full message just to render the list.
|
||||
cached_preview TEXT NOT NULL DEFAULT '',
|
||||
-- Snapshot of folder taken the moment a message is moved to Trash (see
|
||||
-- db.MoveMessageToTrash), so "Restore" (db.RestoreMessage) can put it back where
|
||||
-- it came from instead of just dumping it in INBOX. Empty outside of that window
|
||||
-- (cleared again once restored).
|
||||
restore_folder TEXT NOT NULL DEFAULT '',
|
||||
storage_path TEXT NOT NULL,
|
||||
nonce BLOB NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
@@ -328,13 +333,47 @@ CREATE INDEX IF NOT EXISTS idx_mailbox_messages_folder ON esrv_mailbox_messages(
|
||||
|
||||
-- Explicit record of a mailbox's custom folders, so a freshly created (still empty)
|
||||
-- one shows up in the folder list — esrv_mailbox_messages.folder alone can only prove
|
||||
-- a folder exists once it holds at least one message. Standard folders (INBOX, Spam,
|
||||
-- Sent, Drafts, Trash) are never stored here; they're always shown by the webui
|
||||
-- regardless of this table.
|
||||
-- a folder exists once it holds at least one message. Standard folders (INBOX, Junk,
|
||||
-- Sent, Drafts, Trash) don't need a row to be listed — they're always shown by the
|
||||
-- webui regardless — but DO get one the first time they're renamed, dragged to a new
|
||||
-- position, or made a parent, purely to hold that metadata (see CreateMailboxFolder,
|
||||
-- SetFolderOrder).
|
||||
--
|
||||
-- Custom folders form a real tree, always rooted at one of the 5 standard folders
|
||||
-- (webui only ever offers "New folder" under INBOX, and "delete a folder" re-parents
|
||||
-- it under Trash — see webmailDeleteFolder). Exactly one of parent_id/parent_root is
|
||||
-- ever set per row: parent_id (an id, not a name) when the parent is another custom
|
||||
-- folder — immune to that parent later being renamed, unlike a name-based reference —
|
||||
-- parent_root (one of the 5 fixed, never-renamed standard names) when the parent is a
|
||||
-- standard folder that may not have its own row. A row with BOTH unset (parent_id
|
||||
-- NULL, parent_root '') is legacy data predating this column and is treated as
|
||||
-- "under INBOX" (see db.FolderParentMap) — every custom folder created before this
|
||||
-- feature was flat/top-level anyway, so that default is exactly correct, no backfill
|
||||
-- migration needed.
|
||||
--
|
||||
-- esrv_mailbox_messages.folder itself is untouched by any of this — messages are
|
||||
-- still tagged with a flat folder name exactly as before; the parent/child
|
||||
-- relationship here is purely an organizational layer for the sidebar.
|
||||
CREATE TABLE IF NOT EXISTS esrv_mailbox_folders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
mailbox_id INTEGER NOT NULL REFERENCES esrv_mailboxes(id),
|
||||
name TEXT NOT NULL,
|
||||
parent_id INTEGER REFERENCES esrv_mailbox_folders(id),
|
||||
parent_root TEXT NOT NULL DEFAULT '',
|
||||
-- Sidebar sort position among this folder's siblings (lower first), set only once
|
||||
-- the user drags to reorder. The app always writes an explicit value: -1
|
||||
-- (db.unpositionedFolder) for a row that exists for some other reason (rename, a
|
||||
-- folder just created, becoming a parent) but was never dragged, so it doesn't
|
||||
-- look like a real "dragged to the very top" (0) — FolderPositions filters on
|
||||
-- position >= 0 for exactly this reason. Column default of 0 is inert (every write
|
||||
-- path is explicit); kept only as a harmless fallback.
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
-- Snapshot of parent_id/parent_root taken the moment a folder is deleted (moved
|
||||
-- under Trash — see db.MoveFolderToTrash), so "Restore" (db.RestoreFolder) can put
|
||||
-- it back where it came from instead of just dumping it at INBOX's top level.
|
||||
-- Empty/NULL outside of that window (cleared again once restored).
|
||||
restore_parent_id INTEGER REFERENCES esrv_mailbox_folders(id),
|
||||
restore_parent_root TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(mailbox_id, name)
|
||||
);
|
||||
@@ -391,6 +430,25 @@ CREATE TABLE IF NOT EXISTS esrv_mailbox_pgp_identities (
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- A mailbox's saved email signatures — HTML content (compose is HTML/Quill-based
|
||||
-- already; a plain-text signature is just HTML with no formatting, so one content
|
||||
-- column covers both instead of storing two parallel copies). A mailbox may hold
|
||||
-- several, e.g. one formal and one casual; is_default_new/is_default_reply pick which
|
||||
-- one (if any) compose pre-fills for a brand-new message vs a reply/forward — at most
|
||||
-- one row per mailbox should have each flag set, enforced in the db package (Go), not
|
||||
-- a SQL constraint, since "make this one the default" is naturally an
|
||||
-- update-this-then-clear-the-others operation either way.
|
||||
CREATE TABLE IF NOT EXISTS esrv_mailbox_signatures (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
mailbox_id INTEGER NOT NULL REFERENCES esrv_mailboxes(id),
|
||||
name TEXT NOT NULL,
|
||||
content_html TEXT NOT NULL DEFAULT '',
|
||||
is_default_new INTEGER NOT NULL DEFAULT 0,
|
||||
is_default_reply INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_mailbox_signatures_mailbox ON esrv_mailbox_signatures(mailbox_id);
|
||||
|
||||
-- Other people's PGP public keys a mailbox owner has collected, added by hand —
|
||||
-- mirrors esrv_mailbox_smime_contacts. Used to offer "Encrypt (PGP)" for a
|
||||
-- recipient in compose.
|
||||
@@ -441,6 +499,12 @@ func migrateAddedColumns(db *sql.DB) {
|
||||
`ALTER TABLE esrv_mailbox_smime_identities ADD COLUMN key_pem TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE esrv_mailboxes ADD COLUMN group_messages INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE esrv_mailbox_messages ADD COLUMN cached_preview TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE esrv_mailbox_folders ADD COLUMN position INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE esrv_mailbox_folders ADD COLUMN parent_id INTEGER REFERENCES esrv_mailbox_folders(id)`,
|
||||
`ALTER TABLE esrv_mailbox_folders ADD COLUMN parent_root TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE esrv_mailbox_folders ADD COLUMN restore_parent_id INTEGER REFERENCES esrv_mailbox_folders(id)`,
|
||||
`ALTER TABLE esrv_mailbox_folders ADD COLUMN restore_parent_root TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE esrv_mailbox_messages ADD COLUMN restore_folder TEXT NOT NULL DEFAULT ''`,
|
||||
}
|
||||
// The three old columns above were NOT NULL with no default, so simply adding
|
||||
// key_pem left them behind still blocking every new insert (which only ever sets
|
||||
@@ -458,6 +522,23 @@ func migrateAddedColumns(db *sql.DB) {
|
||||
// defaults to 0 for every pre-existing row above, which would otherwise let that
|
||||
// account skip its username change entirely once it re-hits /first-login next.
|
||||
db.Exec(`UPDATE esrv_admin_users SET must_change_username = 1 WHERE username = ? AND must_change_password = 1`, DefaultAdminUsername)
|
||||
migrateSpamRenamedToJunk(db)
|
||||
}
|
||||
|
||||
// migrateSpamRenamedToJunk renames the standard "Spam" folder to "Junk" for mailboxes
|
||||
// that already had messages/records under the old name — "Junk" is what most desktop
|
||||
// IMAP clients look for by name (see db.StandardMailboxFolders' doc comment). Always
|
||||
// safe to re-run: once nothing's left named "Spam" every UPDATE here matches zero
|
||||
// rows.
|
||||
// ponytail: doesn't handle the (very unlikely) case where a mailbox already has an
|
||||
// unrelated custom folder literally named "Junk" before this rename — that one row's
|
||||
// UPDATE would fail on the UNIQUE(mailbox_id, name) constraint and get silently
|
||||
// skipped, same as every other best-effort statement in this function. Rename that
|
||||
// mailbox's pre-existing "Junk" folder first if this ever comes up in practice.
|
||||
func migrateSpamRenamedToJunk(db *sql.DB) {
|
||||
db.Exec(`UPDATE esrv_mailbox_messages SET folder = 'Junk' WHERE folder = 'Spam'`)
|
||||
db.Exec(`UPDATE esrv_mailbox_folders SET name = 'Junk' WHERE name = 'Spam'`)
|
||||
db.Exec(`UPDATE esrv_mailbox_folders SET parent_root = 'Junk' WHERE parent_root = 'Spam'`)
|
||||
}
|
||||
|
||||
// DB wraps *sql.DB with the query helpers below.
|
||||
|
||||
Reference in New Issue
Block a user