updated webclient setttings

This commit is contained in:
2026-08-15 21:49:25 +01:00
parent 15678c1b6e
commit f60dbcc6b0
41 changed files with 3147 additions and 770 deletions
+123 -11
View File
@@ -270,36 +270,71 @@ CREATE TABLE IF NOT EXISTS esrv_mailbox_aliases (
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Per-mailbox sender allow/block list. pattern is either an exact address
-- Per-mailbox sender allow/block/junk list. pattern is either an exact address
-- ("spam@evil.com") or a whole-domain wildcard ("@evil.com"). A single table with a
-- list_type column, not two near-identical tables.
-- list_type column, not three near-identical tables.
-- 'block' is admin-only (Prefix+"/mailboxes/{id}/lists") and hard-rejects at RCPT
-- time (db.IsBlocked, checked in smtpserver's Rcpt()) — an anti-abuse tool, not
-- something an end user self-manages. 'junk' is the mailbox owner's own self-service
-- Blocklist (webmail Settings, or the message view's "Mark as Junk" action) and is a
-- *soft* block instead: mail is still accepted and delivered, just straight to Junk
-- (db.IsJunked, checked in smtpserver's deliverLocally, bypassing spam scoring
-- entirely the same way 'allow' does) rather than bounced. Different tools for
-- different jobs — collapsing them into one would either strip admins of a real hard
-- reject or hand end users the ability to bounce mail on another user's behalf.
CREATE TABLE IF NOT EXISTS esrv_mailbox_allowblock (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mailbox_id INTEGER NOT NULL REFERENCES esrv_mailboxes(id),
list_type TEXT NOT NULL CHECK(list_type IN ('allow','block')),
list_type TEXT NOT NULL CHECK(list_type IN ('allow','block','junk')),
pattern TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(mailbox_id, list_type, pattern)
);
-- A mailbox owner's own address book — deliberately separate from
-- esrv_mailbox_smime_contacts/esrv_mailbox_pgp_contacts (those hold a certificate/key
-- per address for signing/encryption, not a person's name/phone) and from
-- SuggestRecipients' history-based autocomplete (mailbox_messages.go — reuses
-- cached_to/cached_from rather than a dedicated table, so it has no name/phone either
-- and can't be edited). name is required; phone is optional free text (no format
-- enforced — international numbers, extensions, etc. all vary too much to validate
-- usefully here).
CREATE TABLE IF NOT EXISTS esrv_mailbox_contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mailbox_id INTEGER NOT NULL REFERENCES esrv_mailboxes(id),
email TEXT NOT NULL,
name TEXT NOT NULL,
phone TEXT NOT NULL DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(mailbox_id, email)
);
-- Simple first-match-wins filter rules, evaluated in priority order (lower first) at
-- delivery time, before a message is encrypted and stored — so from/to/subject
-- delivery time, before a message is encrypted and stored — so from/to/subject/body
-- matching works against the real message, not just the plaintext cache columns below.
-- condition_field/op/value are the legacy single-condition columns, kept for rows
-- created before multi-condition support existed. Every rule created since then
-- stores its full condition list in conditions_json (a JSON array of
-- {field,op,value}) instead, combined per match_type ("all"=AND, "any"=OR); a rule
-- with an empty conditions_json falls back to the legacy columns as a single
-- {field,op,value}, value optionally "\n"-joined to mean "any of these" — see
-- mailstore.matchCondition) instead, combined per match_type ("all"=AND, "any"=OR); a
-- rule with an empty conditions_json falls back to the legacy columns as a single
-- condition — see MailboxFilterRule.Conditions() in mailbox_models.go.
-- has_attachment/recipient_type conditions match against a synthetic "yes"/"no" or
-- "to"/"cc"/"bcc" header value computed at delivery time (see smtpserver's
-- deliverLocally), not a real message header.
-- action_options_json holds action-specific parameters that don't apply to every
-- action (currently just "forward"'s keep_copy) — see MailboxFilterRule.ActionOptions().
CREATE TABLE IF NOT EXISTS esrv_mailbox_filter_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mailbox_id INTEGER NOT NULL REFERENCES esrv_mailboxes(id),
name TEXT NOT NULL DEFAULT '',
priority INTEGER NOT NULL DEFAULT 0,
condition_field TEXT NOT NULL CHECK(condition_field IN ('from','to','subject')),
condition_field TEXT NOT NULL CHECK(condition_field IN ('from','to','subject','body','has_attachment','recipient_type')),
condition_op TEXT NOT NULL CHECK(condition_op IN ('contains','equals','starts_with')),
condition_value TEXT NOT NULL,
action TEXT NOT NULL CHECK(action IN ('move_to_folder','delete','mark_read','mark_as_spam')),
action TEXT NOT NULL CHECK(action IN ('move_to_folder','delete','mark_read','mark_as_spam','forward')),
action_value TEXT NOT NULL DEFAULT '',
action_options_json TEXT NOT NULL DEFAULT '',
is_active INTEGER NOT NULL DEFAULT 1,
conditions_json TEXT NOT NULL DEFAULT '',
match_type TEXT NOT NULL DEFAULT 'all',
@@ -470,6 +505,23 @@ CREATE TABLE IF NOT EXISTS esrv_mailbox_signatures (
);
CREATE INDEX IF NOT EXISTS idx_mailbox_signatures_mailbox ON esrv_mailbox_signatures(mailbox_id);
-- Per-alias override of which signature is the default-for-new/default-for-reply,
-- for a mailbox with one or more send-as aliases (esrv_mailbox_aliases) wanting a
-- different signature depending which address they're composing as — e.g. a
-- "Support" signature for support@ and a personal one for their own address.
-- Purely additive on top of esrv_mailbox_signatures' own is_default_new/is_default_reply
-- columns, which remain the fallback default (for_email = the mailbox's own primary
-- address, or an alias with no override row here) — see GetDefaultSignature.
CREATE TABLE IF NOT EXISTS esrv_mailbox_signature_alias_defaults (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mailbox_id INTEGER NOT NULL REFERENCES esrv_mailboxes(id),
for_email TEXT NOT NULL,
for_reply INTEGER NOT NULL DEFAULT 0,
signature_id INTEGER NOT NULL REFERENCES esrv_mailbox_signatures(id),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(mailbox_id, for_email, for_reply)
);
-- 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.
@@ -525,6 +577,12 @@ func migrateAddedColumns(db *sql.DB) {
`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 ''`,
`ALTER TABLE esrv_mailboxes ADD COLUMN remote_images_mode TEXT NOT NULL DEFAULT 'ask'`,
// The condition_field/action CHECK constraints (adding body/has_attachment/
// recipient_type and forward) aren't retrofittable via ALTER TABLE either — see
// migrateFilterRulesAdvancedCheck below. name/action_options_json themselves are
// plain columns and migrate fine here.
`ALTER TABLE esrv_mailbox_filter_rules ADD COLUMN name TEXT NOT NULL DEFAULT ''`,
`ALTER TABLE esrv_mailbox_filter_rules ADD COLUMN action_options_json 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
@@ -544,12 +602,37 @@ func migrateAddedColumns(db *sql.DB) {
db.Exec(`UPDATE esrv_admin_users SET must_change_username = 1 WHERE username = ? AND must_change_password = 1`, DefaultAdminUsername)
migrateSpamRenamedToJunk(db)
migrateFilterRulesMarkAsSpamCheck(db)
migrateFilterRulesAdvancedCheck(db)
migrateAllowBlockJunkCheck(db)
}
// migrateAllowBlockJunkCheck rebuilds esrv_mailbox_allowblock for any DB created
// before 'junk' was added to the list_type CHECK constraint (the self-service
// webmail Blocklist feature) — same rename/recreate/copy/drop approach as
// migrateFilterRulesMarkAsSpamCheck above.
func migrateAllowBlockJunkCheck(db *sql.DB) {
var tableSQL string
if err := db.QueryRow(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'esrv_mailbox_allowblock'`).Scan(&tableSQL); err != nil {
return
}
if strings.Contains(tableSQL, "'junk'") {
return
}
if _, err := db.Exec(`ALTER TABLE esrv_mailbox_allowblock RENAME TO esrv_mailbox_allowblock_old`); err != nil {
return
}
if _, err := db.Exec(schema); err != nil {
return
}
db.Exec(`INSERT INTO esrv_mailbox_allowblock (id, mailbox_id, list_type, pattern, created_at)
SELECT id, mailbox_id, list_type, pattern, created_at FROM esrv_mailbox_allowblock_old`)
db.Exec(`DROP TABLE esrv_mailbox_allowblock_old`)
}
// migrateFilterRulesMarkAsSpamCheck rebuilds esrv_mailbox_filter_rules for any DB
// created before 'mark_as_spam' was added to the action CHECK constraint (webmail's
// "Mark as Junk" auto-blacklist rule, see webmail_mail.go's ensureJunkRuleForSender) —
// SQLite can't ALTER a CHECK constraint on an existing table, so the only way to widen
// created before 'mark_as_spam' was added to the action CHECK constraint (still a
// valid rule-builder action today — see the Rules section of Settings) — SQLite
// can't ALTER a CHECK constraint on an existing table, so the only way to widen
// it is to recreate the table under the current schema and copy the rows across.
// Detects the stale constraint by inspecting sqlite_master rather than tracking a
// schema-version number, so it stays a no-op forever once a DB is caught up.
@@ -574,6 +657,35 @@ func migrateFilterRulesMarkAsSpamCheck(db *sql.DB) {
db.Exec(`DROP TABLE esrv_mailbox_filter_rules_old`)
}
// migrateFilterRulesAdvancedCheck rebuilds esrv_mailbox_filter_rules for any DB created
// before the advanced rule builder (body/has_attachment/recipient_type conditions,
// forward action) widened condition_field/action's CHECK constraints — same
// rename/recreate/copy/drop approach as migrateFilterRulesMarkAsSpamCheck above, and
// deliberately run after it so a DB that predates both migrations gets caught up by
// each in turn without either one needing to know about the other's column set.
// name/action_options_json are plain columns already added by migrateAddedColumns
// above by the time this runs, so the INSERT below can select them unconditionally.
func migrateFilterRulesAdvancedCheck(db *sql.DB) {
var tableSQL string
if err := db.QueryRow(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'esrv_mailbox_filter_rules'`).Scan(&tableSQL); err != nil {
return
}
if strings.Contains(tableSQL, "'forward'") {
return
}
if _, err := db.Exec(`ALTER TABLE esrv_mailbox_filter_rules RENAME TO esrv_mailbox_filter_rules_old`); err != nil {
return
}
if _, err := db.Exec(schema); err != nil {
return
}
db.Exec(`INSERT INTO esrv_mailbox_filter_rules
(id, mailbox_id, name, priority, condition_field, condition_op, condition_value, action, action_value, action_options_json, is_active, conditions_json, match_type, created_at)
SELECT id, mailbox_id, name, priority, condition_field, condition_op, condition_value, action, action_value, action_options_json, is_active, conditions_json, match_type, created_at
FROM esrv_mailbox_filter_rules_old`)
db.Exec(`DROP TABLE esrv_mailbox_filter_rules_old`)
}
// 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