@diskd-ai/email-mcp 0.3.0
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/LICENSE +155 -0
- package/README.md +820 -0
- package/dist/cli/account-commands.d.ts +10 -0
- package/dist/cli/account-commands.d.ts.map +1 -0
- package/dist/cli/account-commands.js +703 -0
- package/dist/cli/account-commands.js.map +1 -0
- package/dist/cli/config-commands.d.ts +9 -0
- package/dist/cli/config-commands.d.ts.map +1 -0
- package/dist/cli/config-commands.js +156 -0
- package/dist/cli/config-commands.js.map +1 -0
- package/dist/cli/guard.d.ts +11 -0
- package/dist/cli/guard.d.ts.map +1 -0
- package/dist/cli/guard.js +18 -0
- package/dist/cli/guard.js.map +1 -0
- package/dist/cli/install-commands.d.ts +12 -0
- package/dist/cli/install-commands.d.ts.map +1 -0
- package/dist/cli/install-commands.js +320 -0
- package/dist/cli/install-commands.js.map +1 -0
- package/dist/cli/notify.d.ts +8 -0
- package/dist/cli/notify.d.ts.map +1 -0
- package/dist/cli/notify.js +143 -0
- package/dist/cli/notify.js.map +1 -0
- package/dist/cli/providers.d.ts +13 -0
- package/dist/cli/providers.d.ts.map +1 -0
- package/dist/cli/providers.js +180 -0
- package/dist/cli/providers.js.map +1 -0
- package/dist/cli/scheduler.d.ts +8 -0
- package/dist/cli/scheduler.d.ts.map +1 -0
- package/dist/cli/scheduler.js +268 -0
- package/dist/cli/scheduler.js.map +1 -0
- package/dist/cli/setup.d.ts +12 -0
- package/dist/cli/setup.d.ts.map +1 -0
- package/dist/cli/setup.js +15 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli/test.d.ts +7 -0
- package/dist/cli/test.d.ts.map +1 -0
- package/dist/cli/test.js +67 -0
- package/dist/cli/test.js.map +1 -0
- package/dist/config/loader.d.ts +34 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +339 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +351 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +165 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/xdg.d.ts +27 -0
- package/dist/config/xdg.d.ts.map +1 -0
- package/dist/config/xdg.js +30 -0
- package/dist/config/xdg.js.map +1 -0
- package/dist/connections/manager.d.ts +42 -0
- package/dist/connections/manager.d.ts.map +1 -0
- package/dist/connections/manager.js +266 -0
- package/dist/connections/manager.js.map +1 -0
- package/dist/connections/types.d.ts +13 -0
- package/dist/connections/types.d.ts.map +1 -0
- package/dist/connections/types.js +2 -0
- package/dist/connections/types.js.map +1 -0
- package/dist/logging.d.ts +46 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +63 -0
- package/dist/logging.js.map +1 -0
- package/dist/main.d.ts +13 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +206 -0
- package/dist/main.js.map +1 -0
- package/dist/prompts/actions.prompt.d.ts +9 -0
- package/dist/prompts/actions.prompt.d.ts.map +1 -0
- package/dist/prompts/actions.prompt.js +64 -0
- package/dist/prompts/actions.prompt.js.map +1 -0
- package/dist/prompts/calendar.prompt.d.ts +9 -0
- package/dist/prompts/calendar.prompt.d.ts.map +1 -0
- package/dist/prompts/calendar.prompt.js +55 -0
- package/dist/prompts/calendar.prompt.js.map +1 -0
- package/dist/prompts/cleanup.prompt.d.ts +9 -0
- package/dist/prompts/cleanup.prompt.d.ts.map +1 -0
- package/dist/prompts/cleanup.prompt.js +78 -0
- package/dist/prompts/cleanup.prompt.js.map +1 -0
- package/dist/prompts/compose.prompt.d.ts +8 -0
- package/dist/prompts/compose.prompt.d.ts.map +1 -0
- package/dist/prompts/compose.prompt.js +116 -0
- package/dist/prompts/compose.prompt.js.map +1 -0
- package/dist/prompts/register.d.ts +8 -0
- package/dist/prompts/register.d.ts.map +1 -0
- package/dist/prompts/register.js +20 -0
- package/dist/prompts/register.js.map +1 -0
- package/dist/prompts/thread.prompt.d.ts +9 -0
- package/dist/prompts/thread.prompt.d.ts.map +1 -0
- package/dist/prompts/thread.prompt.js +58 -0
- package/dist/prompts/thread.prompt.js.map +1 -0
- package/dist/prompts/triage.prompt.d.ts +9 -0
- package/dist/prompts/triage.prompt.d.ts.map +1 -0
- package/dist/prompts/triage.prompt.js +64 -0
- package/dist/prompts/triage.prompt.js.map +1 -0
- package/dist/resources/accounts.resource.d.ts +9 -0
- package/dist/resources/accounts.resource.d.ts.map +1 -0
- package/dist/resources/accounts.resource.js +26 -0
- package/dist/resources/accounts.resource.js.map +1 -0
- package/dist/resources/mailboxes.resource.d.ts +10 -0
- package/dist/resources/mailboxes.resource.d.ts.map +1 -0
- package/dist/resources/mailboxes.resource.js +33 -0
- package/dist/resources/mailboxes.resource.js.map +1 -0
- package/dist/resources/register.d.ts +12 -0
- package/dist/resources/register.d.ts.map +1 -0
- package/dist/resources/register.js +20 -0
- package/dist/resources/register.js.map +1 -0
- package/dist/resources/scheduled.resource.d.ts +9 -0
- package/dist/resources/scheduled.resource.d.ts.map +1 -0
- package/dist/resources/scheduled.resource.js +31 -0
- package/dist/resources/scheduled.resource.js.map +1 -0
- package/dist/resources/stats.resource.d.ts +10 -0
- package/dist/resources/stats.resource.d.ts.map +1 -0
- package/dist/resources/stats.resource.js +45 -0
- package/dist/resources/stats.resource.js.map +1 -0
- package/dist/resources/templates.resource.d.ts +9 -0
- package/dist/resources/templates.resource.d.ts.map +1 -0
- package/dist/resources/templates.resource.js +34 -0
- package/dist/resources/templates.resource.js.map +1 -0
- package/dist/resources/unread.resource.d.ts +11 -0
- package/dist/resources/unread.resource.d.ts.map +1 -0
- package/dist/resources/unread.resource.js +46 -0
- package/dist/resources/unread.resource.js.map +1 -0
- package/dist/safety/audit.d.ts +17 -0
- package/dist/safety/audit.d.ts.map +1 -0
- package/dist/safety/audit.js +50 -0
- package/dist/safety/audit.js.map +1 -0
- package/dist/safety/rate-limiter.d.ts +22 -0
- package/dist/safety/rate-limiter.d.ts.map +1 -0
- package/dist/safety/rate-limiter.js +52 -0
- package/dist/safety/rate-limiter.js.map +1 -0
- package/dist/safety/validation.d.ts +44 -0
- package/dist/safety/validation.d.ts.map +1 -0
- package/dist/safety/validation.js +113 -0
- package/dist/safety/validation.js.map +1 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +25 -0
- package/dist/server.js.map +1 -0
- package/dist/services/calendar.service.d.ts +22 -0
- package/dist/services/calendar.service.d.ts.map +1 -0
- package/dist/services/calendar.service.js +115 -0
- package/dist/services/calendar.service.js.map +1 -0
- package/dist/services/event-bus.d.ts +28 -0
- package/dist/services/event-bus.d.ts.map +1 -0
- package/dist/services/event-bus.js +16 -0
- package/dist/services/event-bus.js.map +1 -0
- package/dist/services/hooks.service.d.ts +77 -0
- package/dist/services/hooks.service.d.ts.map +1 -0
- package/dist/services/hooks.service.js +498 -0
- package/dist/services/hooks.service.js.map +1 -0
- package/dist/services/imap.service.d.ts +133 -0
- package/dist/services/imap.service.d.ts.map +1 -0
- package/dist/services/imap.service.js +1393 -0
- package/dist/services/imap.service.js.map +1 -0
- package/dist/services/label-strategy.d.ts +20 -0
- package/dist/services/label-strategy.d.ts.map +1 -0
- package/dist/services/label-strategy.js +237 -0
- package/dist/services/label-strategy.js.map +1 -0
- package/dist/services/local-calendar.service.d.ts +126 -0
- package/dist/services/local-calendar.service.d.ts.map +1 -0
- package/dist/services/local-calendar.service.js +428 -0
- package/dist/services/local-calendar.service.js.map +1 -0
- package/dist/services/notifier.service.d.ts +69 -0
- package/dist/services/notifier.service.d.ts.map +1 -0
- package/dist/services/notifier.service.js +319 -0
- package/dist/services/notifier.service.js.map +1 -0
- package/dist/services/oauth.service.d.ts +47 -0
- package/dist/services/oauth.service.d.ts.map +1 -0
- package/dist/services/oauth.service.js +140 -0
- package/dist/services/oauth.service.js.map +1 -0
- package/dist/services/presets.d.ts +36 -0
- package/dist/services/presets.d.ts.map +1 -0
- package/dist/services/presets.js +173 -0
- package/dist/services/presets.js.map +1 -0
- package/dist/services/reminders.service.d.ts +63 -0
- package/dist/services/reminders.service.d.ts.map +1 -0
- package/dist/services/reminders.service.js +281 -0
- package/dist/services/reminders.service.js.map +1 -0
- package/dist/services/scheduler.service.d.ts +42 -0
- package/dist/services/scheduler.service.d.ts.map +1 -0
- package/dist/services/scheduler.service.js +260 -0
- package/dist/services/scheduler.service.js.map +1 -0
- package/dist/services/smtp.service.d.ts +40 -0
- package/dist/services/smtp.service.d.ts.map +1 -0
- package/dist/services/smtp.service.js +151 -0
- package/dist/services/smtp.service.js.map +1 -0
- package/dist/services/template.service.d.ts +33 -0
- package/dist/services/template.service.d.ts.map +1 -0
- package/dist/services/template.service.js +123 -0
- package/dist/services/template.service.js.map +1 -0
- package/dist/services/watcher.service.d.ts +36 -0
- package/dist/services/watcher.service.d.ts.map +1 -0
- package/dist/services/watcher.service.js +241 -0
- package/dist/services/watcher.service.js.map +1 -0
- package/dist/tools/accounts.tool.d.ts +7 -0
- package/dist/tools/accounts.tool.d.ts.map +1 -0
- package/dist/tools/accounts.tool.js +29 -0
- package/dist/tools/accounts.tool.js.map +1 -0
- package/dist/tools/analytics.tool.d.ts +9 -0
- package/dist/tools/analytics.tool.d.ts.map +1 -0
- package/dist/tools/analytics.tool.js +27 -0
- package/dist/tools/analytics.tool.js.map +1 -0
- package/dist/tools/attachments.tool.d.ts +7 -0
- package/dist/tools/attachments.tool.d.ts.map +1 -0
- package/dist/tools/attachments.tool.js +45 -0
- package/dist/tools/attachments.tool.js.map +1 -0
- package/dist/tools/bulk.tool.d.ts +7 -0
- package/dist/tools/bulk.tool.d.ts.map +1 -0
- package/dist/tools/bulk.tool.js +75 -0
- package/dist/tools/bulk.tool.js.map +1 -0
- package/dist/tools/calendar.tool.d.ts +19 -0
- package/dist/tools/calendar.tool.d.ts.map +1 -0
- package/dist/tools/calendar.tool.js +538 -0
- package/dist/tools/calendar.tool.js.map +1 -0
- package/dist/tools/contacts.tool.d.ts +7 -0
- package/dist/tools/contacts.tool.d.ts.map +1 -0
- package/dist/tools/contacts.tool.js +44 -0
- package/dist/tools/contacts.tool.js.map +1 -0
- package/dist/tools/drafts.tool.d.ts +8 -0
- package/dist/tools/drafts.tool.d.ts.map +1 -0
- package/dist/tools/drafts.tool.js +92 -0
- package/dist/tools/drafts.tool.js.map +1 -0
- package/dist/tools/emails.tool.d.ts +7 -0
- package/dist/tools/emails.tool.d.ts.map +1 -0
- package/dist/tools/emails.tool.js +400 -0
- package/dist/tools/emails.tool.js.map +1 -0
- package/dist/tools/folders.tool.d.ts +7 -0
- package/dist/tools/folders.tool.d.ts.map +1 -0
- package/dist/tools/folders.tool.js +111 -0
- package/dist/tools/folders.tool.js.map +1 -0
- package/dist/tools/health.tool.d.ts +10 -0
- package/dist/tools/health.tool.d.ts.map +1 -0
- package/dist/tools/health.tool.js +78 -0
- package/dist/tools/health.tool.js.map +1 -0
- package/dist/tools/label.tool.d.ts +11 -0
- package/dist/tools/label.tool.d.ts.map +1 -0
- package/dist/tools/label.tool.js +165 -0
- package/dist/tools/label.tool.js.map +1 -0
- package/dist/tools/locate.tool.d.ts +11 -0
- package/dist/tools/locate.tool.d.ts.map +1 -0
- package/dist/tools/locate.tool.js +59 -0
- package/dist/tools/locate.tool.js.map +1 -0
- package/dist/tools/mailboxes.tool.d.ts +7 -0
- package/dist/tools/mailboxes.tool.d.ts.map +1 -0
- package/dist/tools/mailboxes.tool.js +38 -0
- package/dist/tools/mailboxes.tool.js.map +1 -0
- package/dist/tools/manage.tool.d.ts +7 -0
- package/dist/tools/manage.tool.d.ts.map +1 -0
- package/dist/tools/manage.tool.js +125 -0
- package/dist/tools/manage.tool.js.map +1 -0
- package/dist/tools/register.d.ts +20 -0
- package/dist/tools/register.d.ts.map +1 -0
- package/dist/tools/register.js +53 -0
- package/dist/tools/register.js.map +1 -0
- package/dist/tools/scheduler.tool.d.ts +9 -0
- package/dist/tools/scheduler.tool.d.ts.map +1 -0
- package/dist/tools/scheduler.tool.js +104 -0
- package/dist/tools/scheduler.tool.js.map +1 -0
- package/dist/tools/send.tool.d.ts +7 -0
- package/dist/tools/send.tool.d.ts.map +1 -0
- package/dist/tools/send.tool.js +123 -0
- package/dist/tools/send.tool.js.map +1 -0
- package/dist/tools/templates.tool.d.ts +12 -0
- package/dist/tools/templates.tool.d.ts.map +1 -0
- package/dist/tools/templates.tool.js +140 -0
- package/dist/tools/templates.tool.js.map +1 -0
- package/dist/tools/thread.tool.d.ts +10 -0
- package/dist/tools/thread.tool.d.ts.map +1 -0
- package/dist/tools/thread.tool.js +146 -0
- package/dist/tools/thread.tool.js.map +1 -0
- package/dist/tools/watcher.tool.d.ts +9 -0
- package/dist/tools/watcher.tool.d.ts.map +1 -0
- package/dist/tools/watcher.tool.js +282 -0
- package/dist/tools/watcher.tool.js.map +1 -0
- package/dist/types/index.d.ts +271 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/calendar-notes.d.ts +31 -0
- package/dist/utils/calendar-notes.d.ts.map +1 -0
- package/dist/utils/calendar-notes.js +99 -0
- package/dist/utils/calendar-notes.js.map +1 -0
- package/dist/utils/calendar-state.d.ts +27 -0
- package/dist/utils/calendar-state.d.ts.map +1 -0
- package/dist/utils/calendar-state.js +85 -0
- package/dist/utils/calendar-state.js.map +1 -0
- package/dist/utils/conference-details.d.ts +12 -0
- package/dist/utils/conference-details.d.ts.map +1 -0
- package/dist/utils/conference-details.js +71 -0
- package/dist/utils/conference-details.js.map +1 -0
- package/dist/utils/meeting-url.d.ts +10 -0
- package/dist/utils/meeting-url.d.ts.map +1 -0
- package/dist/utils/meeting-url.js +30 -0
- package/dist/utils/meeting-url.js.map +1 -0
- package/package.json +108 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler service — JSON file-based email scheduling queue.
|
|
3
|
+
*
|
|
4
|
+
* Manages scheduled emails with a local file queue.
|
|
5
|
+
* Source of truth is the JSON files in XDG state directory.
|
|
6
|
+
*/
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { SCHEDULED_DIR, SCHEDULED_SENT_DIR } from '../config/xdg.js';
|
|
11
|
+
/** Max age (ms) for "sending" status before resetting to "pending" */
|
|
12
|
+
const STALE_LOCK_MS = 5 * 60 * 1000;
|
|
13
|
+
/** Max retry attempts before marking as "failed" */
|
|
14
|
+
const MAX_ATTEMPTS = 3;
|
|
15
|
+
export default class SchedulerService {
|
|
16
|
+
smtpService;
|
|
17
|
+
imapService;
|
|
18
|
+
constructor(smtpService, imapService) {
|
|
19
|
+
this.smtpService = smtpService;
|
|
20
|
+
this.imapService = imapService;
|
|
21
|
+
}
|
|
22
|
+
// -------------------------------------------------------------------------
|
|
23
|
+
// Schedule a new email
|
|
24
|
+
// -------------------------------------------------------------------------
|
|
25
|
+
async schedule(account, options) {
|
|
26
|
+
const sendAtDate = new Date(options.sendAt);
|
|
27
|
+
if (Number.isNaN(sendAtDate.getTime())) {
|
|
28
|
+
throw new Error(`Invalid send_at date: ${options.sendAt}`);
|
|
29
|
+
}
|
|
30
|
+
if (sendAtDate.getTime() <= Date.now()) {
|
|
31
|
+
throw new Error('send_at must be in the future');
|
|
32
|
+
}
|
|
33
|
+
const scheduled = {
|
|
34
|
+
id: crypto.randomUUID(),
|
|
35
|
+
account,
|
|
36
|
+
to: options.to,
|
|
37
|
+
cc: options.cc,
|
|
38
|
+
bcc: options.bcc,
|
|
39
|
+
subject: options.subject,
|
|
40
|
+
body: options.body,
|
|
41
|
+
html: options.html ?? false,
|
|
42
|
+
sendAt: sendAtDate.toISOString(),
|
|
43
|
+
createdAt: new Date().toISOString(),
|
|
44
|
+
status: 'pending',
|
|
45
|
+
attempts: 0,
|
|
46
|
+
inReplyTo: options.inReplyTo,
|
|
47
|
+
references: options.references,
|
|
48
|
+
};
|
|
49
|
+
// Save IMAP draft (best-effort)
|
|
50
|
+
try {
|
|
51
|
+
const draftResult = await this.imapService.saveDraft(account, {
|
|
52
|
+
to: options.to,
|
|
53
|
+
subject: `[Scheduled: ${sendAtDate.toLocaleString()}] ${options.subject}`,
|
|
54
|
+
body: options.body,
|
|
55
|
+
cc: options.cc,
|
|
56
|
+
html: options.html,
|
|
57
|
+
});
|
|
58
|
+
scheduled.draftMessageId = String(draftResult.id);
|
|
59
|
+
scheduled.draftMailbox = draftResult.mailbox;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Draft mirror is best-effort
|
|
63
|
+
}
|
|
64
|
+
await SchedulerService.writeScheduledFile(scheduled);
|
|
65
|
+
return scheduled;
|
|
66
|
+
}
|
|
67
|
+
// -------------------------------------------------------------------------
|
|
68
|
+
// List scheduled emails
|
|
69
|
+
// -------------------------------------------------------------------------
|
|
70
|
+
// eslint-disable-next-line class-methods-use-this
|
|
71
|
+
async list(options = {}) {
|
|
72
|
+
const status = options.status ?? 'pending';
|
|
73
|
+
const emails = [];
|
|
74
|
+
// Read pending/sending/failed from main dir
|
|
75
|
+
if (status !== 'sent') {
|
|
76
|
+
const pending = await SchedulerService.readDir(SCHEDULED_DIR);
|
|
77
|
+
emails.push(...pending);
|
|
78
|
+
}
|
|
79
|
+
// Read sent from sent/ subdir
|
|
80
|
+
if (status === 'sent' || status === 'all') {
|
|
81
|
+
const sent = await SchedulerService.readDir(SCHEDULED_SENT_DIR);
|
|
82
|
+
emails.push(...sent);
|
|
83
|
+
}
|
|
84
|
+
// Filter by account if specified
|
|
85
|
+
const filtered = options.account ? emails.filter((e) => e.account === options.account) : emails;
|
|
86
|
+
// Filter by status unless "all"
|
|
87
|
+
if (status !== 'all') {
|
|
88
|
+
return filtered.filter((e) => e.status === status);
|
|
89
|
+
}
|
|
90
|
+
return filtered.sort((a, b) => new Date(a.sendAt).getTime() - new Date(b.sendAt).getTime());
|
|
91
|
+
}
|
|
92
|
+
// -------------------------------------------------------------------------
|
|
93
|
+
// Cancel a scheduled email
|
|
94
|
+
// -------------------------------------------------------------------------
|
|
95
|
+
async cancel(scheduleId) {
|
|
96
|
+
const filePath = path.join(SCHEDULED_DIR, `${scheduleId}.json`);
|
|
97
|
+
let draftDeleted = false;
|
|
98
|
+
try {
|
|
99
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
100
|
+
const scheduled = JSON.parse(content);
|
|
101
|
+
if (scheduled.status !== 'pending') {
|
|
102
|
+
throw new Error(`Cannot cancel email with status "${scheduled.status}"`);
|
|
103
|
+
}
|
|
104
|
+
// Delete IMAP draft (best-effort)
|
|
105
|
+
if (scheduled.draftMessageId && scheduled.draftMailbox) {
|
|
106
|
+
try {
|
|
107
|
+
await this.imapService.deleteEmail(scheduled.account, scheduled.draftMessageId, scheduled.draftMailbox);
|
|
108
|
+
draftDeleted = true;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Draft deletion is best-effort
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
await fs.unlink(filePath);
|
|
115
|
+
return { cancelled: true, draftDeleted };
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
if (err instanceof Error &&
|
|
119
|
+
'code' in err &&
|
|
120
|
+
err.code === 'ENOENT') {
|
|
121
|
+
throw new Error(`Scheduled email "${scheduleId}" not found`);
|
|
122
|
+
}
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// -------------------------------------------------------------------------
|
|
127
|
+
// Check and send overdue emails
|
|
128
|
+
// -------------------------------------------------------------------------
|
|
129
|
+
/* eslint-disable no-await-in-loop, no-continue -- Sequential file processing required */
|
|
130
|
+
async checkAndSend() {
|
|
131
|
+
const result = { sent: 0, failed: 0, errors: [] };
|
|
132
|
+
await SchedulerService.ensureDirs();
|
|
133
|
+
let files;
|
|
134
|
+
try {
|
|
135
|
+
files = await fs.readdir(SCHEDULED_DIR);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
const jsonFiles = files.filter((f) => f.endsWith('.json'));
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
// Process files sequentially — must not double-send
|
|
143
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
144
|
+
for (const file of jsonFiles) {
|
|
145
|
+
const filePath = path.join(SCHEDULED_DIR, file);
|
|
146
|
+
try {
|
|
147
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
148
|
+
const scheduled = JSON.parse(content);
|
|
149
|
+
// Reset stale locks
|
|
150
|
+
if (scheduled.status === 'sending' && scheduled.lastError !== undefined) {
|
|
151
|
+
const lockAge = now - new Date(scheduled.createdAt).getTime();
|
|
152
|
+
if (lockAge > STALE_LOCK_MS) {
|
|
153
|
+
scheduled.status = 'pending';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else if (scheduled.status === 'sending') {
|
|
157
|
+
// Check if it's been sending too long (use sendAt as reference)
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
// Skip non-pending
|
|
161
|
+
if (scheduled.status !== 'pending')
|
|
162
|
+
continue;
|
|
163
|
+
// Skip if not yet due
|
|
164
|
+
if (new Date(scheduled.sendAt).getTime() > now)
|
|
165
|
+
continue;
|
|
166
|
+
// Skip if max attempts exceeded
|
|
167
|
+
if (scheduled.attempts >= MAX_ATTEMPTS) {
|
|
168
|
+
scheduled.status = 'failed';
|
|
169
|
+
scheduled.lastError = 'Max retry attempts exceeded';
|
|
170
|
+
await SchedulerService.writeScheduledFile(scheduled);
|
|
171
|
+
result.failed += 1;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
// Acquire lock
|
|
175
|
+
scheduled.status = 'sending';
|
|
176
|
+
scheduled.attempts += 1;
|
|
177
|
+
await SchedulerService.writeScheduledFile(scheduled);
|
|
178
|
+
// Send
|
|
179
|
+
const sendResult = await this.smtpService.sendEmail(scheduled.account, {
|
|
180
|
+
to: scheduled.to,
|
|
181
|
+
subject: scheduled.subject,
|
|
182
|
+
body: scheduled.body,
|
|
183
|
+
cc: scheduled.cc,
|
|
184
|
+
bcc: scheduled.bcc,
|
|
185
|
+
html: scheduled.html,
|
|
186
|
+
});
|
|
187
|
+
// Mark as sent and move to sent dir
|
|
188
|
+
scheduled.status = 'sent';
|
|
189
|
+
scheduled.sentAt = new Date().toISOString();
|
|
190
|
+
scheduled.sentMessageId = sendResult.messageId;
|
|
191
|
+
const sentPath = path.join(SCHEDULED_SENT_DIR, file);
|
|
192
|
+
await fs.writeFile(sentPath, JSON.stringify(scheduled, null, 2));
|
|
193
|
+
await fs.unlink(filePath);
|
|
194
|
+
// Delete draft (best-effort)
|
|
195
|
+
if (scheduled.draftMessageId && scheduled.draftMailbox) {
|
|
196
|
+
try {
|
|
197
|
+
await this.imapService.deleteEmail(scheduled.account, scheduled.draftMessageId, scheduled.draftMailbox);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// Best-effort
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
result.sent += 1;
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
207
|
+
result.errors.push(`${file}: ${errorMsg}`);
|
|
208
|
+
// Mark as failed in the file
|
|
209
|
+
try {
|
|
210
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
211
|
+
const scheduled = JSON.parse(content);
|
|
212
|
+
scheduled.status = scheduled.attempts >= MAX_ATTEMPTS ? 'failed' : 'pending';
|
|
213
|
+
scheduled.lastError = errorMsg;
|
|
214
|
+
await fs.writeFile(filePath, JSON.stringify(scheduled, null, 2));
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// If we can't even update the file, skip
|
|
218
|
+
}
|
|
219
|
+
result.failed += 1;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
/* eslint-enable no-await-in-loop, no-continue */
|
|
225
|
+
// -------------------------------------------------------------------------
|
|
226
|
+
// Private helpers
|
|
227
|
+
// -------------------------------------------------------------------------
|
|
228
|
+
static async ensureDirs() {
|
|
229
|
+
await fs.mkdir(SCHEDULED_DIR, { recursive: true });
|
|
230
|
+
await fs.mkdir(SCHEDULED_SENT_DIR, { recursive: true });
|
|
231
|
+
}
|
|
232
|
+
static async writeScheduledFile(scheduled) {
|
|
233
|
+
await SchedulerService.ensureDirs();
|
|
234
|
+
const filePath = path.join(SCHEDULED_DIR, `${scheduled.id}.json`);
|
|
235
|
+
await fs.writeFile(filePath, JSON.stringify(scheduled, null, 2));
|
|
236
|
+
}
|
|
237
|
+
static async readDir(dirPath) {
|
|
238
|
+
const emails = [];
|
|
239
|
+
try {
|
|
240
|
+
const files = await fs.readdir(dirPath);
|
|
241
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
242
|
+
for (const file of files) {
|
|
243
|
+
if (!file.endsWith('.json'))
|
|
244
|
+
continue; // eslint-disable-line no-continue
|
|
245
|
+
try {
|
|
246
|
+
const content = await fs.readFile(path.join(dirPath, file), 'utf-8'); // eslint-disable-line no-await-in-loop
|
|
247
|
+
emails.push(JSON.parse(content));
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// Skip corrupted files
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// Directory may not exist yet
|
|
256
|
+
}
|
|
257
|
+
return emails;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=scheduler.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.service.js","sourceRoot":"","sources":["../../src/services/scheduler.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMTP service — pure business logic for email send operations.
|
|
3
|
+
*
|
|
4
|
+
* No MCP dependency — fully unit-testable.
|
|
5
|
+
*/
|
|
6
|
+
import type { IConnectionManager } from '../connections/types.js';
|
|
7
|
+
import type RateLimiter from '../safety/rate-limiter.js';
|
|
8
|
+
import type { SendResult } from '../types/index.js';
|
|
9
|
+
import type ImapService from './imap.service.js';
|
|
10
|
+
export default class SmtpService {
|
|
11
|
+
private connections;
|
|
12
|
+
private rateLimiter;
|
|
13
|
+
private imapService;
|
|
14
|
+
constructor(connections: IConnectionManager, rateLimiter: RateLimiter, imapService: ImapService);
|
|
15
|
+
sendEmail(accountName: string, options: {
|
|
16
|
+
to: string[];
|
|
17
|
+
subject: string;
|
|
18
|
+
body: string;
|
|
19
|
+
cc?: string[];
|
|
20
|
+
bcc?: string[];
|
|
21
|
+
html?: boolean;
|
|
22
|
+
}): Promise<SendResult>;
|
|
23
|
+
replyToEmail(accountName: string, options: {
|
|
24
|
+
emailId: string;
|
|
25
|
+
mailbox?: string;
|
|
26
|
+
body: string;
|
|
27
|
+
replyAll?: boolean;
|
|
28
|
+
html?: boolean;
|
|
29
|
+
}): Promise<SendResult>;
|
|
30
|
+
forwardEmail(accountName: string, options: {
|
|
31
|
+
emailId: string;
|
|
32
|
+
mailbox?: string;
|
|
33
|
+
to: string[];
|
|
34
|
+
body?: string;
|
|
35
|
+
cc?: string[];
|
|
36
|
+
}): Promise<SendResult>;
|
|
37
|
+
private checkRateLimit;
|
|
38
|
+
sendDraft(accountName: string, draftId: number, mailbox?: string): Promise<SendResult>;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=smtp.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smtp.service.d.ts","sourceRoot":"","sources":["../../src/services/smtp.service.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,KAAK,WAAW,MAAM,2BAA2B,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,WAAW,MAAM,mBAAmB,CAAC;AAEjD,MAAM,CAAC,OAAO,OAAO,WAAW;IAE5B,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,WAAW;gBAFX,WAAW,EAAE,kBAAkB,EAC/B,WAAW,EAAE,WAAW,EACxB,WAAW,EAAE,WAAW;IAO5B,SAAS,CACb,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;QACP,EAAE,EAAE,MAAM,EAAE,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;QACd,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;QACf,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,GACA,OAAO,CAAC,UAAU,CAAC;IAyBhB,YAAY,CAChB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,GACA,OAAO,CAAC,UAAU,CAAC;IAsDhB,YAAY,CAChB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,EAAE,EAAE,MAAM,EAAE,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;KACf,GACA,OAAO,CAAC,UAAU,CAAC;IA4CtB,OAAO,CAAC,cAAc;IAahB,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;CAkC7F"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMTP service — pure business logic for email send operations.
|
|
3
|
+
*
|
|
4
|
+
* No MCP dependency — fully unit-testable.
|
|
5
|
+
*/
|
|
6
|
+
export default class SmtpService {
|
|
7
|
+
connections;
|
|
8
|
+
rateLimiter;
|
|
9
|
+
imapService;
|
|
10
|
+
constructor(connections, rateLimiter, imapService) {
|
|
11
|
+
this.connections = connections;
|
|
12
|
+
this.rateLimiter = rateLimiter;
|
|
13
|
+
this.imapService = imapService;
|
|
14
|
+
}
|
|
15
|
+
// -------------------------------------------------------------------------
|
|
16
|
+
// Send email
|
|
17
|
+
// -------------------------------------------------------------------------
|
|
18
|
+
async sendEmail(accountName, options) {
|
|
19
|
+
this.checkRateLimit(accountName);
|
|
20
|
+
const account = this.connections.getAccount(accountName);
|
|
21
|
+
const transport = await this.connections.getSmtpTransport(accountName);
|
|
22
|
+
const result = await transport.sendMail({
|
|
23
|
+
from: account.fullName ? `"${account.fullName}" <${account.email}>` : account.email,
|
|
24
|
+
to: options.to.join(', '),
|
|
25
|
+
cc: options.cc?.join(', '),
|
|
26
|
+
bcc: options.bcc?.join(', '),
|
|
27
|
+
subject: options.subject,
|
|
28
|
+
...(options.html ? { html: options.body } : { text: options.body }),
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
messageId: result.messageId ?? '',
|
|
32
|
+
status: 'sent',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// -------------------------------------------------------------------------
|
|
36
|
+
// Reply
|
|
37
|
+
// -------------------------------------------------------------------------
|
|
38
|
+
async replyToEmail(accountName, options) {
|
|
39
|
+
this.checkRateLimit(accountName);
|
|
40
|
+
const account = this.connections.getAccount(accountName);
|
|
41
|
+
const original = await this.imapService.getEmail(accountName, options.emailId, options.mailbox);
|
|
42
|
+
// Build recipient list
|
|
43
|
+
const to = [original.from.address];
|
|
44
|
+
const cc = [];
|
|
45
|
+
if (options.replyAll) {
|
|
46
|
+
// Add all original To recipients except ourselves
|
|
47
|
+
original.to
|
|
48
|
+
.filter((addr) => addr.address !== account.email)
|
|
49
|
+
.forEach((addr) => {
|
|
50
|
+
to.push(addr.address);
|
|
51
|
+
});
|
|
52
|
+
// Add CC recipients except ourselves
|
|
53
|
+
(original.cc ?? [])
|
|
54
|
+
.filter((addr) => addr.address !== account.email)
|
|
55
|
+
.forEach((addr) => {
|
|
56
|
+
cc.push(addr.address);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// Build threading headers
|
|
60
|
+
const references = [...(original.references ?? []), original.messageId].filter(Boolean);
|
|
61
|
+
const subject = original.subject.startsWith('Re:')
|
|
62
|
+
? original.subject
|
|
63
|
+
: `Re: ${original.subject}`;
|
|
64
|
+
const transport = await this.connections.getSmtpTransport(accountName);
|
|
65
|
+
const result = await transport.sendMail({
|
|
66
|
+
from: account.fullName ? `"${account.fullName}" <${account.email}>` : account.email,
|
|
67
|
+
to: to.join(', '),
|
|
68
|
+
cc: cc.length > 0 ? cc.join(', ') : undefined,
|
|
69
|
+
subject,
|
|
70
|
+
inReplyTo: original.messageId,
|
|
71
|
+
references: references.join(' '),
|
|
72
|
+
...(options.html ? { html: options.body } : { text: options.body }),
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
messageId: result.messageId ?? '',
|
|
76
|
+
status: 'sent',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// -------------------------------------------------------------------------
|
|
80
|
+
// Forward
|
|
81
|
+
// -------------------------------------------------------------------------
|
|
82
|
+
async forwardEmail(accountName, options) {
|
|
83
|
+
this.checkRateLimit(accountName);
|
|
84
|
+
const account = this.connections.getAccount(accountName);
|
|
85
|
+
const original = await this.imapService.getEmail(accountName, options.emailId, options.mailbox);
|
|
86
|
+
const subject = original.subject.startsWith('Fwd:')
|
|
87
|
+
? original.subject
|
|
88
|
+
: `Fwd: ${original.subject}`;
|
|
89
|
+
// Build forwarded message body
|
|
90
|
+
const forwardHeader = [
|
|
91
|
+
'',
|
|
92
|
+
'---------- Forwarded message ----------',
|
|
93
|
+
`From: ${original.from.name ? `${original.from.name} <${original.from.address}>` : original.from.address}`,
|
|
94
|
+
`Date: ${original.date}`,
|
|
95
|
+
`Subject: ${original.subject}`,
|
|
96
|
+
`To: ${original.to.map((a) => a.address).join(', ')}`,
|
|
97
|
+
'',
|
|
98
|
+
].join('\n');
|
|
99
|
+
const originalBody = original.bodyText ?? original.bodyHtml ?? '';
|
|
100
|
+
const fullBody = (options.body ?? '') + forwardHeader + originalBody;
|
|
101
|
+
const transport = await this.connections.getSmtpTransport(accountName);
|
|
102
|
+
const result = await transport.sendMail({
|
|
103
|
+
from: account.fullName ? `"${account.fullName}" <${account.email}>` : account.email,
|
|
104
|
+
to: options.to.join(', '),
|
|
105
|
+
cc: options.cc?.join(', '),
|
|
106
|
+
subject,
|
|
107
|
+
text: fullBody,
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
messageId: result.messageId ?? '',
|
|
111
|
+
status: 'sent',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// -------------------------------------------------------------------------
|
|
115
|
+
// Rate limit check
|
|
116
|
+
// -------------------------------------------------------------------------
|
|
117
|
+
checkRateLimit(accountName) {
|
|
118
|
+
if (!this.rateLimiter.tryConsume(accountName)) {
|
|
119
|
+
throw new Error(`Rate limit exceeded for account "${accountName}". ` +
|
|
120
|
+
`Please wait before sending more emails.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// -------------------------------------------------------------------------
|
|
124
|
+
// Send draft
|
|
125
|
+
// -------------------------------------------------------------------------
|
|
126
|
+
async sendDraft(accountName, draftId, mailbox) {
|
|
127
|
+
this.checkRateLimit(accountName);
|
|
128
|
+
// Fetch the draft via IMAP
|
|
129
|
+
const { email: draft, mailbox: draftsPath } = await this.imapService.fetchDraft(accountName, draftId, mailbox);
|
|
130
|
+
const account = this.connections.getAccount(accountName);
|
|
131
|
+
const transport = await this.connections.getSmtpTransport(accountName);
|
|
132
|
+
const to = draft.to.map((a) => a.address).join(', ');
|
|
133
|
+
const cc = draft.cc?.map((a) => a.address).join(', ');
|
|
134
|
+
const result = await transport.sendMail({
|
|
135
|
+
from: account.fullName ? `"${account.fullName}" <${account.email}>` : account.email,
|
|
136
|
+
to,
|
|
137
|
+
cc,
|
|
138
|
+
subject: draft.subject,
|
|
139
|
+
inReplyTo: draft.inReplyTo,
|
|
140
|
+
references: draft.references?.join(' '),
|
|
141
|
+
...(draft.bodyHtml ? { html: draft.bodyHtml } : { text: draft.bodyText ?? '' }),
|
|
142
|
+
});
|
|
143
|
+
// Delete the draft after successful send
|
|
144
|
+
await this.imapService.deleteDraft(accountName, draftId, draftsPath);
|
|
145
|
+
return {
|
|
146
|
+
messageId: result.messageId ?? '',
|
|
147
|
+
status: 'sent',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=smtp.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smtp.service.js","sourceRoot":"","sources":["../../src/services/smtp.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template service — load, parse, and apply user-defined email templates.
|
|
3
|
+
*
|
|
4
|
+
* Templates are stored as TOML files in the XDG config templates directory.
|
|
5
|
+
* Each template has a name, optional description, subject, body, and a list
|
|
6
|
+
* of variable names used for {{variable}} substitution.
|
|
7
|
+
*/
|
|
8
|
+
import type { EmailTemplate } from '../types/index.js';
|
|
9
|
+
export default class TemplateService {
|
|
10
|
+
private templatesDir;
|
|
11
|
+
constructor(templatesDir?: string);
|
|
12
|
+
/**
|
|
13
|
+
* List all templates from the templates directory.
|
|
14
|
+
* Returns metadata only (name, description, variables).
|
|
15
|
+
*/
|
|
16
|
+
listTemplates(): Promise<EmailTemplate[]>;
|
|
17
|
+
/**
|
|
18
|
+
* Get a single template by name.
|
|
19
|
+
*/
|
|
20
|
+
getTemplate(name: string): Promise<EmailTemplate>;
|
|
21
|
+
/**
|
|
22
|
+
* Apply variable substitution to a template.
|
|
23
|
+
* Returns the composed subject and body with variables replaced.
|
|
24
|
+
* Missing variables are left as {{variable}} placeholders.
|
|
25
|
+
*/
|
|
26
|
+
applyTemplate(name: string, variables: Record<string, string>, html?: boolean): Promise<{
|
|
27
|
+
subject: string;
|
|
28
|
+
body: string;
|
|
29
|
+
}>;
|
|
30
|
+
/** Get the templates directory path. */
|
|
31
|
+
get directory(): string;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=template.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template.service.d.ts","sourceRoot":"","sources":["../../src/services/template.service.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAUH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AA2CvD,MAAM,CAAC,OAAO,OAAO,eAAe;IAClC,OAAO,CAAC,YAAY,CAAS;gBAEjB,YAAY,SAAgB;IAIxC;;;OAGG;IACG,aAAa,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;IA2B/C;;OAEG;IACG,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAUvD;;;;OAIG;IACG,aAAa,CACjB,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACjC,IAAI,UAAQ,GACX,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAsB7C,wCAAwC;IACxC,IAAI,SAAS,IAAI,MAAM,CAEtB;CACF"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template service — load, parse, and apply user-defined email templates.
|
|
3
|
+
*
|
|
4
|
+
* Templates are stored as TOML files in the XDG config templates directory.
|
|
5
|
+
* Each template has a name, optional description, subject, body, and a list
|
|
6
|
+
* of variable names used for {{variable}} substitution.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { parse as parseTOML } from 'smol-toml';
|
|
11
|
+
import { TEMPLATES_DIR } from '../config/xdg.js';
|
|
12
|
+
import { sanitizeTemplateVariable } from '../safety/validation.js';
|
|
13
|
+
/**
|
|
14
|
+
* Validate that a parsed object looks like an EmailTemplate.
|
|
15
|
+
* Returns the template or throws with a descriptive message.
|
|
16
|
+
*/
|
|
17
|
+
function validateTemplate(raw, filename) {
|
|
18
|
+
const { name, description, subject, body, variables } = raw;
|
|
19
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
20
|
+
throw new Error(`Template "${filename}" is missing a valid "name" field`);
|
|
21
|
+
}
|
|
22
|
+
if (typeof subject !== 'string') {
|
|
23
|
+
throw new Error(`Template "${filename}" is missing a "subject" field`);
|
|
24
|
+
}
|
|
25
|
+
if (typeof body !== 'string') {
|
|
26
|
+
throw new Error(`Template "${filename}" is missing a "body" field`);
|
|
27
|
+
}
|
|
28
|
+
if (!Array.isArray(variables) || !variables.every((v) => typeof v === 'string')) {
|
|
29
|
+
throw new Error(`Template "${filename}" must have a "variables" array of strings`);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
name,
|
|
33
|
+
description: typeof description === 'string' ? description : undefined,
|
|
34
|
+
subject,
|
|
35
|
+
body,
|
|
36
|
+
variables,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Replace all {{variable}} placeholders in a string with the provided values.
|
|
41
|
+
*/
|
|
42
|
+
function substituteVariables(text, variables) {
|
|
43
|
+
return text.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
|
|
44
|
+
if (varName in variables) {
|
|
45
|
+
return variables[varName];
|
|
46
|
+
}
|
|
47
|
+
return match; // Leave unresolved placeholders as-is
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export default class TemplateService {
|
|
51
|
+
templatesDir;
|
|
52
|
+
constructor(templatesDir = TEMPLATES_DIR) {
|
|
53
|
+
this.templatesDir = templatesDir;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* List all templates from the templates directory.
|
|
57
|
+
* Returns metadata only (name, description, variables).
|
|
58
|
+
*/
|
|
59
|
+
async listTemplates() {
|
|
60
|
+
try {
|
|
61
|
+
await fs.access(this.templatesDir);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return []; // Directory doesn't exist yet — no templates
|
|
65
|
+
}
|
|
66
|
+
const entries = await fs.readdir(this.templatesDir);
|
|
67
|
+
const tomlFiles = entries.filter((f) => f.endsWith('.toml'));
|
|
68
|
+
const templates = [];
|
|
69
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
70
|
+
for (const file of tomlFiles) {
|
|
71
|
+
try {
|
|
72
|
+
const filePath = path.join(this.templatesDir, file);
|
|
73
|
+
// eslint-disable-next-line no-await-in-loop
|
|
74
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
75
|
+
const raw = parseTOML(content);
|
|
76
|
+
templates.push(validateTemplate(raw, file));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Skip invalid templates silently
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return templates;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get a single template by name.
|
|
86
|
+
*/
|
|
87
|
+
async getTemplate(name) {
|
|
88
|
+
const templates = await this.listTemplates();
|
|
89
|
+
const template = templates.find((t) => t.name === name);
|
|
90
|
+
if (!template) {
|
|
91
|
+
const available = templates.map((t) => t.name).join(', ') || 'none';
|
|
92
|
+
throw new Error(`Template "${name}" not found. Available: ${available}`);
|
|
93
|
+
}
|
|
94
|
+
return template;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Apply variable substitution to a template.
|
|
98
|
+
* Returns the composed subject and body with variables replaced.
|
|
99
|
+
* Missing variables are left as {{variable}} placeholders.
|
|
100
|
+
*/
|
|
101
|
+
async applyTemplate(name, variables, html = false) {
|
|
102
|
+
const template = await this.getTemplate(name);
|
|
103
|
+
const sanitized = Object.fromEntries(Object.entries(variables).map(([k, v]) => [k, sanitizeTemplateVariable(v, html)]));
|
|
104
|
+
// Warn about missing variables
|
|
105
|
+
const missing = template.variables.filter((v) => !(v in sanitized));
|
|
106
|
+
if (missing.length > 0) {
|
|
107
|
+
const composed = {
|
|
108
|
+
subject: substituteVariables(template.subject, sanitized),
|
|
109
|
+
body: substituteVariables(template.body, sanitized),
|
|
110
|
+
};
|
|
111
|
+
return composed;
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
subject: substituteVariables(template.subject, sanitized),
|
|
115
|
+
body: substituteVariables(template.body, sanitized),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Get the templates directory path. */
|
|
119
|
+
get directory() {
|
|
120
|
+
return this.templatesDir;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=template.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template.service.js","sourceRoot":"","sources":["../../src/services/template.service.ts"],"names":[],"mappings":"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"}
|