@bobfrankston/rmfmail 1.1.115 → 1.1.117

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,6 @@ import { MailxDB, parseSerial } from "@bobfrankston/mailx-store";
12
12
  const __dirname = import.meta.dirname;
13
13
  import { ImapManager } from "@bobfrankston/mailx-imap";
14
14
  import * as gsync from "./google-sync.js";
15
- import { sniffAndFixCharset } from "./charset.js";
16
15
  import { Store } from "@bobfrankston/mailx-store";
17
16
  import { SyncQueue } from "./sync-queue.js";
18
17
  import { Reconciler } from "./reconciler.js";
@@ -20,18 +19,10 @@ import { loadSettings, saveSettings, loadAccounts, loadAccountsAsync, saveAccoun
20
19
  import type { AccountConfig, Folder, AutocompleteRequest, AutocompleteResponse, AutocompleteSettings, AiTransformRequest, AiTransformResponse, ExtractedEvent, MailxApi } from "@bobfrankston/mailx-types";
21
20
  import { sanitizeHtml, encodeQuotedPrintable, htmlToPlainText } from "@bobfrankston/mailx-types";
22
21
 
23
- /** Detect mis-labeled charset and rewrite the part header to `utf-8` when
24
- * the body bytes are actually valid UTF-8. PHPMailer-driven senders are
25
- * the chronic offender they declare `charset=iso-8859-1` (the PHP
26
- * default) and then encode "", "'", "…" etc. as UTF-8, which simpleParser
27
- * faithfully decodes as Latin-1 and produces "â??" garbage. We only
28
- * override the obviously-wrong declarations (`iso-8859-1`, `us-ascii`,
29
- * `windows-1252`, `latin1`); anything else passes through unchanged.
30
- * Operates byte-wise so MIME boundaries / base64 / etc. are preserved.
31
- * The `utf-8` validity test rejects 0xC0–0xC1 / 0xF5–0xFF and continuation
32
- * bytes out of place, which would be common in genuine Latin-1 text. */
33
- // sniffAndFixCharset moved to ./charset.ts so LocalStore can use it
34
- // without creating a circular dep with index.ts.
22
+ // Charset normalization (sniffAndFixCharset) is owned solely by
23
+ // mailx-store/charset.ts — the single live parse path (LocalStore.getMessage)
24
+ // calls it there. mailx-service used to keep a duplicate copy that nothing
25
+ // invoked; removed 2026-05-21 (economy of mechanism one charset fixer).
35
26
 
36
27
  // parseListUnsubscribe moved to ./local-store.ts (only consumer is the
37
28
  // body-read path, which is now part of LocalStore).
@@ -131,6 +122,10 @@ export class MailxService implements MailxApi {
131
122
  // comment in loadContactsConfig() for why we don't re-walk on
132
123
  // every fs.watch firing of contacts.jsonc.
133
124
  private _contactsCorpusSeeded = false;
125
+ // Retry bookkeeping for loadContactsConfig when the cloud isn't ready
126
+ // yet at startup. Without retries, a failed first read left the denylist
127
+ // / preferred list unloaded for the whole session.
128
+ private _contactsRetries = 0;
134
129
  // Settings cache. `loadSettings()` does a synchronous read of
135
130
  // accounts.jsonc (via loadAccounts) AND preferences.jsonc — both of
136
131
  // which may live on a GDrive-mounted shared dir where readFileSync
@@ -258,9 +253,33 @@ export class MailxService implements MailxApi {
258
253
  cloudAvailable = true;
259
254
  } catch { /* cloud unavailable */ }
260
255
 
256
+ // Cloud not ready — the read THREW (GDrive auth/init still in
257
+ // progress; loadContactsConfig() is fired from the constructor). Do
258
+ // NOT fall through to applyContactsConfig({denylist:[]}): that wipes
259
+ // the in-memory denylist for the whole session, so every "never
260
+ // suggest this address" choice silently comes back next run (Bob
261
+ // 2026-05-21: "it's on drive but not read for the new session").
262
+ // Retry with backoff until the cloud is actually up.
263
+ if (!cloudAvailable) {
264
+ const RETRY_DELAYS = [3_000, 8_000, 20_000, 45_000, 90_000, 90_000];
265
+ if (this._contactsRetries < RETRY_DELAYS.length) {
266
+ const delay = RETRY_DELAYS[this._contactsRetries];
267
+ this._contactsRetries++;
268
+ console.log(` [contacts] cloud not ready — retry ${this._contactsRetries} in ${delay / 1000}s`);
269
+ setTimeout(() => { this.loadContactsConfig().catch(() => { /* */ }); }, delay);
270
+ } else {
271
+ console.error(` [contacts] cloud unreachable after ${RETRY_DELAYS.length} retries — denylist/preferred NOT loaded this session`);
272
+ }
273
+ return null;
274
+ }
275
+ // Cloud reachable — clear the retry counter so a later fs.watch
276
+ // reload that briefly fails can retry afresh.
277
+ this._contactsRetries = 0;
278
+
261
279
  if (!raw) {
262
- // No file (yet). Reset in-memory denylist and seed discovered
263
- // from the local message corpus so autocomplete works immediately.
280
+ // No file (yet) genuinely missing, cloud confirmed reachable.
281
+ // Reset in-memory denylist and seed discovered from the local
282
+ // message corpus so autocomplete works immediately.
264
283
  await this.db.applyContactsConfig({ preferred: [], denylist: [], discovered: [] });
265
284
  try { await this.db.seedContactsFromMessages(); } catch { /* corpus may be empty */ }
266
285
  // Auto-bootstrap GDrive copy if cloud is reachable. The file gets