@bobfrankston/mailx 1.0.233 → 1.0.235
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"height":1344,"width":2151,"x":
|
|
1
|
+
{"height":1344,"width":2151,"x":656,"y":239}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.235",
|
|
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.297",
|
|
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.297",
|
|
82
82
|
"@capacitor/android": "^8.3.0",
|
|
83
83
|
"@capacitor/cli": "^8.3.0",
|
|
84
84
|
"@capacitor/core": "^8.3.0",
|
|
@@ -1009,9 +1009,15 @@ export class ImapManager extends EventEmitter {
|
|
|
1009
1009
|
// the previous high. upsertMessage's primary-key dedup handles it.
|
|
1010
1010
|
void highestUid;
|
|
1011
1011
|
let stored = 0;
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1012
|
+
let errors = 0;
|
|
1013
|
+
// Don't wrap the whole batch in one transaction: a single bad row
|
|
1014
|
+
// would roll back the entire batch. E.g. a message with a malformed
|
|
1015
|
+
// Date header gave `new Date(rawStr).getTime() === NaN`, SQLite
|
|
1016
|
+
// coerced that to NULL, the NOT NULL constraint failed, and the
|
|
1017
|
+
// whole Gmail sync lost 200 messages per tick. Now each row runs
|
|
1018
|
+
// standalone — bad rows are logged and skipped.
|
|
1019
|
+
for (const msg of msgs) {
|
|
1020
|
+
try {
|
|
1015
1021
|
const flags = [];
|
|
1016
1022
|
if (msg.seen)
|
|
1017
1023
|
flags.push("\\Seen");
|
|
@@ -1021,12 +1027,20 @@ export class ImapManager extends EventEmitter {
|
|
|
1021
1027
|
flags.push("\\Answered");
|
|
1022
1028
|
if (msg.draft)
|
|
1023
1029
|
flags.push("\\Draft");
|
|
1030
|
+
// Sanitize date: reject NaN (from malformed RFC 822 Date headers)
|
|
1031
|
+
// and fall back to "now" so the message still lands in the DB.
|
|
1032
|
+
let dateMs = Date.now();
|
|
1033
|
+
if (msg.date instanceof Date) {
|
|
1034
|
+
const t = msg.date.getTime();
|
|
1035
|
+
if (Number.isFinite(t))
|
|
1036
|
+
dateMs = t;
|
|
1037
|
+
}
|
|
1024
1038
|
this.db.upsertMessage({
|
|
1025
1039
|
accountId, folderId, uid: msg.uid,
|
|
1026
1040
|
messageId: msg.messageId || "",
|
|
1027
1041
|
inReplyTo: msg.inReplyTo || "",
|
|
1028
1042
|
references: msg.references || [],
|
|
1029
|
-
date:
|
|
1043
|
+
date: dateMs,
|
|
1030
1044
|
subject: msg.subject || "",
|
|
1031
1045
|
from: toEmailAddress(msg.from?.[0] || {}),
|
|
1032
1046
|
to: toEmailAddresses(msg.to || []),
|
|
@@ -1035,12 +1049,15 @@ export class ImapManager extends EventEmitter {
|
|
|
1035
1049
|
});
|
|
1036
1050
|
stored++;
|
|
1037
1051
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1052
|
+
catch (e) {
|
|
1053
|
+
errors++;
|
|
1054
|
+
if (errors <= 3) {
|
|
1055
|
+
console.error(` [api] upsert ${accountId}/${folderId}/${msg.uid} (${msg.messageId}): ${e.message}`);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1043
1058
|
}
|
|
1059
|
+
if (errors > 0)
|
|
1060
|
+
console.error(` [api] storeApiMessages: ${errors} of ${msgs.length} rows failed (${stored} stored)`);
|
|
1044
1061
|
return stored;
|
|
1045
1062
|
}
|
|
1046
1063
|
/** Kill and recreate the persistent ops connection */
|
|
@@ -1957,6 +1974,8 @@ export class ImapManager extends EventEmitter {
|
|
|
1957
1974
|
const ccMatch = raw.match(/^Cc:\s*(.+)$/mi);
|
|
1958
1975
|
const bccMatch = raw.match(/^Bcc:\s*(.+)$/mi);
|
|
1959
1976
|
const fromMatch = raw.match(/^From:\s*(.+)$/mi);
|
|
1977
|
+
const subjectMatch = raw.match(/^Subject:\s*(.+)$/mi);
|
|
1978
|
+
const messageIdMatch = raw.match(/^Message-ID:\s*(<[^>]+>)/mi);
|
|
1960
1979
|
const recipients = [
|
|
1961
1980
|
...(toMatch ? parseAddrs(toMatch[1]) : []),
|
|
1962
1981
|
...(ccMatch ? parseAddrs(ccMatch[1]) : []),
|
|
@@ -1965,9 +1984,22 @@ export class ImapManager extends EventEmitter {
|
|
|
1965
1984
|
const sender = fromMatch ? (parseAddrs(fromMatch[1])[0] || account.email) : account.email;
|
|
1966
1985
|
if (recipients.length === 0)
|
|
1967
1986
|
throw new Error("No recipients");
|
|
1987
|
+
// Dedup: skip if this Message-ID has already been sent. Prevents the
|
|
1988
|
+
// outbox from re-sending the same file across crash/restart cycles.
|
|
1989
|
+
// Without this, a queued .ltr that was mid-delivery when mailx crashed
|
|
1990
|
+
// would be re-sent on every startup until the rename loop completed.
|
|
1991
|
+
const messageId = messageIdMatch ? messageIdMatch[1] : "";
|
|
1992
|
+
if (messageId && this.db.hasSentMessage(messageId)) {
|
|
1993
|
+
console.log(` [smtp] ${accountId}: SKIP ${messageId} — already in sent_log`);
|
|
1994
|
+
return; // caller will move the file to sent/ without re-sending
|
|
1995
|
+
}
|
|
1968
1996
|
const rawToSend = raw.replace(/^Bcc:.*\r?\n/mi, "");
|
|
1969
1997
|
this.saveSendingCopy(accountId, rawToSend, "sent");
|
|
1970
1998
|
await transport.sendMail({ envelope: { from: sender, to: recipients }, raw: rawToSend });
|
|
1999
|
+
// Record the successful send so future attempts dedupe against it.
|
|
2000
|
+
if (messageId) {
|
|
2001
|
+
this.db.recordSent(messageId, accountId, subjectMatch?.[1]?.trim() || "", recipients);
|
|
2002
|
+
}
|
|
1971
2003
|
console.log(` [smtp] ${accountId}: sent to ${recipients.join(", ")}`);
|
|
1972
2004
|
}
|
|
1973
2005
|
/** Process Outbox — send pending messages with flag-based interlock */
|
|
@@ -7,6 +7,11 @@ import type { MessageEnvelope, Folder, EmailAddress, PagedResult, MessageQuery }
|
|
|
7
7
|
export declare class MailxDB {
|
|
8
8
|
private db;
|
|
9
9
|
constructor(dbDir: string);
|
|
10
|
+
/** Has this Message-ID already been sent? Used to prevent the outbox from
|
|
11
|
+
* re-sending the same raw file across crash/restart cycles. */
|
|
12
|
+
hasSentMessage(messageId: string): boolean;
|
|
13
|
+
/** Record a successfully sent message so future attempts are skipped. */
|
|
14
|
+
recordSent(messageId: string, accountId: string, subject: string, recipients: string[]): void;
|
|
10
15
|
/** Idempotently add a column to a table if it's missing. */
|
|
11
16
|
private addColumnIfMissing;
|
|
12
17
|
/** Compute a thread id for an incoming message. Strategy:
|
|
@@ -62,6 +62,14 @@ const SCHEMA = `
|
|
|
62
62
|
CREATE INDEX IF NOT EXISTS idx_messages_thread_id
|
|
63
63
|
ON messages(account_id, thread_id);
|
|
64
64
|
|
|
65
|
+
CREATE TABLE IF NOT EXISTS sent_log (
|
|
66
|
+
message_id TEXT PRIMARY KEY,
|
|
67
|
+
account_id TEXT NOT NULL,
|
|
68
|
+
subject TEXT DEFAULT '',
|
|
69
|
+
recipients TEXT DEFAULT '',
|
|
70
|
+
sent_at INTEGER NOT NULL
|
|
71
|
+
);
|
|
72
|
+
|
|
65
73
|
CREATE TABLE IF NOT EXISTS queue (
|
|
66
74
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
67
75
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
@@ -137,6 +145,26 @@ export class MailxDB {
|
|
|
137
145
|
}
|
|
138
146
|
catch { /* already exists */ }
|
|
139
147
|
}
|
|
148
|
+
// ── Sent-log (dedup) ──
|
|
149
|
+
/** Has this Message-ID already been sent? Used to prevent the outbox from
|
|
150
|
+
* re-sending the same raw file across crash/restart cycles. */
|
|
151
|
+
hasSentMessage(messageId) {
|
|
152
|
+
if (!messageId)
|
|
153
|
+
return false;
|
|
154
|
+
const row = this.db.prepare("SELECT 1 FROM sent_log WHERE message_id = ? LIMIT 1").get(messageId);
|
|
155
|
+
return !!row;
|
|
156
|
+
}
|
|
157
|
+
/** Record a successfully sent message so future attempts are skipped. */
|
|
158
|
+
recordSent(messageId, accountId, subject, recipients) {
|
|
159
|
+
if (!messageId)
|
|
160
|
+
return;
|
|
161
|
+
try {
|
|
162
|
+
this.db.prepare("INSERT INTO sent_log (message_id, account_id, subject, recipients, sent_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT(message_id) DO NOTHING").run(messageId, accountId, subject || "", recipients.join(", "), Date.now());
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
console.error(` [sent_log] failed to record ${messageId}: ${e.message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
140
168
|
/** Idempotently add a column to a table if it's missing. */
|
|
141
169
|
addColumnIfMissing(table, column, sqlType) {
|
|
142
170
|
try {
|