@bobfrankston/rmfmail 1.2.232 → 1.2.233

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.
Files changed (146) hide show
  1. package/client/android-bootstrap.bundle.js +164 -152
  2. package/client/android-bootstrap.bundle.js.map +4 -4
  3. package/client/app.bundle.js +10 -104
  4. package/client/app.bundle.js.map +4 -4
  5. package/client/components/message-list.js +0 -60
  6. package/client/components/message-list.js.map +1 -1
  7. package/client/components/message-list.ts +0 -56
  8. package/client/compose/compose.bundle.js +1 -12
  9. package/client/compose/compose.bundle.js.map +2 -2
  10. package/client/compose/compose.html +0 -8
  11. package/client/compose/ghost-text.js +0 -8
  12. package/client/compose/ghost-text.js.map +1 -1
  13. package/client/compose/ghost-text.ts +0 -6
  14. package/client/compose/spellcheck-core.js +0 -1
  15. package/client/compose/spellcheck-core.js.map +1 -1
  16. package/client/compose/spellcheck-core.ts +0 -1
  17. package/client/index.html +0 -8
  18. package/client/lib/local-service.js +0 -73
  19. package/client/lib/local-service.js.map +1 -1
  20. package/client/lib/local-service.ts +0 -71
  21. package/client/lib/local-store.js +0 -3
  22. package/client/lib/local-store.js.map +1 -1
  23. package/client/lib/local-store.ts +0 -4
  24. package/package.json +1 -3
  25. package/packages/mailx-host/index.d.ts +0 -1
  26. package/packages/mailx-host/index.d.ts.map +1 -1
  27. package/packages/mailx-host/index.js +0 -1
  28. package/packages/mailx-host/index.js.map +1 -1
  29. package/packages/mailx-host/index.ts +0 -1
  30. package/packages/mailx-host/package.json +1 -1
  31. package/packages/mailx-service/index.d.ts.map +1 -1
  32. package/packages/mailx-service/index.js +36 -139
  33. package/packages/mailx-service/index.js.map +1 -1
  34. package/packages/mailx-service/index.ts +36 -148
  35. package/packages/mailx-settings/cloud.d.ts +0 -5
  36. package/packages/mailx-settings/cloud.d.ts.map +1 -1
  37. package/packages/mailx-settings/cloud.js +0 -16
  38. package/packages/mailx-settings/cloud.js.map +1 -1
  39. package/packages/mailx-settings/cloud.ts +0 -12
  40. package/packages/mailx-settings/index.d.ts +0 -3
  41. package/packages/mailx-settings/index.d.ts.map +1 -1
  42. package/packages/mailx-settings/index.js +0 -5
  43. package/packages/mailx-settings/index.js.map +1 -1
  44. package/packages/mailx-settings/index.ts +0 -7
  45. package/packages/mailx-store-web/main-thread-host.d.ts +1 -1
  46. package/packages/mailx-store-web/main-thread-host.d.ts.map +1 -1
  47. package/packages/mailx-store-web/main-thread-host.js +1 -38
  48. package/packages/mailx-store-web/main-thread-host.js.map +1 -1
  49. package/packages/mailx-store-web/main-thread-host.ts +0 -42
  50. package/packages/mailx-store-web/package.json +1 -1
  51. package/packages/mailx-store-web/web-service.d.ts.map +1 -1
  52. package/packages/mailx-store-web/web-service.js +28 -114
  53. package/packages/mailx-store-web/web-service.js.map +1 -1
  54. package/packages/mailx-store-web/web-service.ts +28 -124
  55. package/packages/mailx-store-web/web-settings.d.ts +0 -4
  56. package/packages/mailx-store-web/web-settings.d.ts.map +1 -1
  57. package/packages/mailx-store-web/web-settings.js +0 -29
  58. package/packages/mailx-store-web/web-settings.js.map +1 -1
  59. package/packages/mailx-store-web/web-settings.ts +0 -24
  60. package/packages/mailx-types/index.d.ts +2 -0
  61. package/packages/mailx-types/index.d.ts.map +1 -1
  62. package/packages/mailx-types/index.js +1 -0
  63. package/packages/mailx-types/index.js.map +1 -1
  64. package/packages/mailx-types/index.ts +2 -0
  65. package/packages/mailx-types/mime-build.d.ts +74 -0
  66. package/packages/mailx-types/mime-build.d.ts.map +1 -0
  67. package/packages/mailx-types/mime-build.js +160 -0
  68. package/packages/mailx-types/mime-build.js.map +1 -0
  69. package/packages/mailx-types/mime-build.ts +217 -0
  70. package/packages/mailx-types/package.json +1 -1
  71. package/packages/mailx-types/test/mime-build.test.mjs +200 -0
  72. package/client/components/calendar.js +0 -220
  73. package/client/components/calendar.js.map +0 -1
  74. package/client/components/calendar.ts +0 -213
  75. package/client/components/tasks.js +0 -261
  76. package/client/components/tasks.js.map +0 -1
  77. package/client/components/tasks.ts +0 -234
  78. package/packages/mailx-compose/.gitattributes +0 -10
  79. package/packages/mailx-compose/index.d.ts +0 -21
  80. package/packages/mailx-compose/index.d.ts.map +0 -1
  81. package/packages/mailx-compose/index.js +0 -66
  82. package/packages/mailx-compose/index.js.map +0 -1
  83. package/packages/mailx-compose/index.ts +0 -91
  84. package/packages/mailx-compose/package.json +0 -18
  85. package/packages/mailx-compose/tsconfig.json +0 -9
  86. package/packages/mailx-core/index.d.ts +0 -142
  87. package/packages/mailx-core/index.d.ts.map +0 -1
  88. package/packages/mailx-core/index.js +0 -430
  89. package/packages/mailx-core/index.js.map +0 -1
  90. package/packages/mailx-core/index.ts +0 -450
  91. package/packages/mailx-core/ipc.d.ts +0 -13
  92. package/packages/mailx-core/ipc.d.ts.map +0 -1
  93. package/packages/mailx-core/ipc.js +0 -56
  94. package/packages/mailx-core/ipc.js.map +0 -1
  95. package/packages/mailx-core/ipc.ts +0 -62
  96. package/packages/mailx-core/package.json +0 -18
  97. package/packages/mailx-core/tsconfig.json +0 -9
  98. package/packages/mailx-send/.gitattributes +0 -10
  99. package/packages/mailx-send/README.md +0 -217
  100. package/packages/mailx-send/cli-queue.d.ts +0 -11
  101. package/packages/mailx-send/cli-queue.d.ts.map +0 -1
  102. package/packages/mailx-send/cli-queue.js +0 -59
  103. package/packages/mailx-send/cli-queue.js.map +0 -1
  104. package/packages/mailx-send/cli-queue.ts +0 -62
  105. package/packages/mailx-send/cli-send.d.ts +0 -17
  106. package/packages/mailx-send/cli-send.d.ts.map +0 -1
  107. package/packages/mailx-send/cli-send.js +0 -75
  108. package/packages/mailx-send/cli-send.js.map +0 -1
  109. package/packages/mailx-send/cli-send.ts +0 -83
  110. package/packages/mailx-send/cli.d.ts +0 -22
  111. package/packages/mailx-send/cli.d.ts.map +0 -1
  112. package/packages/mailx-send/cli.js +0 -115
  113. package/packages/mailx-send/cli.js.map +0 -1
  114. package/packages/mailx-send/cli.ts +0 -126
  115. package/packages/mailx-send/index.d.ts +0 -77
  116. package/packages/mailx-send/index.d.ts.map +0 -1
  117. package/packages/mailx-send/index.js +0 -269
  118. package/packages/mailx-send/index.js.map +0 -1
  119. package/packages/mailx-send/index.ts +0 -338
  120. package/packages/mailx-send/mailsend/README.md +0 -133
  121. package/packages/mailx-send/mailsend/cli.d.ts +0 -12
  122. package/packages/mailx-send/mailsend/cli.d.ts.map +0 -1
  123. package/packages/mailx-send/mailsend/cli.js +0 -70
  124. package/packages/mailx-send/mailsend/cli.js.map +0 -1
  125. package/packages/mailx-send/mailsend/cli.ts +0 -81
  126. package/packages/mailx-send/mailsend/index.d.ts +0 -77
  127. package/packages/mailx-send/mailsend/index.d.ts.map +0 -1
  128. package/packages/mailx-send/mailsend/index.js +0 -264
  129. package/packages/mailx-send/mailsend/index.js.map +0 -1
  130. package/packages/mailx-send/mailsend/index.ts +0 -336
  131. package/packages/mailx-send/mailsend/package-lock.json +0 -65
  132. package/packages/mailx-send/mailsend/package.json +0 -26
  133. package/packages/mailx-send/mailsend/tsconfig.json +0 -21
  134. package/packages/mailx-send/package-lock.json +0 -65
  135. package/packages/mailx-send/package.json +0 -37
  136. package/packages/mailx-send/tsconfig.json +0 -21
  137. package/packages/mailx-server/.gitattributes +0 -10
  138. package/packages/mailx-server/index.d.ts +0 -8
  139. package/packages/mailx-server/index.d.ts.map +0 -1
  140. package/packages/mailx-server/index.js +0 -412
  141. package/packages/mailx-server/index.js.map +0 -1
  142. package/packages/mailx-server/index.ts +0 -445
  143. package/packages/mailx-server/package.json +0 -28
  144. package/packages/mailx-server/tsconfig.json +0 -9
  145. package/packages/mailx-server/tsconfig.tsbuildinfo +0 -1
  146. /package/packages/mailx-imap/{node_modules.npmglobalize-stash-40260 → node_modules.npmglobalize-stash-40004}/.package-lock.json +0 -0
