@bobfrankston/mailx 1.0.213 → 1.0.214
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.214",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@bobfrankston/iflow-node": "^0.1.2",
|
|
25
25
|
"@bobfrankston/miscinfo": "^1.0.8",
|
|
26
26
|
"@bobfrankston/oauthsupport": "^1.0.22",
|
|
27
|
-
"@bobfrankston/msger": "^0.1.
|
|
27
|
+
"@bobfrankston/msger": "^0.1.276",
|
|
28
28
|
"@capacitor/android": "^8.3.0",
|
|
29
29
|
"@capacitor/cli": "^8.3.0",
|
|
30
30
|
"@capacitor/core": "^8.3.0",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"@bobfrankston/iflow-node": "^0.1.2",
|
|
79
79
|
"@bobfrankston/miscinfo": "^1.0.8",
|
|
80
80
|
"@bobfrankston/oauthsupport": "^1.0.22",
|
|
81
|
-
"@bobfrankston/msger": "^0.1.
|
|
81
|
+
"@bobfrankston/msger": "^0.1.276",
|
|
82
82
|
"@capacitor/android": "^8.3.0",
|
|
83
83
|
"@capacitor/cli": "^8.3.0",
|
|
84
84
|
"@capacitor/core": "^8.3.0",
|
|
@@ -346,7 +346,7 @@ export class ImapManager extends EventEmitter {
|
|
|
346
346
|
const tokenDir = path.join(getConfigDir(), "tokens", account.imap.user.replace(/[@.]/g, "_"));
|
|
347
347
|
tokenProvider = async () => {
|
|
348
348
|
const result = await authenticateOAuth(credPath, {
|
|
349
|
-
scope: "https://mail.google.com/ https://www.googleapis.com/auth/contacts.readonly",
|
|
349
|
+
scope: "https://mail.google.com/ https://www.googleapis.com/auth/contacts.readonly https://www.googleapis.com/auth/calendar",
|
|
350
350
|
tokenDirectory: tokenDir,
|
|
351
351
|
credentialsKey: "installed",
|
|
352
352
|
loginHint: account.imap.user,
|
|
@@ -360,6 +360,9 @@ export class ImapManager extends EventEmitter {
|
|
|
360
360
|
username: account.imap.user,
|
|
361
361
|
password: account.imap.password,
|
|
362
362
|
tokenProvider,
|
|
363
|
+
// Slow Dovecot servers (e.g. iecc.com) can stall >60s during multi-body FETCH.
|
|
364
|
+
// Raise the inactivity timeout so the connection isn't dropped mid-stream.
|
|
365
|
+
inactivityTimeout: 180000,
|
|
363
366
|
});
|
|
364
367
|
this.configs.set(account.id, config);
|
|
365
368
|
// Register account in DB
|
|
@@ -1731,19 +1734,36 @@ export class ImapManager extends EventEmitter {
|
|
|
1731
1734
|
return;
|
|
1732
1735
|
// Gmail/API accounts: send directly via SMTP from local queue (no IMAP outbox)
|
|
1733
1736
|
const sentDir = path.join(getConfigDir(), "sending", accountId, "sent");
|
|
1737
|
+
const failedDir = path.join(getConfigDir(), "sending", accountId, "failed");
|
|
1734
1738
|
fs.mkdirSync(sentDir, { recursive: true });
|
|
1739
|
+
fs.mkdirSync(failedDir, { recursive: true });
|
|
1735
1740
|
if (this.isGmailAccount(accountId)) {
|
|
1736
1741
|
for (const { dir, file } of filesToSend) {
|
|
1737
1742
|
const filePath = path.join(dir, file);
|
|
1738
1743
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
1739
1744
|
try {
|
|
1740
1745
|
await this.sendRawViaSMTP(accountId, raw);
|
|
1741
|
-
// Move to sent/
|
|
1742
1746
|
fs.renameSync(filePath, path.join(sentDir, file));
|
|
1743
1747
|
console.log(` [outbox] Sent ${file} via SMTP → sent/`);
|
|
1744
1748
|
}
|
|
1745
1749
|
catch (e) {
|
|
1746
|
-
|
|
1750
|
+
// Critical: NEVER leave a .ltr in the queue after a send attempt unless the
|
|
1751
|
+
// failure was clearly pre-connect. An error after the SMTP session opened may
|
|
1752
|
+
// mean the server actually accepted DATA but we lost the ack — retrying would
|
|
1753
|
+
// double-send. Classify conservatively: only keep-in-queue for clearly
|
|
1754
|
+
// network-level / pre-auth errors.
|
|
1755
|
+
const code = String(e?.code || "");
|
|
1756
|
+
const cmd = String(e?.command || "").toUpperCase();
|
|
1757
|
+
const preConnect = /^(ECONNREFUSED|ENOTFOUND|EAI_AGAIN|ECONNECTION|ETIMEDOUT)$/.test(code)
|
|
1758
|
+
&& (!cmd || cmd === "CONN");
|
|
1759
|
+
if (preConnect) {
|
|
1760
|
+
console.error(` [outbox] Pre-connect failure for ${file} (${code}), will retry: ${e.message}`);
|
|
1761
|
+
}
|
|
1762
|
+
else {
|
|
1763
|
+
// Ambiguous or terminal — move to failed/ so we don't resend if SMTP actually delivered
|
|
1764
|
+
fs.renameSync(filePath, path.join(failedDir, file));
|
|
1765
|
+
console.error(` [outbox] Send failed for ${file} → failed/ (no auto-retry, code=${code || "?"}): ${e.message}`);
|
|
1766
|
+
}
|
|
1747
1767
|
}
|
|
1748
1768
|
}
|
|
1749
1769
|
return;
|
|
@@ -1939,12 +1959,21 @@ export class ImapManager extends EventEmitter {
|
|
|
1939
1959
|
this.outboxBackoffDelay.delete(accountId);
|
|
1940
1960
|
}
|
|
1941
1961
|
catch (e) {
|
|
1942
|
-
//
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1962
|
+
// Stale-socket errors (Dovecot silently drops idle connections):
|
|
1963
|
+
// don't back off — just reconnect on the next tick. The 300s
|
|
1964
|
+
// backoff is meant for real auth/network failures, not dead sockets.
|
|
1965
|
+
const msg = String(e?.message || e);
|
|
1966
|
+
if (/Not connected|ECONNRESET|socket hang up|EPIPE|write after end/i.test(msg)) {
|
|
1967
|
+
console.error(` [outbox] Stale connection for ${accountId}: ${msg} — will retry next tick`);
|
|
1968
|
+
}
|
|
1969
|
+
else {
|
|
1970
|
+
// Exponential backoff: 60s → 120s → 300s (max 5min)
|
|
1971
|
+
const prevDelay = this.outboxBackoffDelay.get(accountId) || 0;
|
|
1972
|
+
const delay = prevDelay ? Math.min(prevDelay * 2, 300000) : 60000;
|
|
1973
|
+
this.outboxBackoffDelay.set(accountId, delay);
|
|
1974
|
+
this.outboxBackoff.set(accountId, now + delay);
|
|
1975
|
+
console.error(` [outbox] Error for ${accountId}: ${imapError(e)} (retry in ${Math.round(delay / 1000)}s)`);
|
|
1976
|
+
}
|
|
1948
1977
|
}
|
|
1949
1978
|
}
|
|
1950
1979
|
};
|
|
@@ -288,7 +288,7 @@ const OAUTH_CLIENT = {
|
|
|
288
288
|
// Use full drive scope so we can read/write the desktop's accounts.jsonc + clients.jsonc.
|
|
289
289
|
// drive.file is per-consent-grant: files created by desktop's grant aren't visible to Android's grant
|
|
290
290
|
// even with the same client_id. drive (full) lets us see all files the user has access to.
|
|
291
|
-
const OAUTH_SCOPES = "https://mail.google.com/ https://www.googleapis.com/auth/contacts.readonly https://www.googleapis.com/auth/drive";
|
|
291
|
+
const OAUTH_SCOPES = "https://mail.google.com/ https://www.googleapis.com/auth/contacts.readonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/calendar";
|
|
292
292
|
// ── Token cache (IndexedDB) ──
|
|
293
293
|
async function getCachedToken(email) {
|
|
294
294
|
const key = `oauth-token-${email.replace(/[@.]/g, "_")}`;
|