@bobfrankston/mailx 1.0.174 → 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.
@@ -0,0 +1 @@
1
+ {"height":1047,"width":1844,"x":664,"y":330}
@@ -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.174",
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",
@@ -24,7 +24,7 @@
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.223",
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 */
@@ -1681,6 +1681,23 @@ export class ImapManager extends EventEmitter {
1681
1681
  const files = fs.readdirSync(localQueue).filter(f => f.endsWith(".ltr"));
1682
1682
  if (files.length === 0)
1683
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
1684
1701
  try {
1685
1702
  const outboxPath = await this.ensureOutbox(accountId);
1686
1703
  const client = await this.createClientWithLimit(accountId);
@@ -1704,6 +1721,48 @@ export class ImapManager extends EventEmitter {
1704
1721
  // IMAP still unreachable — leave files for next attempt
1705
1722
  }
1706
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
+ }
1707
1766
  /** Process Outbox — send pending messages with flag-based interlock */
1708
1767
  async processOutbox(accountId) {
1709
1768
  const outboxFolder = this.findFolder(accountId, "outbox");
@@ -1712,7 +1771,7 @@ export class ImapManager extends EventEmitter {
1712
1771
  // Skip if this account's sync is failing — don't pile up connections
1713
1772
  if (this.connectionBackoff.has(accountId) && Date.now() < (this.connectionBackoff.get(accountId) || 0))
1714
1773
  return;
1715
- // 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
1716
1775
  if (this.isGmailAccount(accountId))
1717
1776
  return;
1718
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==