@@ -1,338 +0,0 @@
1
- /**
2
- * @bobfrankston/mailsend
3
- * Queue-based mail sender. Each account has its own queue directory.
4
- * Messages queued as .ltr (RFC 822) files, sent via SMTP with retry.
5
- * Supports password and OAuth2 (Gmail) authentication.
6
- *
7
- * Directory structure:
8
- * {baseDir}/{accountId}/queue/ — pending .ltr files
9
- * {baseDir}/{accountId}/sent/ — sent messages (optional, YYYY/MM/DD)
10
- * {baseDir}/{accountId}/fail/ — failed messages for retry
11
- */
12
-
13
- import * as fs from "node:fs";
14
- import * as path from "node:path";
15
- import { createTransport, type Transporter } from "nodemailer";
16
- import { encodeHeaderWord, encodeAddressHeader } from "@bobfrankston/mailx-types";
17
-
18
- // ── Types ──
19
-
20
- export interface SmtpConfig {
21
- host: string;
22
- port: number;
23
- secure?: boolean;
24
- auth: "password" | "oauth2";
25
- user: string;
26
- password?: string;
27
- /** Default From address, e.g. "Bob Frankston <bob@iecc.com>" */
28
- from?: string;
29
- tokenProvider?: () => Promise<string>;
30
- }
31
-
32
- export interface MailMessage {
33
- from: string;
34
- to: string[];
35
- cc?: string[];
36
- bcc?: string[];
37
- subject: string;
38
- html?: string;
39
- text?: string;
40
- inReplyTo?: string;
41
- references?: string;
42
- accountId?: string;
43
- }
44
-
45
- export interface MailSendConfig {
46
- /** Base directory for all queues */
47
- baseDir: string;
48
- /** SMTP configs keyed by account ID */
49
- accounts: Record<string, SmtpConfig>;
50
- /** Log sent messages to sent/ subdirectory */
51
- logSent?: boolean;
52
- /** Retry interval in ms (default: 5 minutes) */
53
- retryIntervalMs?: number;
54
- /** Default account ID */
55
- defaultAccount?: string;
56
- }
57
-
58
- // ── File helpers ──
59
-
60
- function generateFilename(): string {
61
- const now = new Date();
62
- const pad2 = (n: number) => String(n).padStart(2, "0");
63
- const ts = `${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}_${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
64
- const seq = String(Math.floor(Math.random() * 10000)).padStart(4, "0");
65
- return `${ts}-${seq}.ltr`;
66
- }
67
-
68
- function datePath(): string {
69
- const dt = new Date();
70
- const pad2 = (n: number) => String(n).padStart(2, "0");
71
- return `${dt.getFullYear()}/${pad2(dt.getMonth() + 1)}/${pad2(dt.getDate())}`;
72
- }
73
-
74
- function moveToDateDir(filePath: string, destBase: string): void {
75
- const dest = path.join(destBase, datePath());
76
- fs.mkdirSync(dest, { recursive: true });
77
- fs.renameSync(filePath, path.join(dest, path.basename(filePath)));
78
- }
79
-
80
- function listLtr(dir: string): string[] {
81
- if (!fs.existsSync(dir)) return [];
82
- return fs.readdirSync(dir).filter(f => f.endsWith(".ltr")).sort();
83
- }
84
-
85
- function walkLtrCount(dir: string): number {
86
- if (!fs.existsSync(dir)) return 0;
87
- let count = 0;
88
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
89
- if (entry.isDirectory()) count += walkLtrCount(path.join(dir, entry.name));
90
- else if (entry.name.endsWith(".ltr")) count++;
91
- }
92
- return count;
93
- }
94
-
95
- function walkLtrFiles(dir: string): string[] {
96
- if (!fs.existsSync(dir)) return [];
97
- const files: string[] = [];
98
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
99
- const full = path.join(dir, entry.name);
100
- if (entry.isDirectory()) files.push(...walkLtrFiles(full));
101
- else if (entry.name.endsWith(".ltr")) files.push(full);
102
- }
103
- return files;
104
- }
105
-
106
- // ── RFC 822 builder ──
107
-
108
- export function buildRfc822(msg: MailMessage): string {
109
- const lines: string[] = [];
110
- // RFC 2047-encode anything non-ASCII. Raw 8-bit here makes the whole
111
- // message EAI (RFC 6530) and it bounces from every receiver without
112
- // SMTPUTF8 — one curly apostrophe in a reply subject was enough
113
- // (Bob 2026-08-08, gal.iecc.com bounce).
114
- lines.push(`From: ${encodeAddressHeader(msg.from)}`);
115
- lines.push(`To: ${encodeAddressHeader(msg.to.join(", "))}`);
116
- if (msg.cc?.length) lines.push(`Cc: ${encodeAddressHeader(msg.cc.join(", "))}`);
117
- if (msg.bcc?.length) lines.push(`Bcc: ${encodeAddressHeader(msg.bcc.join(", "))}`);
118
- lines.push(`Subject: ${encodeHeaderWord(msg.subject)}`);
119
- lines.push(`Date: ${new Date().toUTCString()}`);
120
- if (msg.inReplyTo) lines.push(`In-Reply-To: ${msg.inReplyTo}`);
121
- if (msg.references) lines.push(`References: ${msg.references}`);
122
- if (msg.accountId) lines.push(`X-MailSend-Account: ${msg.accountId}`);
123
- lines.push(`MIME-Version: 1.0`);
124
-
125
- if (msg.html) {
126
- lines.push(`Content-Type: text/html; charset=UTF-8`);
127
- lines.push(`Content-Transfer-Encoding: 8bit`);
128
- lines.push("");
129
- lines.push(msg.html);
130
- } else {
131
- lines.push(`Content-Type: text/plain; charset=UTF-8`);
132
- lines.push(`Content-Transfer-Encoding: 8bit`);
133
- lines.push("");
134
- lines.push(msg.text || "");
135
- }
136
-
137
- return lines.join("\r\n");
138
- }
139
-
140
- // ── Transport cache ──
141
-
142
- const transports: Map<string, Transporter> = new Map();
143
-
144
- function getTransport(config: SmtpConfig): Transporter {
145
- const key = `${config.host}:${config.port}:${config.user}`;
146
- let transport = transports.get(key);
147
- if (!transport) {
148
- const opts: any = {
149
- host: config.host,
150
- port: config.port,
151
- secure: config.secure ?? (config.port === 465),
152
- tls: { rejectUnauthorized: false },
153
- };
154
-
155
- if (config.auth === "password") {
156
- opts.auth = { user: config.user, pass: config.password };
157
- } else if (config.auth === "oauth2") {
158
- opts.auth = { type: "OAuth2", user: config.user };
159
- }
160
-
161
- transport = createTransport(opts);
162
- transports.set(key, transport);
163
- }
164
- return transport;
165
- }
166
-
167
- // ── MailSender ──
168
-
169
- let singleton: MailSender | null = null;
170
-
171
- export class MailSender {
172
- private config: MailSendConfig;
173
- private processing = false;
174
- private retryTimer: ReturnType<typeof setInterval> | null = null;
175
-
176
- constructor(config: MailSendConfig) {
177
- this.config = config;
178
- // Ensure base directory exists
179
- fs.mkdirSync(config.baseDir, { recursive: true });
180
- }
181
-
182
- private accountDir(accountId: string): string {
183
- return path.join(this.config.baseDir, accountId);
184
- }
185
-
186
- private queueDir(accountId: string): string {
187
- const dir = path.join(this.accountDir(accountId), "queue");
188
- fs.mkdirSync(dir, { recursive: true });
189
- return dir;
190
- }
191
-
192
- private sentDir(accountId: string): string {
193
- return path.join(this.accountDir(accountId), "sent");
194
- }
195
-
196
- private failDir(accountId: string): string {
197
- const dir = path.join(this.accountDir(accountId), "fail");
198
- fs.mkdirSync(dir, { recursive: true });
199
- return dir;
200
- }
201
-
202
- /** Queue a message. Returns the filename. */
203
- queueMessage(msg: MailMessage): string {
204
- const accountId = msg.accountId || this.config.defaultAccount;
205
- if (!accountId) throw new Error("No accountId and no defaultAccount configured");
206
- msg.accountId = accountId;
207
-
208
- const content = buildRfc822(msg);
209
- const filename = generateFilename();
210
- fs.writeFileSync(path.join(this.queueDir(accountId), filename), content);
211
-
212
- // Process immediately (non-blocking)
213
- this.processQueue(accountId).catch(e =>
214
- console.error(`[mailsend] Process error: ${e.message}`)
215
- );
216
-
217
- return filename;
218
- }
219
-
220
- /** Process pending messages for one account */
221
- async processQueue(accountId: string): Promise<void> {
222
- if (this.processing) return;
223
- this.processing = true;
224
-
225
- try {
226
- const smtpConfig = this.config.accounts[accountId];
227
- if (!smtpConfig) {
228
- console.error(`[mailsend] No SMTP config for: ${accountId}`);
229
- return;
230
- }
231
-
232
- const files = listLtr(this.queueDir(accountId));
233
- for (const file of files) {
234
- await this.sendFile(path.join(this.queueDir(accountId), file), accountId, smtpConfig);
235
- }
236
- } finally {
237
- this.processing = false;
238
- }
239
- }
240
-
241
- /** Process all account queues */
242
- async processAllQueues(): Promise<void> {
243
- for (const accountId of Object.keys(this.config.accounts)) {
244
- await this.processQueue(accountId);
245
- }
246
- }
247
-
248
- private async sendFile(filePath: string, accountId: string, smtpConfig: SmtpConfig): Promise<boolean> {
249
- const content = fs.readFileSync(filePath, "utf-8");
250
-
251
- try {
252
- const transport = getTransport(smtpConfig);
253
-
254
- if (smtpConfig.auth === "oauth2" && smtpConfig.tokenProvider) {
255
- const token = await smtpConfig.tokenProvider();
256
- (transport as any).options.auth.accessToken = token;
257
- }
258
-
259
- const result = await transport.sendMail({ raw: content });
260
-
261
- if (result.rejected?.length > 0) {
262
- console.error(`[mailsend] Rejected: ${result.rejected.join(", ")} — ${path.basename(filePath)}`);
263
- moveToDateDir(filePath, this.failDir(accountId));
264
- return false;
265
- }
266
-
267
- console.log(`[mailsend] Sent: ${path.basename(filePath)} → ${result.accepted?.join(", ")} (${result.messageId})`);
268
-
269
- if (this.config.logSent) {
270
- moveToDateDir(filePath, this.sentDir(accountId));
271
- } else {
272
- fs.unlinkSync(filePath);
273
- }
274
- return true;
275
- } catch (e: any) {
276
- console.error(`[mailsend] Error: ${path.basename(filePath)}: ${e.message}`);
277
- moveToDateDir(filePath, this.failDir(accountId));
278
- return false;
279
- }
280
- }
281
-
282
- /** Start background worker — retries failed, processes queues */
283
- startWorker(): void {
284
- if (this.retryTimer) return;
285
- const interval = this.config.retryIntervalMs || 300000;
286
-
287
- this.retryTimer = setInterval(async () => {
288
- for (const accountId of Object.keys(this.config.accounts)) {
289
- this.retryFailed(accountId);
290
- }
291
- await this.processAllQueues();
292
- }, interval);
293
- }
294
-
295
- private retryFailed(accountId: string): void {
296
- const failFiles = walkLtrFiles(this.failDir(accountId));
297
- const queueDir = this.queueDir(accountId);
298
- for (const file of failFiles) {
299
- fs.renameSync(file, path.join(queueDir, path.basename(file)));
300
- }
301
- }
302
-
303
- stopWorker(): void {
304
- if (this.retryTimer) {
305
- clearInterval(this.retryTimer);
306
- this.retryTimer = null;
307
- }
308
- }
309
-
310
- /** Get queue status per account */
311
- getStatus(): Record<string, { pending: number; failed: number }> {
312
- const result: Record<string, { pending: number; failed: number }> = {};
313
- for (const accountId of Object.keys(this.config.accounts)) {
314
- result[accountId] = {
315
- pending: listLtr(this.queueDir(accountId)).length,
316
- failed: walkLtrCount(this.failDir(accountId)),
317
- };
318
- }
319
- return result;
320
- }
321
- }
322
-
323
- // ── Singleton API ──
324
-
325
- export function initMailSend(config: MailSendConfig): MailSender {
326
- singleton = new MailSender(config);
327
- singleton.startWorker();
328
- return singleton;
329
- }
330
-
331
- export function getMailSender(): MailSender | null {
332
- return singleton;
333
- }
334
-
335
- export function queueMessage(msg: MailMessage): string {
336
- if (!singleton) throw new Error("mailsend not initialized — call initMailSend() first");
337
- return singleton.queueMessage(msg);
338
- }
@@ -1,133 +0,0 @@
1
- # @bobfrankston/mailsend
2
-
3
- Queue-based mail sender with SMTP and OAuth2 support. Messages are queued as RFC 822 `.ltr` files and sent via SMTP with automatic retry.
4
-
5
- Each account gets its own queue, sent log, and fail directory.
6
-
7
- ## Install
8
-
9
- ```bash
10
- npm install @bobfrankston/mailsend
11
- # or globally for CLI:
12
- npm install -g @bobfrankston/mailsend
13
- ```
14
-
15
- ## Directory Structure
16
-
17
- ```
18
- {baseDir}/
19
- {accountId}/
20
- queue/ pending .ltr files
21
- sent/ sent messages (YYYY/MM/DD/) — optional
22
- fail/ failed messages for retry
23
- ```
24
-
25
- ## Library Usage
26
-
27
- ```typescript
28
- import { initMailSend, queueMessage } from "@bobfrankston/mailsend";
29
-
30
- // Initialize once at startup
31
- initMailSend({
32
- baseDir: "./mailqueue",
33
- defaultAccount: "work",
34
- logSent: true,
35
- retryIntervalMs: 300000, // 5 min
36
- accounts: {
37
- work: {
38
- host: "smtp.example.com",
39
- port: 587,
40
- auth: "password",
41
- user: "bob",
42
- password: "secret",
43
- },
44
- gmail: {
45
- host: "smtp.gmail.com",
46
- port: 587,
47
- auth: "oauth2",
48
- user: "bob@gmail.com",
49
- tokenProvider: async () => getOAuthToken(), // your OAuth2 token function
50
- },
51
- },
52
- });
53
-
54
- // Queue a message — returns immediately, sent in background
55
- queueMessage({
56
- from: "Bob <bob@example.com>",
57
- to: ["alice@example.com"],
58
- subject: "Hello",
59
- text: "Plain text body",
60
- // accountId: "work", // optional, uses defaultAccount
61
- });
62
-
63
- // Queue HTML email on a specific account
64
- queueMessage({
65
- from: "Bob <bob@gmail.com>",
66
- to: ["alice@example.com"],
67
- subject: "Hello",
68
- html: "<p>Rich <b>HTML</b> body</p>",
69
- accountId: "gmail",
70
- });
71
- ```
72
-
73
- ## CLI Usage
74
-
75
- Create a `mailsend.json` config file:
76
-
77
- ```json
78
- {
79
- "baseDir": "./mailqueue",
80
- "defaultAccount": "work",
81
- "logSent": true,
82
- "accounts": {
83
- "work": {
84
- "host": "smtp.example.com",
85
- "port": 587,
86
- "auth": "password",
87
- "user": "bob",
88
- "password": "secret"
89
- }
90
- }
91
- }
92
- ```
93
-
94
- Then:
95
-
96
- ```bash
97
- # Send a message (queued to default account)
98
- mailsend --to alice@example.com --subject "Hello" --body "Message"
99
-
100
- # Send via a specific account queue
101
- mailsend -q gmail --to alice@example.com --subject "Hello" --body "Message"
102
-
103
- # Check queue status
104
- mailsend --status
105
-
106
- # Process queues now (retry failed, send pending)
107
- mailsend --process
108
-
109
- # Use a different config file
110
- mailsend --config /path/to/mailsend.json --to ...
111
- ```
112
-
113
- ## Queue Format
114
-
115
- Messages are stored as `.ltr` files — standard RFC 822 format with additional `X-MailSend-Account` header for routing.
116
-
117
- Filenames: `YYYYMMDD_HHMMSS-NNNN.ltr`
118
-
119
- ## Retry
120
-
121
- Failed messages move to `{accountId}/fail/YYYY/MM/DD/`. The background worker retries them at the configured interval (default 5 minutes) by moving them back to the queue.
122
-
123
- ## OAuth2 (Gmail)
124
-
125
- For Gmail SMTP, set `auth: "oauth2"` and provide a `tokenProvider` function that returns a current access token. This integrates with `@bobfrankston/oauthsupport` or any OAuth2 flow.
126
-
127
- ## Sent Log
128
-
129
- When `logSent: true`, sent messages are preserved in `{accountId}/sent/YYYY/MM/DD/` for debugging. When false, sent messages are deleted.
130
-
131
- ## License
132
-
133
- MIT
@@ -1,12 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * mailsend CLI — queue and send email from the command line.
4
- *
5
- * Usage:
6
- * mailsend --to user@example.com --subject "Hello" --body "Message text"
7
- * mailsend -q gmail --to user@example.com --subject "Test"
8
- * mailsend --status
9
- * mailsend --process
10
- */
11
- export {};
12
- //# sourceMappingURL=cli.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["cli.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG"}
@@ -1,70 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * mailsend CLI — queue and send email from the command line.
4
- *
5
- * Usage:
6
- * mailsend --to user@example.com --subject "Hello" --body "Message text"
7
- * mailsend -q gmail --to user@example.com --subject "Test"
8
- * mailsend --status
9
- * mailsend --process
10
- */
11
- import * as fs from "node:fs";
12
- import { initMailSend, queueMessage } from "./index.js";
13
- const args = process.argv.slice(2);
14
- function getArg(name) {
15
- const short = { q: "queue" };
16
- const i = Math.max(args.indexOf(`--${name}`), args.indexOf(`-${Object.entries(short).find(([, v]) => v === name)?.[0] || ""}`));
17
- if (i === -1)
18
- return undefined;
19
- return args[i + 1];
20
- }
21
- function hasFlag(name) {
22
- return args.includes(`--${name}`);
23
- }
24
- // Load config
25
- const configPath = getArg("config") || "mailsend.json";
26
- if (!fs.existsSync(configPath)) {
27
- console.error(`Config file not found: ${configPath}`);
28
- console.error(`Create a mailsend.json with baseDir, accounts, etc.`);
29
- process.exit(1);
30
- }
31
- const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
32
- const sender = initMailSend(config);
33
- if (hasFlag("status")) {
34
- const status = sender.getStatus();
35
- for (const [id, s] of Object.entries(status)) {
36
- console.log(` ${id}: ${s.pending} pending, ${s.failed} failed`);
37
- }
38
- process.exit(0);
39
- }
40
- if (hasFlag("process")) {
41
- await sender.processAllQueues();
42
- process.exit(0);
43
- }
44
- const to = getArg("to");
45
- const subject = getArg("subject") || "(no subject)";
46
- const body = getArg("body") || "";
47
- const from = getArg("from") || "";
48
- const accountId = getArg("queue") || config.defaultAccount || "";
49
- if (!to) {
50
- console.error("Usage: mailsend --to addr [--subject text] [--body text] [--from addr] [-q account]");
51
- console.error(" mailsend --status");
52
- console.error(" mailsend --process");
53
- process.exit(1);
54
- }
55
- const acct = config.accounts[accountId];
56
- if (!acct) {
57
- console.error(`Unknown account: ${accountId}`);
58
- console.error(`Available: ${Object.keys(config.accounts).join(", ")}`);
59
- process.exit(1);
60
- }
61
- const filename = queueMessage({
62
- from: from || acct.from || acct.user,
63
- to: to.split(",").map(s => s.trim()),
64
- subject,
65
- text: body,
66
- accountId,
67
- });
68
- console.log(`Queued: ${filename}`);
69
- setTimeout(() => process.exit(0), 2000);
70
- //# sourceMappingURL=cli.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["cli.ts"],"names":[],"mappings":"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"}
@@ -1,81 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * mailsend CLI — queue and send email from the command line.
4
- *
5
- * Usage:
6
- * mailsend --to user@example.com --subject "Hello" --body "Message text"
7
- * mailsend -q gmail --to user@example.com --subject "Test"
8
- * mailsend --status
9
- * mailsend --process
10
- */
11
-
12
- import * as fs from "node:fs";
13
- import { initMailSend, queueMessage, getMailSender, type MailSendConfig } from "./index.js";
14
-
15
- const args = process.argv.slice(2);
16
-
17
- function getArg(name: string): string | undefined {
18
- const short: Record<string, string> = { q: "queue" };
19
- const i = Math.max(args.indexOf(`--${name}`), args.indexOf(`-${Object.entries(short).find(([, v]) => v === name)?.[0] || ""}`));
20
- if (i === -1) return undefined;
21
- return args[i + 1];
22
- }
23
-
24
- function hasFlag(name: string): boolean {
25
- return args.includes(`--${name}`);
26
- }
27
-
28
- // Load config
29
- const configPath = getArg("config") || "mailsend.json";
30
- if (!fs.existsSync(configPath)) {
31
- console.error(`Config file not found: ${configPath}`);
32
- console.error(`Create a mailsend.json with baseDir, accounts, etc.`);
33
- process.exit(1);
34
- }
35
-
36
- const config: MailSendConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
37
- const sender = initMailSend(config);
38
-
39
- if (hasFlag("status")) {
40
- const status = sender.getStatus();
41
- for (const [id, s] of Object.entries(status)) {
42
- console.log(` ${id}: ${s.pending} pending, ${s.failed} failed`);
43
- }
44
- process.exit(0);
45
- }
46
-
47
- if (hasFlag("process")) {
48
- await sender.processAllQueues();
49
- process.exit(0);
50
- }
51
-
52
- const to = getArg("to");
53
- const subject = getArg("subject") || "(no subject)";
54
- const body = getArg("body") || "";
55
- const from = getArg("from") || "";
56
- const accountId = getArg("queue") || config.defaultAccount || "";
57
-
58
- if (!to) {
59
- console.error("Usage: mailsend --to addr [--subject text] [--body text] [--from addr] [-q account]");
60
- console.error(" mailsend --status");
61
- console.error(" mailsend --process");
62
- process.exit(1);
63
- }
64
-
65
- const acct = config.accounts[accountId];
66
- if (!acct) {
67
- console.error(`Unknown account: ${accountId}`);
68
- console.error(`Available: ${Object.keys(config.accounts).join(", ")}`);
69
- process.exit(1);
70
- }
71
-
72
- const filename = queueMessage({
73
- from: from || acct.from || acct.user,
74
- to: to.split(",").map(s => s.trim()),
75
- subject,
76
- text: body,
77
- accountId,
78
- });
79
-
80
- console.log(`Queued: ${filename}`);
81
- setTimeout(() => process.exit(0), 2000);