updated layout for webmail

This commit is contained in:
2026-08-15 16:31:27 +01:00
parent f283c90f11
commit 6f0c305367
35 changed files with 2660 additions and 219 deletions
+84 -3
View File
@@ -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.