@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.
Files changed (295) hide show
  1. package/LICENSE +155 -0
  2. package/README.md +820 -0
  3. package/dist/cli/account-commands.d.ts +10 -0
  4. package/dist/cli/account-commands.d.ts.map +1 -0
  5. package/dist/cli/account-commands.js +703 -0
  6. package/dist/cli/account-commands.js.map +1 -0
  7. package/dist/cli/config-commands.d.ts +9 -0
  8. package/dist/cli/config-commands.d.ts.map +1 -0
  9. package/dist/cli/config-commands.js +156 -0
  10. package/dist/cli/config-commands.js.map +1 -0
  11. package/dist/cli/guard.d.ts +11 -0
  12. package/dist/cli/guard.d.ts.map +1 -0
  13. package/dist/cli/guard.js +18 -0
  14. package/dist/cli/guard.js.map +1 -0
  15. package/dist/cli/install-commands.d.ts +12 -0
  16. package/dist/cli/install-commands.d.ts.map +1 -0
  17. package/dist/cli/install-commands.js +320 -0
  18. package/dist/cli/install-commands.js.map +1 -0
  19. package/dist/cli/notify.d.ts +8 -0
  20. package/dist/cli/notify.d.ts.map +1 -0
  21. package/dist/cli/notify.js +143 -0
  22. package/dist/cli/notify.js.map +1 -0
  23. package/dist/cli/providers.d.ts +13 -0
  24. package/dist/cli/providers.d.ts.map +1 -0
  25. package/dist/cli/providers.js +180 -0
  26. package/dist/cli/providers.js.map +1 -0
  27. package/dist/cli/scheduler.d.ts +8 -0
  28. package/dist/cli/scheduler.d.ts.map +1 -0
  29. package/dist/cli/scheduler.js +268 -0
  30. package/dist/cli/scheduler.js.map +1 -0
  31. package/dist/cli/setup.d.ts +12 -0
  32. package/dist/cli/setup.d.ts.map +1 -0
  33. package/dist/cli/setup.js +15 -0
  34. package/dist/cli/setup.js.map +1 -0
  35. package/dist/cli/test.d.ts +7 -0
  36. package/dist/cli/test.d.ts.map +1 -0
  37. package/dist/cli/test.js +67 -0
  38. package/dist/cli/test.js.map +1 -0
  39. package/dist/config/loader.d.ts +34 -0
  40. package/dist/config/loader.d.ts.map +1 -0
  41. package/dist/config/loader.js +339 -0
  42. package/dist/config/loader.js.map +1 -0
  43. package/dist/config/schema.d.ts +351 -0
  44. package/dist/config/schema.d.ts.map +1 -0
  45. package/dist/config/schema.js +165 -0
  46. package/dist/config/schema.js.map +1 -0
  47. package/dist/config/xdg.d.ts +27 -0
  48. package/dist/config/xdg.d.ts.map +1 -0
  49. package/dist/config/xdg.js +30 -0
  50. package/dist/config/xdg.js.map +1 -0
  51. package/dist/connections/manager.d.ts +42 -0
  52. package/dist/connections/manager.d.ts.map +1 -0
  53. package/dist/connections/manager.js +266 -0
  54. package/dist/connections/manager.js.map +1 -0
  55. package/dist/connections/types.d.ts +13 -0
  56. package/dist/connections/types.d.ts.map +1 -0
  57. package/dist/connections/types.js +2 -0
  58. package/dist/connections/types.js.map +1 -0
  59. package/dist/logging.d.ts +46 -0
  60. package/dist/logging.d.ts.map +1 -0
  61. package/dist/logging.js +63 -0
  62. package/dist/logging.js.map +1 -0
  63. package/dist/main.d.ts +13 -0
  64. package/dist/main.d.ts.map +1 -0
  65. package/dist/main.js +206 -0
  66. package/dist/main.js.map +1 -0
  67. package/dist/prompts/actions.prompt.d.ts +9 -0
  68. package/dist/prompts/actions.prompt.d.ts.map +1 -0
  69. package/dist/prompts/actions.prompt.js +64 -0
  70. package/dist/prompts/actions.prompt.js.map +1 -0
  71. package/dist/prompts/calendar.prompt.d.ts +9 -0
  72. package/dist/prompts/calendar.prompt.d.ts.map +1 -0
  73. package/dist/prompts/calendar.prompt.js +55 -0
  74. package/dist/prompts/calendar.prompt.js.map +1 -0
  75. package/dist/prompts/cleanup.prompt.d.ts +9 -0
  76. package/dist/prompts/cleanup.prompt.d.ts.map +1 -0
  77. package/dist/prompts/cleanup.prompt.js +78 -0
  78. package/dist/prompts/cleanup.prompt.js.map +1 -0
  79. package/dist/prompts/compose.prompt.d.ts +8 -0
  80. package/dist/prompts/compose.prompt.d.ts.map +1 -0
  81. package/dist/prompts/compose.prompt.js +116 -0
  82. package/dist/prompts/compose.prompt.js.map +1 -0
  83. package/dist/prompts/register.d.ts +8 -0
  84. package/dist/prompts/register.d.ts.map +1 -0
  85. package/dist/prompts/register.js +20 -0
  86. package/dist/prompts/register.js.map +1 -0
  87. package/dist/prompts/thread.prompt.d.ts +9 -0
  88. package/dist/prompts/thread.prompt.d.ts.map +1 -0
  89. package/dist/prompts/thread.prompt.js +58 -0
  90. package/dist/prompts/thread.prompt.js.map +1 -0
  91. package/dist/prompts/triage.prompt.d.ts +9 -0
  92. package/dist/prompts/triage.prompt.d.ts.map +1 -0
  93. package/dist/prompts/triage.prompt.js +64 -0
  94. package/dist/prompts/triage.prompt.js.map +1 -0
  95. package/dist/resources/accounts.resource.d.ts +9 -0
  96. package/dist/resources/accounts.resource.d.ts.map +1 -0
  97. package/dist/resources/accounts.resource.js +26 -0
  98. package/dist/resources/accounts.resource.js.map +1 -0
  99. package/dist/resources/mailboxes.resource.d.ts +10 -0
  100. package/dist/resources/mailboxes.resource.d.ts.map +1 -0
  101. package/dist/resources/mailboxes.resource.js +33 -0
  102. package/dist/resources/mailboxes.resource.js.map +1 -0
  103. package/dist/resources/register.d.ts +12 -0
  104. package/dist/resources/register.d.ts.map +1 -0
  105. package/dist/resources/register.js +20 -0
  106. package/dist/resources/register.js.map +1 -0
  107. package/dist/resources/scheduled.resource.d.ts +9 -0
  108. package/dist/resources/scheduled.resource.d.ts.map +1 -0
  109. package/dist/resources/scheduled.resource.js +31 -0
  110. package/dist/resources/scheduled.resource.js.map +1 -0
  111. package/dist/resources/stats.resource.d.ts +10 -0
  112. package/dist/resources/stats.resource.d.ts.map +1 -0
  113. package/dist/resources/stats.resource.js +45 -0
  114. package/dist/resources/stats.resource.js.map +1 -0
  115. package/dist/resources/templates.resource.d.ts +9 -0
  116. package/dist/resources/templates.resource.d.ts.map +1 -0
  117. package/dist/resources/templates.resource.js +34 -0
  118. package/dist/resources/templates.resource.js.map +1 -0
  119. package/dist/resources/unread.resource.d.ts +11 -0
  120. package/dist/resources/unread.resource.d.ts.map +1 -0
  121. package/dist/resources/unread.resource.js +46 -0
  122. package/dist/resources/unread.resource.js.map +1 -0
  123. package/dist/safety/audit.d.ts +17 -0
  124. package/dist/safety/audit.d.ts.map +1 -0
  125. package/dist/safety/audit.js +50 -0
  126. package/dist/safety/audit.js.map +1 -0
  127. package/dist/safety/rate-limiter.d.ts +22 -0
  128. package/dist/safety/rate-limiter.d.ts.map +1 -0
  129. package/dist/safety/rate-limiter.js +52 -0
  130. package/dist/safety/rate-limiter.js.map +1 -0
  131. package/dist/safety/validation.d.ts +44 -0
  132. package/dist/safety/validation.d.ts.map +1 -0
  133. package/dist/safety/validation.js +113 -0
  134. package/dist/safety/validation.js.map +1 -0
  135. package/dist/server.d.ts +10 -0
  136. package/dist/server.d.ts.map +1 -0
  137. package/dist/server.js +25 -0
  138. package/dist/server.js.map +1 -0
  139. package/dist/services/calendar.service.d.ts +22 -0
  140. package/dist/services/calendar.service.d.ts.map +1 -0
  141. package/dist/services/calendar.service.js +115 -0
  142. package/dist/services/calendar.service.js.map +1 -0
  143. package/dist/services/event-bus.d.ts +28 -0
  144. package/dist/services/event-bus.d.ts.map +1 -0
  145. package/dist/services/event-bus.js +16 -0
  146. package/dist/services/event-bus.js.map +1 -0
  147. package/dist/services/hooks.service.d.ts +77 -0
  148. package/dist/services/hooks.service.d.ts.map +1 -0
  149. package/dist/services/hooks.service.js +498 -0
  150. package/dist/services/hooks.service.js.map +1 -0
  151. package/dist/services/imap.service.d.ts +133 -0
  152. package/dist/services/imap.service.d.ts.map +1 -0
  153. package/dist/services/imap.service.js +1393 -0
  154. package/dist/services/imap.service.js.map +1 -0
  155. package/dist/services/label-strategy.d.ts +20 -0
  156. package/dist/services/label-strategy.d.ts.map +1 -0
  157. package/dist/services/label-strategy.js +237 -0
  158. package/dist/services/label-strategy.js.map +1 -0
  159. package/dist/services/local-calendar.service.d.ts +126 -0
  160. package/dist/services/local-calendar.service.d.ts.map +1 -0
  161. package/dist/services/local-calendar.service.js +428 -0
  162. package/dist/services/local-calendar.service.js.map +1 -0
  163. package/dist/services/notifier.service.d.ts +69 -0
  164. package/dist/services/notifier.service.d.ts.map +1 -0
  165. package/dist/services/notifier.service.js +319 -0
  166. package/dist/services/notifier.service.js.map +1 -0
  167. package/dist/services/oauth.service.d.ts +47 -0
  168. package/dist/services/oauth.service.d.ts.map +1 -0
  169. package/dist/services/oauth.service.js +140 -0
  170. package/dist/services/oauth.service.js.map +1 -0
  171. package/dist/services/presets.d.ts +36 -0
  172. package/dist/services/presets.d.ts.map +1 -0
  173. package/dist/services/presets.js +173 -0
  174. package/dist/services/presets.js.map +1 -0
  175. package/dist/services/reminders.service.d.ts +63 -0
  176. package/dist/services/reminders.service.d.ts.map +1 -0
  177. package/dist/services/reminders.service.js +281 -0
  178. package/dist/services/reminders.service.js.map +1 -0
  179. package/dist/services/scheduler.service.d.ts +42 -0
  180. package/dist/services/scheduler.service.d.ts.map +1 -0
  181. package/dist/services/scheduler.service.js +260 -0
  182. package/dist/services/scheduler.service.js.map +1 -0
  183. package/dist/services/smtp.service.d.ts +40 -0
  184. package/dist/services/smtp.service.d.ts.map +1 -0
  185. package/dist/services/smtp.service.js +151 -0
  186. package/dist/services/smtp.service.js.map +1 -0
  187. package/dist/services/template.service.d.ts +33 -0
  188. package/dist/services/template.service.d.ts.map +1 -0
  189. package/dist/services/template.service.js +123 -0
  190. package/dist/services/template.service.js.map +1 -0
  191. package/dist/services/watcher.service.d.ts +36 -0
  192. package/dist/services/watcher.service.d.ts.map +1 -0
  193. package/dist/services/watcher.service.js +241 -0
  194. package/dist/services/watcher.service.js.map +1 -0
  195. package/dist/tools/accounts.tool.d.ts +7 -0
  196. package/dist/tools/accounts.tool.d.ts.map +1 -0
  197. package/dist/tools/accounts.tool.js +29 -0
  198. package/dist/tools/accounts.tool.js.map +1 -0
  199. package/dist/tools/analytics.tool.d.ts +9 -0
  200. package/dist/tools/analytics.tool.d.ts.map +1 -0
  201. package/dist/tools/analytics.tool.js +27 -0
  202. package/dist/tools/analytics.tool.js.map +1 -0
  203. package/dist/tools/attachments.tool.d.ts +7 -0
  204. package/dist/tools/attachments.tool.d.ts.map +1 -0
  205. package/dist/tools/attachments.tool.js +45 -0
  206. package/dist/tools/attachments.tool.js.map +1 -0
  207. package/dist/tools/bulk.tool.d.ts +7 -0
  208. package/dist/tools/bulk.tool.d.ts.map +1 -0
  209. package/dist/tools/bulk.tool.js +75 -0
  210. package/dist/tools/bulk.tool.js.map +1 -0
  211. package/dist/tools/calendar.tool.d.ts +19 -0
  212. package/dist/tools/calendar.tool.d.ts.map +1 -0
  213. package/dist/tools/calendar.tool.js +538 -0
  214. package/dist/tools/calendar.tool.js.map +1 -0
  215. package/dist/tools/contacts.tool.d.ts +7 -0
  216. package/dist/tools/contacts.tool.d.ts.map +1 -0
  217. package/dist/tools/contacts.tool.js +44 -0
  218. package/dist/tools/contacts.tool.js.map +1 -0
  219. package/dist/tools/drafts.tool.d.ts +8 -0
  220. package/dist/tools/drafts.tool.d.ts.map +1 -0
  221. package/dist/tools/drafts.tool.js +92 -0
  222. package/dist/tools/drafts.tool.js.map +1 -0
  223. package/dist/tools/emails.tool.d.ts +7 -0
  224. package/dist/tools/emails.tool.d.ts.map +1 -0
  225. package/dist/tools/emails.tool.js +400 -0
  226. package/dist/tools/emails.tool.js.map +1 -0
  227. package/dist/tools/folders.tool.d.ts +7 -0
  228. package/dist/tools/folders.tool.d.ts.map +1 -0
  229. package/dist/tools/folders.tool.js +111 -0
  230. package/dist/tools/folders.tool.js.map +1 -0
  231. package/dist/tools/health.tool.d.ts +10 -0
  232. package/dist/tools/health.tool.d.ts.map +1 -0
  233. package/dist/tools/health.tool.js +78 -0
  234. package/dist/tools/health.tool.js.map +1 -0
  235. package/dist/tools/label.tool.d.ts +11 -0
  236. package/dist/tools/label.tool.d.ts.map +1 -0
  237. package/dist/tools/label.tool.js +165 -0
  238. package/dist/tools/label.tool.js.map +1 -0
  239. package/dist/tools/locate.tool.d.ts +11 -0
  240. package/dist/tools/locate.tool.d.ts.map +1 -0
  241. package/dist/tools/locate.tool.js +59 -0
  242. package/dist/tools/locate.tool.js.map +1 -0
  243. package/dist/tools/mailboxes.tool.d.ts +7 -0
  244. package/dist/tools/mailboxes.tool.d.ts.map +1 -0
  245. package/dist/tools/mailboxes.tool.js +38 -0
  246. package/dist/tools/mailboxes.tool.js.map +1 -0
  247. package/dist/tools/manage.tool.d.ts +7 -0
  248. package/dist/tools/manage.tool.d.ts.map +1 -0
  249. package/dist/tools/manage.tool.js +125 -0
  250. package/dist/tools/manage.tool.js.map +1 -0
  251. package/dist/tools/register.d.ts +20 -0
  252. package/dist/tools/register.d.ts.map +1 -0
  253. package/dist/tools/register.js +53 -0
  254. package/dist/tools/register.js.map +1 -0
  255. package/dist/tools/scheduler.tool.d.ts +9 -0
  256. package/dist/tools/scheduler.tool.d.ts.map +1 -0
  257. package/dist/tools/scheduler.tool.js +104 -0
  258. package/dist/tools/scheduler.tool.js.map +1 -0
  259. package/dist/tools/send.tool.d.ts +7 -0
  260. package/dist/tools/send.tool.d.ts.map +1 -0
  261. package/dist/tools/send.tool.js +123 -0
  262. package/dist/tools/send.tool.js.map +1 -0
  263. package/dist/tools/templates.tool.d.ts +12 -0
  264. package/dist/tools/templates.tool.d.ts.map +1 -0
  265. package/dist/tools/templates.tool.js +140 -0
  266. package/dist/tools/templates.tool.js.map +1 -0
  267. package/dist/tools/thread.tool.d.ts +10 -0
  268. package/dist/tools/thread.tool.d.ts.map +1 -0
  269. package/dist/tools/thread.tool.js +146 -0
  270. package/dist/tools/thread.tool.js.map +1 -0
  271. package/dist/tools/watcher.tool.d.ts +9 -0
  272. package/dist/tools/watcher.tool.d.ts.map +1 -0
  273. package/dist/tools/watcher.tool.js +282 -0
  274. package/dist/tools/watcher.tool.js.map +1 -0
  275. package/dist/types/index.d.ts +271 -0
  276. package/dist/types/index.d.ts.map +1 -0
  277. package/dist/types/index.js +5 -0
  278. package/dist/types/index.js.map +1 -0
  279. package/dist/utils/calendar-notes.d.ts +31 -0
  280. package/dist/utils/calendar-notes.d.ts.map +1 -0
  281. package/dist/utils/calendar-notes.js +99 -0
  282. package/dist/utils/calendar-notes.js.map +1 -0
  283. package/dist/utils/calendar-state.d.ts +27 -0
  284. package/dist/utils/calendar-state.d.ts.map +1 -0
  285. package/dist/utils/calendar-state.js +85 -0
  286. package/dist/utils/calendar-state.js.map +1 -0
  287. package/dist/utils/conference-details.d.ts +12 -0
  288. package/dist/utils/conference-details.d.ts.map +1 -0
  289. package/dist/utils/conference-details.js +71 -0
  290. package/dist/utils/conference-details.js.map +1 -0
  291. package/dist/utils/meeting-url.d.ts +10 -0
  292. package/dist/utils/meeting-url.d.ts.map +1 -0
  293. package/dist/utils/meeting-url.js +30 -0
  294. package/dist/utils/meeting-url.js.map +1 -0
  295. 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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"}