@bobfrankston/mailx 1.0.173 → 1.0.175

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/bin/mailx.js CHANGED
@@ -740,10 +740,13 @@ async function main() {
740
740
  console.log(`${reason} — shutting down`);
741
741
  imapManager.stopPeriodicSync();
742
742
  imapManager.stopOutboxWorker();
743
+ // 3s hard timeout — don't hang on broken IMAP connections
744
+ const forceExit = setTimeout(() => { console.log("Forced exit"); process.exit(0); }, 3000);
743
745
  try {
744
746
  await imapManager.shutdown();
745
747
  }
746
748
  catch { /* proceed */ }
749
+ clearTimeout(forceExit);
747
750
  db.close();
748
751
  process.exit(0);
749
752
  }
@@ -0,0 +1 @@
1
+ {"height":1047,"width":1844,"x":664,"y":330}
package/client/app.js CHANGED
@@ -286,8 +286,14 @@ async function openCompose(mode) {
286
286
  }
287
287
  // Store init data for compose window to pick up
288
288
  sessionStorage.setItem("composeInit", JSON.stringify(init));
289
- // Use relative URL so it works with both HTTP and custom protocol (msger://)
290
- window.open("compose/compose.html", "_blank", "width=800,height=600,menubar=no,toolbar=no,status=no");
289
+ // IPC mode: navigate in same window (popups don't have custom protocol)
290
+ // HTTP mode: open as popup window
291
+ if (typeof mailxapi !== "undefined") {
292
+ window.location.href = "compose/compose.html";
293
+ }
294
+ else {
295
+ window.open("compose/compose.html", "_blank", "width=800,height=600,menubar=no,toolbar=no,status=no");
296
+ }
291
297
  }
292
298
  function quoteBody(msg) {
293
299
  const date = new Date(msg.date).toLocaleString();
@@ -135,7 +135,12 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
135
135
  draftFolderId: msg.folderId,
136
136
  };
137
137
  sessionStorage.setItem("composeInit", JSON.stringify(init));
138
- window.open("compose/compose.html", "_blank", "width=800,height=600,menubar=no,toolbar=no,status=no");
138
+ if (typeof window.mailxapi !== "undefined") {
139
+ window.location.href = "compose/compose.html";
140
+ }
141
+ else {
142
+ window.open("compose/compose.html", "_blank", "width=800,height=600,menubar=no,toolbar=no,status=no");
143
+ }
139
144
  };
140
145
  }
