@bobfrankston/mailx 1.0.233 → 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":1060,"y":329}
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.233",
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.295",
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.295",
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',
@@ -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 {