@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.
|
|
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.
|
|
23
|
+
"@bobfrankston/iflow": "^1.0.33",
|
|
24
24
|
"@bobfrankston/miscinfo": "^1.0.6",
|
|
25
|
-
"@bobfrankston/oauthsupport": "^1.0.
|
|
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
|
|
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
|
|
187
|
-
this.emit("accountError", account.id, e
|
|
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
|
|
441
|
-
console.error(`Sync error for ${accountId}: ${e
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1191
|
-
|
|
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) {
|