@bobfrankston/mailx 1.0.232 → 1.0.234
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.234",
|
|
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.296",
|
|
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.296",
|
|
82
82
|
"@capacitor/android": "^8.3.0",
|
|
83
83
|
"@capacitor/cli": "^8.3.0",
|
|
84
84
|
"@capacitor/core": "^8.3.0",
|
|
@@ -1957,6 +1957,8 @@ export class ImapManager extends EventEmitter {
|
|
|
1957
1957
|
const ccMatch = raw.match(/^Cc:\s*(.+)$/mi);
|
|
1958
1958
|
const bccMatch = raw.match(/^Bcc:\s*(.+)$/mi);
|
|
1959
1959
|
const fromMatch = raw.match(/^From:\s*(.+)$/mi);
|
|
1960
|
+
const subjectMatch = raw.match(/^Subject:\s*(.+)$/mi);
|
|
1961
|
+
const messageIdMatch = raw.match(/^Message-ID:\s*(<[^>]+>)/mi);
|
|
1960
1962
|
const recipients = [
|
|
1961
1963
|
...(toMatch ? parseAddrs(toMatch[1]) : []),
|
|
1962
1964
|
...(ccMatch ? parseAddrs(ccMatch[1]) : []),
|
|
@@ -1965,9 +1967,22 @@ export class ImapManager extends EventEmitter {
|
|
|
1965
1967
|
const sender = fromMatch ? (parseAddrs(fromMatch[1])[0] || account.email) : account.email;
|
|
1966
1968
|
if (recipients.length === 0)
|
|
1967
1969
|
throw new Error("No recipients");
|
|
1970
|
+
// Dedup: skip if this Message-ID has already been sent. Prevents the
|
|
1971
|
+
// outbox from re-sending the same file across crash/restart cycles.
|
|
1972
|
+
// Without this, a queued .ltr that was mid-delivery when mailx crashed
|
|
1973
|
+
// would be re-sent on every startup until the rename loop completed.
|
|
1974
|
+
const messageId = messageIdMatch ? messageIdMatch[1] : "";
|
|
1975
|
+
if (messageId && this.db.hasSentMessage(messageId)) {
|
|
1976
|
+
console.log(` [smtp] ${accountId}: SKIP ${messageId} — already in sent_log`);
|
|
1977
|
+
return; // caller will move the file to sent/ without re-sending
|
|
1978
|
+
}
|
|
1968
1979
|
const rawToSend = raw.replace(/^Bcc:.*\r?\n/mi, "");
|
|
1969
1980
|
this.saveSendingCopy(accountId, rawToSend, "sent");
|
|
1970
1981
|
await transport.sendMail({ envelope: { from: sender, to: recipients }, raw: rawToSend });
|
|
1982
|
+
// Record the successful send so future attempts dedupe against it.
|
|
1983
|
+
if (messageId) {
|
|
1984
|
+
this.db.recordSent(messageId, accountId, subjectMatch?.[1]?.trim() || "", recipients);
|
|
1985
|
+
}
|
|
1971
1986
|
console.log(` [smtp] ${accountId}: sent to ${recipients.join(", ")}`);
|
|
1972
1987
|
}
|
|
1973
1988
|
/** 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',
|
|
@@ -127,20 +135,49 @@ export class MailxDB {
|
|
|
127
135
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
128
136
|
this.db.exec(SCHEMA);
|
|
129
137
|
// Idempotent migrations for older databases that predate new columns.
|
|
130
|
-
// SQLite doesn't support "ADD COLUMN IF NOT EXISTS", so we
|
|
131
|
-
//
|
|
138
|
+
// SQLite doesn't support "ADD COLUMN IF NOT EXISTS", so we just try the
|
|
139
|
+
// ALTER and catch the "duplicate column" error. Simpler and more robust
|
|
140
|
+
// than probing via PRAGMA table_info (which can behave differently
|
|
141
|
+
// across sqlite drivers).
|
|
132
142
|
this.addColumnIfMissing("messages", "thread_id", "TEXT");
|
|
133
143
|
try {
|
|
134
144
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_messages_thread_id ON messages(account_id, thread_id)");
|
|
135
145
|
}
|
|
136
146
|
catch { /* already exists */ }
|
|
137
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
|
+
}
|
|
138
168
|
/** Idempotently add a column to a table if it's missing. */
|
|
139
169
|
addColumnIfMissing(table, column, sqlType) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
170
|
+
try {
|
|
171
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${sqlType}`);
|
|
172
|
+
console.log(` [db] added column ${table}.${column}`);
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
const msg = String(e?.message || e);
|
|
176
|
+
// "duplicate column name" is the expected case when column already exists
|
|
177
|
+
if (!/duplicate column/i.test(msg)) {
|
|
178
|
+
console.error(` [db] migration ${table}.${column} failed: ${msg}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
144
181
|
}
|
|
145
182
|
/** Compute a thread id for an incoming message. Strategy:
|
|
146
183
|
* 1. If any ancestor (in_reply_to or references) is already present in
|