@bobfrankston/mailx 1.0.69 → 1.0.73

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.69",
3
+ "version": "1.0.73",
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,9 +20,9 @@
20
20
  "postinstall": "node launcher/builder/postinstall.js"
21
21
  },
22
22
  "dependencies": {
23
- "@bobfrankston/iflow": "^1.0.32",
23
+ "@bobfrankston/iflow": "^1.0.33",
24
24
  "@bobfrankston/miscinfo": "^1.0.6",
25
- "@bobfrankston/oauthsupport": "^1.0.17",
25
+ "@bobfrankston/oauthsupport": "^1.0.18",
26
26
  "@bobfrankston/rust-builder": "^0.1.2",
27
27
  "@capacitor/android": "^8.3.0",
28
28
  "@capacitor/cli": "^8.3.0",
@@ -112,7 +112,8 @@ export declare class ImapManager extends EventEmitter {
112
112
  /** Stop Outbox worker */
113
113
  stopOutboxWorker(): void;
114
114
  private contactsSyncToken;
115
- /** Get an OAuth token for Google Contacts API */
115
+ /** Get an OAuth token for Google APIs (contacts, calendar, etc.)
116
+ * Uses the SAME token as IMAP — scopes are combined in one grant */
116
117
  private getContactsToken;
117
118
  /** Sync contacts from Google People API */
118
119
  syncGoogleContacts(accountId: string): Promise<number>;
@@ -4,7 +4,6 @@
4
4
  * Syncs messages to local store, emits events for new mail.
5
5
  */
6
6
  import { ImapClient, createAutoImapConfig } from "@bobfrankston/iflow";
7
- import { authenticateOAuth } from "@bobfrankston/oauthsupport";
8
7
  import { FileMessageStore } from "@bobfrankston/mailx-store";
9
8
  import { loadSettings, getStorePath, getConfigDir } from "@bobfrankston/mailx-settings";
10
9
  import { EventEmitter } from "node:events";
@@ -13,6 +12,15 @@ import * as path from "node:path";
13
12
  import { simpleParser } from "mailparser";
14
13
  import { createTransport } from "nodemailer";
15
14
  import * as os from "node:os";
15
+ /** Extract full error detail from imapflow errors */
16
+ function imapError(err) {
17
+ const parts = [err.message || "Unknown error"];
18
+ if (err.responseText)
19
+ parts.push(err.responseText);
20
+ if (err.responseStatus)
21
+ parts.push(`[${err.responseStatus}]`);
22
+ return parts.join(" — ");
23
+ }
16
24
  /** Convert iflow address objects to our EmailAddress */
17
25
  function toEmailAddress(addr) {
18
26
  return {
@@ -183,8 +191,8 @@ export class ImapManager extends EventEmitter {
183
191
  console.log(` [auth] ${account.id}: token valid`);
184
192
  }
185
193
  catch (e) {
186
- console.error(` [auth] ${account.id}: ${e.message}`);
187
- this.emit("accountError", account.id, e.message, "Re-authenticate: click the button below or run mailx -setup");
194
+ console.error(` [auth] ${account.id}: ${imapError(e)}`);
195
+ this.emit("accountError", account.id, imapError(e), "Re-authenticate: click the button below or run mailx -setup");
188
196
  }
189
197
  }
190
198
  }
@@ -437,13 +445,13 @@ export class ImapManager extends EventEmitter {
437
445
  }
438
446
  }
439
447
  catch (e) {
440
- this.emit("syncError", accountId, e.message);
441
- console.error(`Sync error for ${accountId}: ${e.message}`);
448
+ this.emit("syncError", accountId, imapError(e));
449
+ console.error(`Sync error for ${accountId}: ${imapError(e)}`);
442
450
  // Emit user-facing error — always offer re-auth for OAuth accounts
443
451
  const config = this.configs.get(accountId);
444
452
  const isOAuth = !!config?.tokenProvider;
445
453
  const hint = isOAuth ? "Authentication may have expired" : "Check server connectivity";
446
- this.emit("accountError", accountId, e.message || "unknown", hint);
454
+ this.emit("accountError", accountId, imapError(e), hint);
447
455
  }
448
456
  finally {
449
457
  if (client)
@@ -1169,7 +1177,7 @@ export class ImapManager extends EventEmitter {
1169
1177
  const prev = this.outboxBackoff.get(accountId);
1170
1178
  const delay = prev ? Math.min((now - prev + 30000) * 2, 300000) : 30000;
1171
1179
  this.outboxBackoff.set(accountId, now + delay);
1172
- console.error(` [outbox] Error for ${accountId}: ${e.message} (retry in ${Math.round(delay / 1000)}s)`);
1180
+ console.error(` [outbox] Error for ${accountId}: ${imapError(e)} (retry in ${Math.round(delay / 1000)}s)`);
1173
1181
  }
1174
1182
  }
1175
1183
  };
@@ -1185,40 +1193,11 @@ export class ImapManager extends EventEmitter {
1185
1193
  }
1186
1194
  // ── Google Contacts Sync ──
1187
1195
  contactsSyncToken = null;
1188
- /** Get an OAuth token for Google Contacts API */
1196
+ /** Get an OAuth token for Google APIs (contacts, calendar, etc.)
1197
+ * Uses the SAME token as IMAP — scopes are combined in one grant */
1189
1198
  async getContactsToken(accountId) {
1190
- const settings = loadSettings();
1191
- const account = settings.accounts.find(a => a.id === accountId);
1192
- if (!account || account.imap.auth !== "oauth2")
1193
- return null;
1194
- // Find iflow-credentials.json from the iflow package
1195
- const credentialsCandidates = [
1196
- path.resolve(import.meta.dirname, "..", "..", "node_modules", "@bobfrankston", "iflow", "iflow-credentials.json"),
1197
- path.resolve(import.meta.dirname, "..", "..", "..", "node_modules", "@bobfrankston", "iflow", "iflow-credentials.json"),
1198
- ];
1199
- const credentialsPath = credentialsCandidates.find(p => fs.existsSync(p));
1200
- if (!credentialsPath) {
1201
- console.error(" [contacts] iflow-credentials.json not found");
1202
- return null;
1203
- }
1204
- const accountDir = account.imap.user.replace(/[@.]/g, "_");
1205
- const tokenDir = path.join(getConfigDir(), "tokens", accountDir);
1206
- const tokenPath = path.join(tokenDir, "contacts-token.json");
1207
- // If no cached token exists, skip — don't open interactive browser consent for contacts
1208
- // The user can trigger contacts sync manually or it will be set up during mailx -setup
1209
- if (!fs.existsSync(tokenPath)) {
1210
- console.log(` [contacts] No token for ${accountId} — skipping (run mailx -setup to authorize)`);
1211
- return null;
1212
- }
1213
- const token = await authenticateOAuth(credentialsPath, {
1214
- scope: "https://www.googleapis.com/auth/contacts.readonly",
1215
- tokenDirectory: tokenDir,
1216
- tokenFileName: "contacts-token.json",
1217
- credentialsKey: "installed",
1218
- includeOfflineAccess: true,
1219
- loginHint: account.imap.user,
1220
- });
1221
- return token?.access_token || null;
1199
+ // Reuse the IMAP token — it now includes contacts.readonly scope
1200
+ return this.getOAuthToken(accountId);
1222
1201
  }
1223
1202
  /** Sync contacts from Google People API */
1224
1203
  async syncGoogleContacts(accountId) {