141
146
  else {
@@ -5,6 +5,15 @@
5
5
  */
6
6
  import { createEditor } from "./editor.js";
7
7
  import { getVersion, getSettings, getAccounts, searchContacts, sendMessage, saveDraft as apiSaveDraft, deleteDraft } from "../lib/api-client.js";
8
+ /** Close compose — navigate back in IPC mode, window.close() in HTTP mode */
9
+ function closeCompose() {
10
+ if (typeof window.mailxapi !== "undefined") {
11
+ window.location.href = "../index.html";
12
+ }
13
+ else {
14
+ closeCompose();
15
+ }
16
+ }
8
17
  // ── Load editor scripts dynamically ──
9
18
  function loadScript(src) {
10
19
  return new Promise((resolve, reject) => {
@@ -349,7 +358,7 @@ document.getElementById("btn-send")?.addEventListener("click", async () => {
349
358
  if (draftUid) {
350
359
  deleteDraft(getFromAccountId(), draftUid).catch(() => { });
351
360
  }
352
- window.close();
361
+ closeCompose();
353
362
  }
354
363
  catch (e) {
355
364
  btn.disabled = false;
@@ -8,9 +8,12 @@ function createQuillEditor(container) {
8
8
  placeholder: "Write your message...",
9
9
  modules: {
10
10
  toolbar: [
11
+ [{ font: [] }, { size: ["small", false, "large", "huge"] }],
11
12
  [{ header: [1, 2, 3, false] }],
12
13
  ["bold", "italic", "underline", "strike"],
14
+ [{ color: [] }, { background: [] }],
13
15
  [{ list: "ordered" }, { list: "bullet" }],
16
+ [{ align: [] }],
14
17
  ["blockquote", "link", "image"],
15
18
  ["clean"]
16
19
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.173",
3
+ "version": "1.0.175",
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,11 +20,11 @@
20
20
  "postinstall": "node bin/postinstall.js"
21
21
  },
22
22
  "dependencies": {
23
- "@bobfrankston/iflow-direct": "^0.1.4",
23
+ "@bobfrankston/iflow-direct": "^0.1.5",
24
24
  "@bobfrankston/iflow-node": "^0.1.1",
25
25
  "@bobfrankston/miscinfo": "^1.0.7",
26
26
  "@bobfrankston/oauthsupport": "^1.0.20",
27
- "@bobfrankston/msger": "^0.1.222",
27
+ "@bobfrankston/msger": "^0.1.224",
28
28
  "@capacitor/android": "^8.3.0",
29
29
  "@capacitor/cli": "^8.3.0",
30
30
  "@capacitor/core": "^8.3.0",
@@ -184,6 +184,8 @@ export declare class ImapManager extends EventEmitter {
184
184
  queueOutgoing(accountId: string, rawMessage: string | Buffer): Promise<void>;
185
185
  /** Process local file queue — move to IMAP Outbox when server is reachable */
186
186
  private processLocalQueue;
187
+ /** Send a raw RFC 2822 message via SMTP for a given account */
188
+ private sendRawViaSMTP;
187
189
  /** Process Outbox — send pending messages with flag-based interlock */
188
190
  processOutbox(accountId: string): Promise<void>;
189
191
  /** Start background Outbox worker — runs immediately then every 10 seconds */
@@ -291,8 +291,15 @@ export class ImapManager extends EventEmitter {
291
291
  const client = this.opsClients.get(accountId);
292
292
  this.opsClients.delete(accountId);
293
293
  if (client) {
294
+ // Force-close: don't wait for LOGOUT on a possibly dead socket
294
295
  try {
295
- await (client._realLogout || client.logout)();
296
+ const timeout = new Promise(r => setTimeout(r, 2000));
297
+ await Promise.race([(client._realLogout || client.logout)(), timeout]);
298
+ }
299
+ catch { /* */ }
300
+ // Destroy underlying socket if still open
301
+ try {
302
+ client.destroy?.();
296
303
  }
297
304
  catch { /* */ }
298
305
  console.log(` [conn] ${accountId}: disconnected`);
@@ -1674,6 +1681,23 @@ export class ImapManager extends EventEmitter {
1674
1681
  const files = fs.readdirSync(localQueue).filter(f => f.endsWith(".ltr"));
1675
1682
  if (files.length === 0)
1676
1683
  return;
1684
+ // Gmail/API accounts: send directly via SMTP from local queue (no IMAP outbox)
1685
+ if (this.isGmailAccount(accountId)) {
1686
+ for (const file of files) {
1687
+ const filePath = path.join(localQueue, file);
1688
+ const raw = fs.readFileSync(filePath, "utf-8");
1689
+ try {
1690
+ await this.sendRawViaSMTP(accountId, raw);
1691
+ fs.unlinkSync(filePath);
1692
+ console.log(` [outbox] Sent local ${file} via SMTP`);
1693
+ }
1694
+ catch (e) {
1695
+ console.error(` [outbox] Send failed for ${file}: ${e.message}`);
1696
+ }
1697
+ }
1698
+ return;
1699
+ }
1700
+ // IMAP accounts: append to IMAP Outbox for multi-machine interlock
1677
1701
  try {
1678
1702
  const outboxPath = await this.ensureOutbox(accountId);
1679
1703
  const client = await this.createClientWithLimit(accountId);
@@ -1697,6 +1721,48 @@ export class ImapManager extends EventEmitter {
1697
1721
  // IMAP still unreachable — leave files for next attempt
1698
1722
  }
1699
1723
  }
1724
+ /** Send a raw RFC 2822 message via SMTP for a given account */
1725
+ async sendRawViaSMTP(accountId, raw) {
1726
+ const settings = loadSettings();
1727
+ const account = settings.accounts.find(a => a.id === accountId);
1728
+ if (!account?.smtp)
1729
+ throw new Error(`No SMTP config for ${accountId}`);
1730
+ let smtpAuth;
1731
+ if (account.smtp.auth === "password") {
1732
+ smtpAuth = { user: account.smtp.user, pass: account.smtp.password };
1733
+ }
1734
+ else if (account.smtp.auth === "oauth2") {
1735
+ const accessToken = await this.getOAuthToken(accountId);
1736
+ if (!accessToken)
1737
+ throw new Error("OAuth token not available");
1738
+ smtpAuth = { type: "OAuth2", user: account.smtp.user, accessToken };
1739
+ }
1740
+ const { createTransport } = await import("nodemailer");
1741
+ const transport = createTransport({
1742
+ host: account.smtp.host,
1743
+ port: account.smtp.port,
1744
+ secure: account.smtp.port === 465,
1745
+ auth: smtpAuth,
1746
+ tls: { rejectUnauthorized: false },
1747
+ });
1748
+ const parseAddrs = (s) => s.match(/[\w.+-]+@[\w.-]+/g) || [];
1749
+ const toMatch = raw.match(/^To:\s*(.+)$/mi);
1750
+ const ccMatch = raw.match(/^Cc:\s*(.+)$/mi);
1751
+ const bccMatch = raw.match(/^Bcc:\s*(.+)$/mi);
1752
+ const fromMatch = raw.match(/^From:\s*(.+)$/mi);
1753
+ const recipients = [
1754
+ ...(toMatch ? parseAddrs(toMatch[1]) : []),
1755
+ ...(ccMatch ? parseAddrs(ccMatch[1]) : []),
1756
+ ...(bccMatch ? parseAddrs(bccMatch[1]) : []),
1757
+ ];
1758
+ const sender = fromMatch ? (parseAddrs(fromMatch[1])[0] || account.email) : account.email;
1759
+ if (recipients.length === 0)
1760
+ throw new Error("No recipients");
1761
+ const rawToSend = raw.replace(/^Bcc:.*\r?\n/mi, "");
1762
+ this.saveSendingCopy(accountId, rawToSend, "sent");
1763
+ await transport.sendMail({ envelope: { from: sender, to: recipients }, raw: rawToSend });
1764
+ console.log(` [smtp] ${accountId}: sent to ${recipients.join(", ")}`);
1765
+ }
1700
1766
  /** Process Outbox — send pending messages with flag-based interlock */
1701
1767
  async processOutbox(accountId) {
1702
1768
  const outboxFolder = this.findFolder(accountId, "outbox");
@@ -1705,7 +1771,7 @@ export class ImapManager extends EventEmitter {
1705
1771
  // Skip if this account's sync is failing — don't pile up connections
1706
1772
  if (this.connectionBackoff.has(accountId) && Date.now() < (this.connectionBackoff.get(accountId) || 0))
1707
1773
  return;
1708
- // Gmail uses SMTP for sending (not IMAP outbox) skip IMAP outbox check
1774
+ // Gmail: skip IMAP outbox check — sending handled by processLocalQueue which sends directly via SMTP
1709
1775
  if (this.isGmailAccount(accountId))
1710
1776
  return;
1711
1777
  const settings = loadSettings();
@@ -0,0 +1,58 @@
1
+ From: Bob Frankston <Bob.Frankston@Gmail.com>
2
+ To: David P. Reed <dpreed@deepplum.com>
3
+ Cc: Brian DeLacey <bdelacey@gmail.com>
4
+ Bcc: bob@bob.ma
5
+ Subject: Re: Converting POPFile from perl to Python
6
+ Date: Wed, 08 Apr 2026 01:02:13 GMT
7
+ Message-ID: <1775610133517.ug0o0gii4wm@Gmail.com>
8
+ MIME-Version: 1.0
9
+ Content-Type: text/html; charset=UTF-8
10
+ Content-Transfer-Encoding: base64
11
+
12
+ PHA+SSdtIHJlc3BvbmRpbmcgdXNpbmcgbXkgZW1haWwgcHJvZ3JhbS48L3A+PHA+PGJyPjwvcD48
13
+ cD5UaGVyZSBpcyBzbyBtdWNoIHRvIGNhdGNoIHVwIHdpdGggLS0gdGhlIGdvb2QgYW5kIHRoZSBi
14
+ YWQuIE9uZSBvYnNlcnZhdGlvbiBpcyB0aGF0IGl0IGxlYXJuZWQgdG8gbXVjaCB0byBwcm9ncmFt
15
+ IGxpa2UgYSBodW1hbiBhbmQgbm90IGVub3VnaC4gT25lIGh1bW9yb3VzIHRoaW5nIGlzIHdoZW4g
16
+ aXQgZXN0aW1hdGVzIGEgdGFzayB3aWxsIHRha2UgYSB3ZWVrIGJ1dCBpdCBkb2VzIGl0IGluIGZp
17
+ dmUgbWludXRlcyBiZWNhdXNlIGl0IGlzIHVzaW5nIHN0YW5kYXJkIG1lYXN1cmVzLiBJdCdzIGRl
18
+ c2lnbiBzZW5zZSBpcyBmdXJzaGl0IGJ1dCBpZiB5b3Ugd29yayB3aXRoIGl0IGFuZCBnZXQgcGFz
19
+ dCB0aGUgSmVuZ2EgbW9kZSB5b3UgY2FuIGdldCBsb3RzIGRvbmUuIEZvciBleGFtcGxlLCBpbWFw
20
+ IGRvZXNuJ3Qgd29yayB3ZWxsIHdpdGggZ21haWwgYnV0IGl0IHNhaWQgdGhhdCBhZGRpbmcgZ21h
21
+ aWwgd2FzIGEgbWFqb3IgcHJvamVjdCBidXQgSSBzYWlkIGRvIGl0IGFueXdheSBhbmQgZml2ZSBt
22
+ aW51dGVzIGxhdGVyIGl0IHdhcyBkb25lLiBucG0gaW5zdGFsbCAtZyBAYm9iZnJhbmtzdG9uL21h
23
+ aWx4IGlzbid0IHF1aXQgcmVhZHkgZm9yIHByaW1lIHRpbWUuPC9wPjxwPjxicj48L3A+PHA+U3Rp
24
+ bGwgYSBodWdlIHRvZG8gbGlzdCAoaW4gdGhlIG1haWx4IGRpciBpZiB5b3UncmUgY3VyaW91cy4g
25
+ SXQgd2lsbCB3YW50IHRvIHN0YXJ0IHdpdGggYSBnbWFpbCBhZGRyZXNzIGFuZCBjcmVhdGVzIGEg
26
+ bWFpbHggZGlyZWN0b3J5IG9uIGdkcml2ZS4gQSBnYWluLCBzdGlsbCBsb3RzIG9mIGlzc3VlcyB3
27
+ aGljaCB5b3UgY2FuIHJlcG9ydCB0byBtZSBidXQgaXQgaXMgY29taW5nIHRvZ2V0aGVyLjwvcD48
28
+ cD48YnI+PC9wPjxwPk9uIDQvNy8yMDI2LCA1OjA0OjU3IFBNLCBEYXZpZCBQLiBSZWVkICZsdDtk
29
+ cHJlZWRAZGVlcHBsdW0uY29tJmd0OyB3cm90ZTo8L3A+PGRpdiBjbGFzcz0icWwtY29kZS1ibG9j
30
+ ay1jb250YWluZXIiIHNwZWxsY2hlY2s9ImZhbHNlIj48ZGl2IGNsYXNzPSJxbC1jb2RlLWJsb2Nr
31
+ IiBkYXRhLWxhbmd1YWdlPSJwbGFpbiI+Rmlyc3Qgb3ZlcmFsbCB0ZXN0IGJlZ2FuIGxvb3Bpbmcg
32
+ Zm9yZXZlci4gSSBzdG9wcGVkIGl0LCBhbmQgaW5xdWlyZWQgd2hhdCB0aGUgaGVsbCwgYW5kPC9k
33
+ aXY+PGRpdiBjbGFzcz0icWwtY29kZS1ibG9jayIgZGF0YS1sYW5ndWFnZT0icGxhaW4iPmhlcmUg
34
+ aXMgd2hhdCBpdCBzYXlzLiBHYWFoLiBUaGlzIGlzIG5vdCBleHBsYWluaW5nIG11Y2gsIGFuZCBp
35
+ dCB3cm90ZSB0aGlzIGNvZGUuLi48L2Rpdj48ZGl2IGNsYXNzPSJxbC1jb2RlLWJsb2NrIiBkYXRh
36
+ LWxhbmd1YWdlPSJwbGFpbiI+PGJyPjwvZGl2PjxkaXYgY2xhc3M9InFsLWNvZGUtYmxvY2siIGRh
37
+ dGEtbGFuZ3VhZ2U9InBsYWluIj4tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLTwvZGl2Pjxk
38
+ aXYgY2xhc3M9InFsLWNvZGUtYmxvY2siIGRhdGEtbGFuZ3VhZ2U9InBsYWluIj5UaGUgdGVzdCBo
39
+ YW5ncyBiZWNhdXNlICNoYW5kbGVDbGllbnQgaW4gUE9QM1Byb3h5IHRocm93cyBiZWZvcmUgc2Vu
40
+ ZGluZyB0aGUgYmFubmVyLiBUaGUgY3VscHJpdCBpcyB0aGlzIGRlYWQgY29kZTo8L2Rpdj48ZGl2
41
+ IGNsYXNzPSJxbC1jb2RlLWJsb2NrIiBkYXRhLWxhbmd1YWdlPSJwbGFpbiI+PGJyPjwvZGl2Pjxk
42
+ aXYgY2xhc3M9InFsLWNvZGUtYmxvY2siIGRhdGEtbGFuZ3VhZ2U9InBsYWluIj4gIGNvbnN0IGNs
43
+ aWVudFdyaXRlciA9IG5ldyBXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXIoPC9kaXY+PGRpdiBj
44
+ bGFzcz0icWwtY29kZS1ibG9jayIgZGF0YS1sYW5ndWFnZT0icGxhaW4iPiAgICBjbGllbnQud3Jp
45
+ dGFibGUuZ2V0V3JpdGVyKCkucmVsZWFzZUxvY2soKSBhcyB1bmtub3duIGFzIFdyaXRhYmxlU3Ry
46
+ ZWFtPC9kaXY+PGRpdiBjbGFzcz0icWwtY29kZS1ibG9jayIgZGF0YS1sYW5ndWFnZT0icGxhaW4i
47
+ PiAgKTs8L2Rpdj48ZGl2IGNsYXNzPSJxbC1jb2RlLWJsb2NrIiBkYXRhLWxhbmd1YWdlPSJwbGFp
48
+ biI+PGJyPjwvZGl2PjxkaXYgY2xhc3M9InFsLWNvZGUtYmxvY2siIGRhdGEtbGFuZ3VhZ2U9InBs
49
+ YWluIj4gIHJlbGVhc2VMb2NrKCkgcmV0dXJucyB2b2lkLiBQYXNzaW5nIHZvaWQgdG8gV3JpdGFi
50
+ bGVTdHJlYW1EZWZhdWx0V3JpdGVyIHRocm93cyBhIFR5cGVFcnJvciwgd2hpY2ggaXMgc3dhbGxv
51
+ d2VkIGJ5IHRoZTwvZGl2PjxkaXYgY2xhc3M9InFsLWNvZGUtYmxvY2siIGRhdGEtbGFuZ3VhZ2U9
52
+ InBsYWluIj4gIGFjY2VwdCBsb29wJ3MgLmNhdGNoIOKAlCBzbyB0aGUgY2xpZW50IG5ldmVyIHJl
53
+ Y2VpdmVzIHRoZSBiYW5uZXIgYW5kIHdhaXRzIGZvcmV2ZXIuIGNsaWVudFdyaXRlciBpcyBhbHNv
54
+ IG5ldmVyIHVzZWQuPC9kaXY+PGRpdiBjbGFzcz0icWwtY29kZS1ibG9jayIgZGF0YS1sYW5ndWFn
55
+ ZT0icGxhaW4iPiAgRml4OjwvZGl2PjxkaXYgY2xhc3M9InFsLWNvZGUtYmxvY2siIGRhdGEtbGFu
56
+ Z3VhZ2U9InBsYWluIj48YnI+PC9kaXY+PGRpdiBjbGFzcz0icWwtY29kZS1ibG9jayIgZGF0YS1s
57
+ YW5ndWFnZT0icGxhaW4iPjxicj48L2Rpdj48ZGl2IGNsYXNzPSJxbC1jb2RlLWJsb2NrIiBkYXRh
58
+ LWxhbmd1YWdlPSJwbGFpbiI+PGJyPjwvZGl2PjwvZGl2Pg==