@bobfrankston/mailx 1.0.164 → 1.0.166

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.
package/bin/mailx.js CHANGED
@@ -645,7 +645,8 @@ async function main() {
645
645
  }
646
646
  }
647
647
  const db = new MailxDB(getConfigDir());
648
- const imapManager = new ImapManager(db);
648
+ const { NodeTransport } = await import("@bobfrankston/iflow-node");
649
+ const imapManager = new ImapManager(db, () => new NodeTransport());
649
650
  // Native client is the only option (iflow-direct)
650
651
  const svc = new MailxService(db, imapManager);
651
652
  // Open msger in service mode — custom protocol serves files from client dir
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.164",
3
+ "version": "1.0.166",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -20,11 +20,11 @@
20
20
  "postinstall": "node bin/postinstall.js"
21
21
  },
22
22
  "dependencies": {
23
- "@bobfrankston/iflow-direct": "^0.1.0",
24
- "@bobfrankston/iflow-node": "^0.1.0",
23
+ "@bobfrankston/iflow-direct": "^0.1.2",
24
+ "@bobfrankston/iflow-node": "^0.1.1",
25
25
  "@bobfrankston/miscinfo": "^1.0.7",
26
26
  "@bobfrankston/oauthsupport": "^1.0.20",
27
- "@bobfrankston/msger": "^0.1.214",
27
+ "@bobfrankston/msger": "^0.1.216",
28
28
  "@capacitor/android": "^8.3.0",
29
29
  "@capacitor/cli": "^8.3.0",
30
30
  "@capacitor/core": "^8.3.0",
@@ -9,7 +9,7 @@ import type { ComposeMessage } from "@bobfrankston/mailx-types";
9
9
  declare let db: MailxDB;
10
10
  declare let imapManager: ImapManager;
11
11
  export declare function onEvent(handler: (event: any) => void): void;
12
- export declare function initialize(): Promise<void>;
12
+ export declare function initialize(transportFactory?: any): Promise<void>;
13
13
  export declare function shutdown(): Promise<void>;
14
14
  export declare function getAccounts(): {
15
15
  id: string;
@@ -22,11 +22,11 @@ function emit(event) {
22
22
  catch { /* ignore handler errors */ }
23
23
  }
24
24
  }
25
- export async function initialize() {
25
+ export async function initialize(transportFactory) {
26
26
  initLocalConfig();
27
27
  const dbDir = getConfigDir();
28
28
  db = new MailxDB(dbDir);
29
- imapManager = new ImapManager(db);
29
+ imapManager = new ImapManager(db, transportFactory || (() => { throw new Error("No transport factory provided"); }));
30
30
  // Seed contacts
31
31
  const seeded = db.seedContactsFromMessages();
32
32
  if (seeded > 0)
@@ -3,6 +3,7 @@
3
3
  * Multi-account IMAP management wrapping iflow.
4
4
  * Syncs messages to local store, emits events for new mail.
5
5
  */
6
+ import { type TransportFactory } from "@bobfrankston/iflow-direct";
6
7
  import { MailxDB, FileMessageStore } from "@bobfrankston/mailx-store";
7
8
  import type { AccountConfig, MessageEnvelope, Folder } from "@bobfrankston/mailx-types";
8
9
  import { EventEmitter } from "node:events";
@@ -33,7 +34,8 @@ export declare class ImapManager extends EventEmitter {
33
34
  useNativeClient: boolean;
34
35
  /** Accounts hitting connection limits — back off until this time */
35
36
  private connectionBackoff;
36
- constructor(db: MailxDB);
37
+ private transportFactory;
38
+ constructor(db: MailxDB, transportFactory: TransportFactory);
37
39
  /** Get OAuth access token for an account (for SMTP auth) */
38
40
  getOAuthToken(accountId: string): Promise<string | null>;
39
41
  /** Accounts currently re-authenticating — all operations skip these */
@@ -4,7 +4,7 @@
4
4
  * Syncs messages to local store, emits events for new mail.
5
5
  */
6
6
  import { createAutoImapConfig, CompatImapClient } from "@bobfrankston/iflow-direct";
7
- import { NodeTransport } from "@bobfrankston/iflow-node";
7
+ import { authenticateOAuth } from "@bobfrankston/oauthsupport";
8
8
  import { FileMessageStore } from "@bobfrankston/mailx-store";
9
9
  import { loadSettings, getStorePath, getConfigDir, getHistoryDays } from "@bobfrankston/mailx-settings";
10
10
  import { EventEmitter } from "node:events";
@@ -108,9 +108,11 @@ export class ImapManager extends EventEmitter {
108
108
  /** Accounts hitting connection limits — back off until this time */
109
109
  connectionBackoff = new Map();
110
110
  // Connection management: see withConnection() below — no semaphore needed
111
- constructor(db) {
111
+ transportFactory;
112
+ constructor(db, transportFactory) {
112
113
  super();
113
114
  this.db = db;
115
+ this.transportFactory = transportFactory;
114
116
  const storePath = getStorePath();
115
117
  this.bodyStore = new FileMessageStore(storePath);
116
118
  }
@@ -281,7 +283,7 @@ export class ImapManager extends EventEmitter {
281
283
  const config = this.configs.get(accountId);
282
284
  if (!config)
283
285
  throw new Error(`No config for account ${accountId}`);
284
- return new CompatImapClient(config, () => new NodeTransport({ rejectUnauthorized: config.rejectUnauthorized !== false }));
286
+ return new CompatImapClient(config, this.transportFactory);
285
287
  }
286
288
  /** Disconnect the persistent operational connection for an account */
287
289
  async disconnectOps(accountId) {
@@ -313,13 +315,26 @@ export class ImapManager extends EventEmitter {
313
315
  if (this.configs.has(account.id))
314
316
  return;
315
317
  // createAutoImapConfig auto-detects Gmail from server/username and sets up OAuth
316
- // Token directory in ~/.mailx/ so tokens persist across npm reinstalls
318
+ // For OAuth accounts, provide a tokenProvider using oauthsupport
319
+ let tokenProvider;
320
+ if (account.imap.auth === "oauth2" || (!account.imap.password && account.imap.host?.includes("gmail"))) {
321
+ const credPath = path.join(getConfigDir(), "google-credentials.json");
322
+ const tokenDir = path.join(getConfigDir(), "tokens", account.imap.user.replace(/[@.]/g, "_"));
323
+ tokenProvider = async () => {
324
+ const result = await authenticateOAuth(credPath, {
325
+ scope: "https://mail.google.com/ https://www.googleapis.com/auth/contacts.readonly",
326
+ tokenDirectory: tokenDir,
327
+ loginHint: account.imap.user,
328
+ });
329
+ return result?.access_token || "";
330
+ };
331
+ }
317
332
  const config = createAutoImapConfig({
318
333
  server: account.imap.host,
319
334
  port: account.imap.port,
320
335
  username: account.imap.user,
321
336
  password: account.imap.password,
322
- // tokenDirectory is handled by oauthsupport, not iflow-direct
337
+ tokenProvider,
323
338
  });
324
339
  this.configs.set(account.id, config);
325
340
  // Register account in DB
@@ -387,6 +402,11 @@ export class ImapManager extends EventEmitter {
387
402
  this.db.beginTransaction();
388
403
  try {
389
404
  for (const msg of msgs) {
405
+ // Debug: log subjects with non-ASCII to trace encoding issues
406
+ if (msg.subject && /[^\x00-\x7F]/.test(msg.subject)) {
407
+ const hex = Buffer.from(msg.subject, "utf-8").subarray(0, 40).toString("hex");
408
+ console.log(` [encoding] subject: "${msg.subject.substring(0, 60)}" hex: ${hex}`);
409
+ }
390
410
  if (msg.uid <= highestUid)
391
411
  continue; // already have it
392
412
  const source = msg.source || "";
@@ -499,7 +519,8 @@ export class ImapManager extends EventEmitter {
499
519
  this.emit("folderCountsChanged", accountId, {});
500
520
  }
501
521
  };
502
- messages = await client.fetchMessageByDate(folder.path, startDate, new Date(), { source: false }, onChunk);
522
+ const tomorrow = new Date(Date.now() + 86400000); // IMAP BEFORE is exclusive
523
+ messages = await client.fetchMessageByDate(folder.path, startDate, tomorrow, { source: false }, onChunk);
503
524
  if (totalStored > 0) {
504
525
  console.log(` ${folder.path}: ${totalStored} messages (streamed)`);
505
526
  this.db.recalcFolderCounts(folderId);
@@ -61,15 +61,8 @@ if (settings.accounts.length === 0) {
61
61
  }
62
62
  const dbDir = getConfigDir();
63
63
  const db = new MailxDB(dbDir);
64
- const imapManager = new ImapManager(db);
65
- // Native IMAP client is default. Use --legacy-imap to fall back to imapflow.
66
- if (process.argv.includes("--legacy-imap") || process.argv.includes("-legacy-imap") || process.env.MAILX_LEGACY_IMAP) {
67
- console.log(" Using legacy IMAP client (imapflow)");
68
- }
69
- else {
70
- imapManager.useNativeClient = true;
71
- console.log(" Using native IMAP client (transport-agnostic)");
72
- }
64
+ import { NodeTransport } from "@bobfrankston/iflow-node";
65
+ const imapManager = new ImapManager(db, () => new NodeTransport());
73
66
  // ── Express App ──
74
67
  const app = express();
75
68
  app.use(express.json({ limit: "Infinity" }));