Files
LibreMail/app/schemas/org.libremail.data.local.AccountDatabase/3.json
T
JMR-dev 4d1f2ae9ca fix(yahoo): fail loud and error the account after 4 auth failures (#362)
Change the Yahoo/AOL proactive auth circuit-breaker's after-threshold
behaviour from a silent, self-clearing 30-min open-circuit window to a
fail-loud, permanent stop that surfaces to the user.

- AuthThrottleGate: past circuitOpenThreshold the circuit now LATCHES
  (blockedUntilMillis = Long.MAX_VALUE) instead of opening a self-clearing
  window. onAuthSuccess never clears a latch; only onAccountReadded does.
  The 1-4 spaced-retry ramp and all thresholds/timings are unchanged, and
  only genuine AuthenticationFailedExceptions still count.
- Persistent, user-visible error state: new nullable AccountEntity.authError
  (AccountDatabase migration v2 -> v3 + exported schema) + Account domain
  field + mappers + AccountDao.setAuthError (idempotent conditional write).
  markAccountErroredIfLatched bridges the in-memory latch to the row from
  MailSyncer/MailBackfiller (which hold the Account), keeping the DAO out of
  the mail layer. Both loops also skip a latched/errored account entirely
  (durable across restarts), and a re-add clears the latch + the row error.
- UI: a persistent red indicator + the message on the Settings account row
  (AccountReorderList) and the drawer switcher (FolderDrawer), plus a
  persistent banner atop the mailbox for the shown account (MailboxScreen +
  MailboxViewModel.accountAuthError). String: "Please remove and re-add this
  account with valid credentials".
- All AppLog breadcrumbs stay PII-free (hashed accountLogRef only).

Also merges origin/main, composing authGate with #469's GmailBandwidthTracker
in MailBackfiller/MailSyncer constructors and every test construction site.

Tests: gate latch (no self-clear / success can't clear / re-add clears),
markAccountErroredIfLatched, MailSyncer/MailBackfiller errored+latched skips,
repository reset-on-re-add, mapper round-trip, v2->v3 migration, DAO
set/clear, the mailbox banner + Settings-row Compose renders, and the
on-device gate latch. Full fast gate green (JDK 21).
2026-07-08 23:32:56 -05:00

246 lines
7.3 KiB
JSON

{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "553c7a19d228b3ba8f8a750232b4c7d9",
"entities": [
{
"tableName": "accounts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `email` TEXT NOT NULL, `displayName` TEXT NOT NULL, `authType` TEXT NOT NULL, `sortOrder` INTEGER NOT NULL DEFAULT 0, `authError` TEXT, `imap_host` TEXT NOT NULL, `imap_port` INTEGER NOT NULL, `imap_security` TEXT NOT NULL, `smtp_host` TEXT NOT NULL, `smtp_port` INTEGER NOT NULL, `smtp_security` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "email",
"columnName": "email",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "authType",
"columnName": "authType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sortOrder",
"columnName": "sortOrder",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "authError",
"columnName": "authError",
"affinity": "TEXT"
},
{
"fieldPath": "imap.host",
"columnName": "imap_host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "imap.port",
"columnName": "imap_port",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "imap.security",
"columnName": "imap_security",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "smtp.host",
"columnName": "smtp_host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "smtp.port",
"columnName": "smtp_port",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "smtp.security",
"columnName": "smtp_security",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "credentials",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT NOT NULL, `encryptedSecret` TEXT NOT NULL, PRIMARY KEY(`accountId`))",
"fields": [
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "encryptedSecret",
"columnName": "encryptedSecret",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"accountId"
]
}
},
{
"tableName": "account_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT NOT NULL, `signature` TEXT NOT NULL, `signatureEnabled` INTEGER NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `retentionCount` INTEGER, `retentionMonths` INTEGER, PRIMARY KEY(`accountId`), FOREIGN KEY(`accountId`) REFERENCES `accounts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "signature",
"columnName": "signature",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "signatureEnabled",
"columnName": "signatureEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsEnabled",
"columnName": "notificationsEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "retentionCount",
"columnName": "retentionCount",
"affinity": "INTEGER"
},
{
"fieldPath": "retentionMonths",
"columnName": "retentionMonths",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"accountId"
]
},
"foreignKeys": [
{
"table": "accounts",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"accountId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "signatures",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `accountId` TEXT NOT NULL, `name` TEXT NOT NULL, `contentHtml` TEXT NOT NULL, `isDefault` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`accountId`) REFERENCES `accounts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "contentHtml",
"columnName": "contentHtml",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isDefault",
"columnName": "isDefault",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_signatures_accountId",
"unique": false,
"columnNames": [
"accountId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_signatures_accountId` ON `${TABLE_NAME}` (`accountId`)"
}
],
"foreignKeys": [
{
"table": "accounts",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"accountId"
],
"referencedColumns": [
"id"
]
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '553c7a19d228b3ba8f8a750232b4c7d9')"
]
}
}