nothumanallowed 13.5.199 → 14.0.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.
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Drive routes — Google Drive + OneDrive + Microsoft Todo + Notes
3
+ */
4
+
5
+ import { sendJSON, sendError, parseBody } from '../index.mjs';
6
+ import { loadConfig } from '../../config.mjs';
7
+
8
+ export function register(router) {
9
+ // ── Google Drive ──────────────────────────────────────────────────────
10
+
11
+ router.get('/api/drive', async (req, res) => {
12
+ try {
13
+ const { listFiles, searchFiles } = await import('../../services/google-drive.mjs');
14
+ const config = loadConfig();
15
+ const url = new URL(req.url, 'http://localhost');
16
+ const q = url.searchParams.get('q') || '';
17
+ const files = q ? await searchFiles(config, q) : await listFiles(config);
18
+ sendJSON(res, 200, { files });
19
+ } catch (e) { sendError(res, 500, e.message); }
20
+ });
21
+
22
+ const DRIVE_READ_RE = /^\/api\/drive\/read\/(.+)$/;
23
+ const DRIVE_DL_RE = /^\/api\/drive\/download\/(.+)$/;
24
+ const DRIVE_UPD_RE = /^\/api\/drive\/update\/(.+)$/;
25
+ const DRIVE_DEL_RE = /^\/api\/drive\/delete\/(.+)$/;
26
+
27
+ router.get(DRIVE_READ_RE, async (req, res) => {
28
+ try {
29
+ const { readFileAsText } = await import('../../services/google-drive.mjs');
30
+ const id = req.url.match(DRIVE_READ_RE)?.[1];
31
+ const config = loadConfig();
32
+ sendJSON(res, 200, { content: await readFileAsText(config, id) });
33
+ } catch (e) { sendError(res, 500, e.message); }
34
+ });
35
+
36
+ router.get(DRIVE_DL_RE, async (req, res) => {
37
+ try {
38
+ const { downloadFileContent } = await import('../../services/google-drive.mjs');
39
+ const id = req.url.match(DRIVE_DL_RE)?.[1];
40
+ const config = loadConfig();
41
+ const result = await downloadFileContent(config, id);
42
+ res.writeHead(200, { 'Content-Type': result.mimeType || 'application/octet-stream', 'Content-Disposition': `attachment; filename="${result.name || id}"` });
43
+ res.end(result.data);
44
+ } catch (e) { sendError(res, 500, e.message); }
45
+ });
46
+
47
+ router.post(DRIVE_UPD_RE, async (req, res) => {
48
+ try {
49
+ const { updateFileContent } = await import('../../services/google-drive.mjs');
50
+ const id = req.url.match(DRIVE_UPD_RE)?.[1];
51
+ const body = await parseBody(req);
52
+ const config = loadConfig();
53
+ await updateFileContent(config, id, body.content);
54
+ sendJSON(res, 200, { ok: true });
55
+ } catch (e) { sendError(res, 500, e.message); }
56
+ });
57
+
58
+ router.post('/api/drive/upload', async (req, res) => {
59
+ try {
60
+ const { uploadFile } = await import('../../services/google-drive.mjs');
61
+ const body = await parseBody(req, 20_000_000);
62
+ const config = loadConfig();
63
+ const file = await uploadFile(config, body.name, body.content, body.mimeType, body.folderId);
64
+ sendJSON(res, 201, { file });
65
+ } catch (e) { sendError(res, 500, e.message); }
66
+ });
67
+
68
+ router.post(DRIVE_DEL_RE, async (req, res) => {
69
+ try {
70
+ const { trashFile } = await import('../../services/google-drive.mjs');
71
+ const id = req.url.match(DRIVE_DEL_RE)?.[1];
72
+ const config = loadConfig();
73
+ await trashFile(config, id);
74
+ sendJSON(res, 200, { ok: true });
75
+ } catch (e) { sendError(res, 500, e.message); }
76
+ });
77
+
78
+ router.get('/api/drive/quota', async (_req, res) => {
79
+ try {
80
+ const { getStorageQuota } = await import('../../services/google-drive.mjs');
81
+ const config = loadConfig();
82
+ sendJSON(res, 200, await getStorageQuota(config));
83
+ } catch (e) { sendError(res, 500, e.message); }
84
+ });
85
+
86
+ router.get('/api/drive/recent', async (_req, res) => {
87
+ try {
88
+ const { getRecentFiles } = await import('../../services/google-drive.mjs');
89
+ const config = loadConfig();
90
+ sendJSON(res, 200, { files: await getRecentFiles(config) });
91
+ } catch (e) { sendError(res, 500, e.message); }
92
+ });
93
+
94
+ router.get('/api/drive/starred', async (_req, res) => {
95
+ try {
96
+ const { getStarredFiles } = await import('../../services/google-drive.mjs');
97
+ const config = loadConfig();
98
+ sendJSON(res, 200, { files: await getStarredFiles(config) });
99
+ } catch (e) { sendError(res, 500, e.message); }
100
+ });
101
+
102
+ router.get('/api/drive/shared', async (_req, res) => {
103
+ try {
104
+ const { getSharedFiles } = await import('../../services/google-drive.mjs');
105
+ const config = loadConfig();
106
+ sendJSON(res, 200, { files: await getSharedFiles(config) });
107
+ } catch (e) { sendError(res, 500, e.message); }
108
+ });
109
+
110
+ const DRIVE_FOLDER_RE = /^\/api\/drive\/folder\/(.+)$/;
111
+ router.get(DRIVE_FOLDER_RE, async (req, res) => {
112
+ try {
113
+ const { listFolder } = await import('../../services/google-drive.mjs');
114
+ const folderId = req.url.match(DRIVE_FOLDER_RE)?.[1] || 'root';
115
+ const config = loadConfig();
116
+ sendJSON(res, 200, { files: await listFolder(config, folderId) });
117
+ } catch (e) { sendError(res, 500, e.message); }
118
+ });
119
+
120
+ router.post('/api/drive/folder/create', async (req, res) => {
121
+ try {
122
+ const { createFolder } = await import('../../services/google-drive.mjs');
123
+ const body = await parseBody(req);
124
+ const config = loadConfig();
125
+ if (!body.name) return sendError(res, 400, 'name required');
126
+ const folder = await createFolder(config, body.name, body.parentId || 'root');
127
+ sendJSON(res, 201, { folder });
128
+ } catch (e) { sendError(res, 500, e.message); }
129
+ });
130
+
131
+ // ── OneDrive ──────────────────────────────────────────────────────────
132
+
133
+ router.get('/api/onedrive', async (_req, res) => {
134
+ try {
135
+ const { listOneDriveFiles } = await import('../../services/microsoft-drive.mjs');
136
+ const config = loadConfig();
137
+ sendJSON(res, 200, { files: await listOneDriveFiles(config) });
138
+ } catch (e) { sendError(res, 500, e.message); }
139
+ });
140
+
141
+ // ── Microsoft Todo ────────────────────────────────────────────────────
142
+
143
+ router.get('/api/mstodo', async (_req, res) => {
144
+ try {
145
+ const { getMsTodoTasks } = await import('../../services/microsoft-todo.mjs');
146
+ const config = loadConfig();
147
+ sendJSON(res, 200, { tasks: await getMsTodoTasks(config) });
148
+ } catch (e) { sendError(res, 500, e.message); }
149
+ });
150
+
151
+ router.post('/api/mstodo', async (req, res) => {
152
+ try {
153
+ const body = await parseBody(req);
154
+ const config = loadConfig();
155
+ if (body.action === 'complete') {
156
+ const { completeMsTodoTask } = await import('../../services/microsoft-todo.mjs');
157
+ await completeMsTodoTask(config, body.listId, body.taskId);
158
+ return sendJSON(res, 200, { ok: true });
159
+ }
160
+ const { addMsTodoTask } = await import('../../services/microsoft-todo.mjs');
161
+ const task = await addMsTodoTask(config, body.listId, body.title);
162
+ sendJSON(res, 201, { task });
163
+ } catch (e) { sendError(res, 500, e.message); }
164
+ });
165
+
166
+ // ── Notes ─────────────────────────────────────────────────────────────
167
+
168
+ router.get('/api/notes', async (_req, res) => {
169
+ try {
170
+ const { listNotes } = await import('../../services/notes.mjs');
171
+ sendJSON(res, 200, { notes: listNotes() });
172
+ } catch (e) { sendError(res, 500, e.message); }
173
+ });
174
+
175
+ router.post('/api/notes', async (req, res) => {
176
+ try {
177
+ const body = await parseBody(req);
178
+ const { saveNote, deleteNote } = await import('../../services/notes.mjs');
179
+ if (body.action === 'delete') { deleteNote(body.id); return sendJSON(res, 200, { ok: true }); }
180
+ const note = saveNote(body);
181
+ sendJSON(res, 200, { note });
182
+ } catch (e) { sendError(res, 500, e.message); }
183
+ });
184
+ }
@@ -0,0 +1,428 @@
1
+ /**
2
+ * Email routes — Google Gmail + IMAP accounts
3
+ */
4
+
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import { sendJSON, sendError, parseBody } from '../index.mjs';
8
+ import { loadConfig } from '../../config.mjs';
9
+ import { NHA_DIR } from '../../constants.mjs';
10
+ import { getUnreadImportant, getMessage, listMessages, getTodayEmails, sendEmail, createDraft } from '../../services/mail-router.mjs';
11
+
12
+ export function register(router) {
13
+ // ── Gmail ──────────────────────────────────────────────────────────────
14
+
15
+ router.get('/api/emails', async (req, res) => {
16
+ try {
17
+ const config = loadConfig();
18
+ const url = new URL(req.url, 'http://localhost');
19
+ const folder = url.searchParams.get('folder') || 'inbox';
20
+ const limit = parseInt(url.searchParams.get('pageSize') || url.searchParams.get('limit') || '50');
21
+ const offset = parseInt(url.searchParams.get('page') || url.searchParams.get('offset') || '0') * (url.searchParams.get('page') ? limit : 1);
22
+ try {
23
+ // getUnreadImportant returns fully-parsed messages (subject, from, snippet, etc.)
24
+ // For non-inbox folders fall back to raw id+threadId list
25
+ let emails;
26
+ if (folder === 'inbox' || folder === 'INBOX') {
27
+ emails = await getUnreadImportant(config, limit + offset);
28
+ } else {
29
+ const gmailQuery = folder === 'sent' ? 'in:sent' : folder === 'spam' ? 'in:spam' : folder === 'trash' ? 'in:trash' : `in:${folder}`;
30
+ const refs = await listMessages(config, gmailQuery, limit + offset);
31
+ emails = refs; // raw refs — full detail would require N individual getMessage calls
32
+ }
33
+ sendJSON(res, 200, { emails: emails.slice(offset, offset + limit), total: emails.length });
34
+ } catch (providerErr) {
35
+ if (providerErr.message?.includes('No mail provider') || providerErr.message?.includes('token')) {
36
+ try {
37
+ const { listAccounts, listMessages: imapList } = await import('../../services/email-db.mjs');
38
+ const accounts = listAccounts();
39
+ if (accounts.length === 0) {
40
+ return sendJSON(res, 200, { emails: [], total: 0, authRequired: true });
41
+ }
42
+ const acc = accounts.find(a => a.is_active !== 0) || accounts[0];
43
+ const result = imapList(acc.id, null, limit, typeof offset === 'number' ? offset : 0, '');
44
+ const msgs = (result.messages ?? []).map(m => ({
45
+ id: m.id,
46
+ from: m.from_name ? `${m.from_name} <${m.from_address}>` : (m.from_address || ''),
47
+ subject: m.subject || '(no subject)',
48
+ date: m.internal_date,
49
+ snippet: m.body_preview || '',
50
+ isUnread: !m.is_read,
51
+ isStarred: !!m.is_starred,
52
+ }));
53
+ return sendJSON(res, 200, { emails: msgs, total: result.total ?? msgs.length, source: 'imap' });
54
+ } catch {
55
+ return sendJSON(res, 200, { emails: [], total: 0, authRequired: true });
56
+ }
57
+ }
58
+ throw providerErr;
59
+ }
60
+ } catch (e) { sendError(res, 500, e.message); }
61
+ });
62
+
63
+ router.post('/api/email/read', async (req, res) => {
64
+ try {
65
+ const body = await parseBody(req);
66
+ const config = loadConfig();
67
+ const msg = await getMessage(config, body.id);
68
+ sendJSON(res, 200, { message: msg });
69
+ } catch (e) { sendError(res, 500, e.message); }
70
+ });
71
+
72
+ router.post('/api/email/send', async (req, res) => {
73
+ try {
74
+ const body = await parseBody(req);
75
+ const config = loadConfig();
76
+ await sendEmail(config, body);
77
+ sendJSON(res, 200, { ok: true });
78
+ } catch (e) { sendError(res, 500, e.message); }
79
+ });
80
+
81
+ router.post('/api/email/mark-read', async (req, res) => {
82
+ try {
83
+ const body = await parseBody(req);
84
+ const config = loadConfig();
85
+ const { markAsRead } = await import('../../services/google-gmail.mjs');
86
+ await markAsRead(config, body.messageId);
87
+ sendJSON(res, 200, { ok: true });
88
+ } catch (e) { sendError(res, 500, e.message); }
89
+ });
90
+
91
+ router.post('/api/email/mark-all-read', async (req, res) => {
92
+ try {
93
+ const config = loadConfig();
94
+ const { markAllAsRead } = await import('../../services/google-gmail.mjs');
95
+ const result = await markAllAsRead(config);
96
+ sendJSON(res, 200, { ok: true, count: result?.count ?? 0 });
97
+ } catch (e) { sendError(res, 500, e.message); }
98
+ });
99
+
100
+ // ── IMAP accounts ──────────────────────────────────────────────────────
101
+
102
+ router.get('/api/imap/accounts', async (_req, res) => {
103
+ try {
104
+ const { listAccounts } = await import('../../services/email-db.mjs');
105
+ sendJSON(res, 200, { accounts: listAccounts() });
106
+ } catch (e) { sendError(res, 500, e.message); }
107
+ });
108
+
109
+ router.post('/api/imap/accounts', async (req, res) => {
110
+ try {
111
+ const body = await parseBody(req);
112
+ const { createAccount } = await import('../../services/email-db.mjs');
113
+ const account = createAccount(body);
114
+ sendJSON(res, 201, { account });
115
+ } catch (e) { sendError(res, 500, e.message); }
116
+ });
117
+
118
+ router.post('/api/imap/accounts/update', async (req, res) => {
119
+ try {
120
+ const body = await parseBody(req);
121
+ const { updateAccount } = await import('../../services/email-db.mjs');
122
+ updateAccount(body.id, body);
123
+ sendJSON(res, 200, { ok: true });
124
+ } catch (e) { sendError(res, 500, e.message); }
125
+ });
126
+
127
+ router.post('/api/imap/accounts/delete', async (req, res) => {
128
+ try {
129
+ const body = await parseBody(req);
130
+ const { deleteAccount } = await import('../../services/email-db.mjs');
131
+ deleteAccount(body.id);
132
+ sendJSON(res, 200, { ok: true });
133
+ } catch (e) { sendError(res, 500, e.message); }
134
+ });
135
+
136
+ // POST /api/imap/sync (body: { accountId, force })
137
+ router.post('/api/imap/sync', async (req, res) => {
138
+ try {
139
+ const body = await parseBody(req);
140
+ const { syncAccount } = await import('../../services/email-imap.mjs');
141
+ const result = await syncAccount(body.accountId, { force: body.force });
142
+ sendJSON(res, 200, result ?? { ok: true });
143
+ } catch (e) { sendError(res, 500, e.message); }
144
+ });
145
+
146
+ // POST /api/imap/sync/:accountId (UI calls this format)
147
+ const IMAP_SYNC_RE = /^\/api\/imap\/sync\/(.+)$/;
148
+ router.post(IMAP_SYNC_RE, async (req, res) => {
149
+ try {
150
+ const accountId = req.url.match(IMAP_SYNC_RE)?.[1];
151
+ const body = await parseBody(req).catch(() => ({}));
152
+ const { syncAccount } = await import('../../services/email-imap.mjs');
153
+ const result = await syncAccount(accountId, { force: body.force });
154
+ sendJSON(res, 200, result ?? { ok: true });
155
+ } catch (e) { sendError(res, 500, e.message); }
156
+ });
157
+
158
+ router.get('/api/imap/messages', async (req, res) => {
159
+ try {
160
+ const { listMessages } = await import('../../services/email-db.mjs');
161
+ const url = new URL(req.url, 'http://localhost');
162
+ const accountId = url.searchParams.get('accountId');
163
+ const folder = url.searchParams.get('folder') || 'INBOX';
164
+ const limit = parseInt(url.searchParams.get('limit') || '50');
165
+ const offset = parseInt(url.searchParams.get('offset') || '0');
166
+ const search = url.searchParams.get('search') || '';
167
+ // listMessages(accountId, labelId, limit, offset, search)
168
+ const result = listMessages(accountId, null, limit, offset, search);
169
+ sendJSON(res, 200, { messages: result.messages ?? result, total: result.total ?? 0 });
170
+ } catch (e) { sendError(res, 500, e.message); }
171
+ });
172
+
173
+ router.get('/api/imap/message', async (req, res) => {
174
+ try {
175
+ const { getMessage } = await import('../../services/email-db.mjs');
176
+ const url = new URL(req.url, 'http://localhost');
177
+ const id = url.searchParams.get('id');
178
+ sendJSON(res, 200, { message: getMessage(id) });
179
+ } catch (e) { sendError(res, 500, e.message); }
180
+ });
181
+
182
+ router.get('/api/imap/thread', async (req, res) => {
183
+ try {
184
+ const { getThread } = await import('../../services/email-db.mjs');
185
+ const url = new URL(req.url, 'http://localhost');
186
+ const threadId = url.searchParams.get('threadId');
187
+ const accountId = url.searchParams.get('accountId');
188
+ sendJSON(res, 200, { messages: getThread(threadId, accountId) });
189
+ } catch (e) { sendError(res, 500, e.message); }
190
+ });
191
+
192
+ router.post('/api/imap/send', async (req, res) => {
193
+ try {
194
+ const body = await parseBody(req);
195
+ const { sendImapEmail } = await import('../../services/email-smtp.mjs');
196
+ await sendImapEmail(body);
197
+ sendJSON(res, 200, { ok: true });
198
+ } catch (e) { sendError(res, 500, e.message); }
199
+ });
200
+
201
+ router.post('/api/imap/mark-read', async (req, res) => {
202
+ try {
203
+ const body = await parseBody(req);
204
+ const { markRead } = await import('../../services/email-db.mjs');
205
+ markRead(body.messageId || body.id, true);
206
+ sendJSON(res, 200, { ok: true });
207
+ } catch (e) { sendError(res, 500, e.message); }
208
+ });
209
+
210
+ router.post('/api/imap/mark-starred', async (req, res) => {
211
+ try {
212
+ const body = await parseBody(req);
213
+ const { markStarred } = await import('../../services/email-db.mjs');
214
+ markStarred(body.messageId || body.id, body.starred !== false);
215
+ sendJSON(res, 200, { ok: true });
216
+ } catch (e) { sendError(res, 500, e.message); }
217
+ });
218
+
219
+ router.post('/api/imap/trash', async (req, res) => {
220
+ try {
221
+ const body = await parseBody(req);
222
+ const { softDelete } = await import('../../services/email-db.mjs');
223
+ softDelete(body.messageId || body.id);
224
+ sendJSON(res, 200, { ok: true });
225
+ } catch (e) { sendError(res, 500, e.message); }
226
+ });
227
+
228
+ router.get('/api/imap/labels', async (req, res) => {
229
+ try {
230
+ const { listLabels } = await import('../../services/email-db.mjs');
231
+ const url = new URL(req.url, 'http://localhost');
232
+ sendJSON(res, 200, { labels: listLabels(url.searchParams.get('accountId')) });
233
+ } catch (e) { sendError(res, 500, e.message); }
234
+ });
235
+
236
+ router.post('/api/imap/labels/create', async (req, res) => {
237
+ try {
238
+ const body = await parseBody(req);
239
+ const { createLabel } = await import('../../services/email-db.mjs');
240
+ sendJSON(res, 201, { label: createLabel(body.accountId, body.name, body.color, body.parentId) });
241
+ } catch (e) { sendError(res, 500, e.message); }
242
+ });
243
+
244
+ router.post('/api/imap/labels/assign', async (req, res) => {
245
+ try {
246
+ const body = await parseBody(req);
247
+ const { addMessageToLabel } = await import('../../services/email-db.mjs');
248
+ addMessageToLabel(body.messageId, body.labelId);
249
+ sendJSON(res, 200, { ok: true });
250
+ } catch (e) { sendError(res, 500, e.message); }
251
+ });
252
+
253
+ router.get('/api/imap/unread-count', async (req, res) => {
254
+ try {
255
+ const { listAccounts, getDb } = await import('../../services/email-db.mjs');
256
+ const url = new URL(req.url, 'http://localhost');
257
+ const filterAccountId = url.searchParams.get('accountId');
258
+ const accounts = filterAccountId ? [{ id: filterAccountId }] : listAccounts();
259
+ const db = getDb();
260
+ let total = 0;
261
+ for (const acc of accounts) {
262
+ const row = db.prepare(`
263
+ SELECT COUNT(*) as n FROM email_messages m
264
+ JOIN email_message_labels eml ON eml.message_id = m.id
265
+ JOIN email_labels l ON l.id = eml.label_id
266
+ LEFT JOIN email_message_state s ON s.message_id = m.id
267
+ WHERE m.account_id = ? AND l.system_type = 'inbox'
268
+ AND COALESCE(s.is_read, 0) = 0
269
+ AND m.permanently_deleted = 0
270
+ `).get(acc.id);
271
+ total += row?.n || 0;
272
+ }
273
+ sendJSON(res, 200, { unread: total, count: total });
274
+ } catch (e) { sendError(res, 500, e.message); }
275
+ });
276
+
277
+ router.post('/api/imap/mark-all-read', async (req, res) => {
278
+ try {
279
+ const body = await parseBody(req);
280
+ const { markAllRead } = await import('../../services/email-db.mjs');
281
+ markAllRead(body.accountId, body.labelId || body.folder);
282
+ sendJSON(res, 200, { ok: true });
283
+ } catch (e) { sendError(res, 500, e.message); }
284
+ });
285
+
286
+ router.get('/api/imap/attachment', async (req, res) => {
287
+ try {
288
+ const { getDb } = await import('../../services/email-db.mjs');
289
+ const url = new URL(req.url, 'http://localhost');
290
+ const messageId = url.searchParams.get('messageId');
291
+ const attachmentId = url.searchParams.get('attachmentId');
292
+ const db = getDb();
293
+ const att = db.prepare('SELECT * FROM attachments WHERE id=? AND message_id=?').get(attachmentId, messageId);
294
+ if (!att) return sendError(res, 404, 'Attachment not found');
295
+ res.writeHead(200, { 'Content-Type': att.content_type || 'application/octet-stream', 'Content-Disposition': `attachment; filename="${att.filename || 'attachment'}"` });
296
+ res.end(Buffer.from(att.data || '', 'base64'));
297
+ } catch (e) { sendError(res, 500, e.message); }
298
+ });
299
+
300
+ // Blocked senders + rules + signatures + drafts (abbreviated — full parity)
301
+ router.get('/api/imap/blocked', async (req, res) => {
302
+ try {
303
+ const { listBlockedSenders } = await import('../../services/email-db.mjs');
304
+ const url = new URL(req.url, 'http://localhost');
305
+ sendJSON(res, 200, { blocked: listBlockedSenders(url.searchParams.get('accountId')) });
306
+ } catch (e) { sendError(res, 500, e.message); }
307
+ });
308
+
309
+ router.post('/api/imap/blocked/add', async (req, res) => {
310
+ try {
311
+ const body = await parseBody(req);
312
+ const { addBlockedSender } = await import('../../services/email-db.mjs');
313
+ addBlockedSender(body.accountId, body.email);
314
+ sendJSON(res, 200, { ok: true });
315
+ } catch (e) { sendError(res, 500, e.message); }
316
+ });
317
+
318
+ router.get('/api/imap/signatures', async (req, res) => {
319
+ try {
320
+ const { listSignatures } = await import('../../services/email-db.mjs');
321
+ const url = new URL(req.url, 'http://localhost');
322
+ sendJSON(res, 200, { signatures: listSignatures(url.searchParams.get('accountId')) });
323
+ } catch (e) { sendError(res, 500, e.message); }
324
+ });
325
+
326
+ router.post('/api/imap/signatures/create', async (req, res) => {
327
+ try {
328
+ const body = await parseBody(req);
329
+ const { createSignature } = await import('../../services/email-db.mjs');
330
+ sendJSON(res, 201, { signature: createSignature(body.accountId, body.name, body.content, body.isDefault) });
331
+ } catch (e) { sendError(res, 500, e.message); }
332
+ });
333
+
334
+ router.get('/api/imap/drafts', async (req, res) => {
335
+ try {
336
+ const { listDrafts } = await import('../../services/email-db.mjs');
337
+ const url = new URL(req.url, 'http://localhost');
338
+ sendJSON(res, 200, { drafts: listDrafts(url.searchParams.get('accountId')) });
339
+ } catch (e) { sendError(res, 500, e.message); }
340
+ });
341
+
342
+ router.post('/api/imap/drafts/save', async (req, res) => {
343
+ try {
344
+ const body = await parseBody(req);
345
+ const { saveDraft } = await import('../../services/email-db.mjs');
346
+ sendJSON(res, 200, { draft: saveDraft(body) });
347
+ } catch (e) { sendError(res, 500, e.message); }
348
+ });
349
+
350
+ router.post('/api/imap/drafts/delete', async (req, res) => {
351
+ try {
352
+ const body = await parseBody(req);
353
+ const { deleteDraft } = await import('../../services/email-db.mjs');
354
+ deleteDraft(body.id);
355
+ sendJSON(res, 200, { ok: true });
356
+ } catch (e) { sendError(res, 500, e.message); }
357
+ });
358
+
359
+ router.post('/api/imap/labels/update', async (req, res) => {
360
+ try {
361
+ const body = await parseBody(req);
362
+ const { updateLabel } = await import('../../services/email-db.mjs');
363
+ if (!body.id) return sendError(res, 400, 'id required');
364
+ updateLabel(body.id, body);
365
+ sendJSON(res, 200, { ok: true });
366
+ } catch (e) { sendError(res, 500, e.message); }
367
+ });
368
+
369
+ router.post('/api/imap/labels/delete', async (req, res) => {
370
+ try {
371
+ const body = await parseBody(req);
372
+ const { deleteLabel } = await import('../../services/email-db.mjs');
373
+ if (!body.id) return sendError(res, 400, 'id required');
374
+ deleteLabel(body.id);
375
+ sendJSON(res, 200, { ok: true });
376
+ } catch (e) { sendError(res, 500, e.message); }
377
+ });
378
+
379
+ router.post('/api/imap/blocked/remove', async (req, res) => {
380
+ try {
381
+ const body = await parseBody(req);
382
+ const { unblockSender } = await import('../../services/email-db.mjs');
383
+ if (!body.id) return sendError(res, 400, 'id required');
384
+ unblockSender(body.id);
385
+ sendJSON(res, 200, { ok: true });
386
+ } catch (e) { sendError(res, 500, e.message); }
387
+ });
388
+
389
+ router.get('/api/imap/rules', async (req, res) => {
390
+ try {
391
+ const { listArchivingRules } = await import('../../services/email-db.mjs');
392
+ const url = new URL(req.url, 'http://localhost');
393
+ const accountId = url.searchParams.get('accountId');
394
+ if (!accountId) return sendError(res, 400, 'accountId required');
395
+ sendJSON(res, 200, { rules: listArchivingRules(accountId) });
396
+ } catch (e) { sendError(res, 500, e.message); }
397
+ });
398
+
399
+ router.post('/api/imap/rules/create', async (req, res) => {
400
+ try {
401
+ const body = await parseBody(req);
402
+ const { createArchivingRule } = await import('../../services/email-db.mjs');
403
+ if (!body.accountId || !body.matchType || !body.matchValue) return sendError(res, 400, 'accountId, matchType, matchValue required');
404
+ const id = createArchivingRule(body.accountId, body.matchType, body.matchValue, body.targetLabelId);
405
+ sendJSON(res, 200, { ok: true, id });
406
+ } catch (e) { sendError(res, 500, e.message); }
407
+ });
408
+
409
+ router.post('/api/imap/rules/delete', async (req, res) => {
410
+ try {
411
+ const body = await parseBody(req);
412
+ const { deleteArchivingRule } = await import('../../services/email-db.mjs');
413
+ if (!body.id) return sendError(res, 400, 'id required');
414
+ deleteArchivingRule(body.id);
415
+ sendJSON(res, 200, { ok: true });
416
+ } catch (e) { sendError(res, 500, e.message); }
417
+ });
418
+
419
+ router.post('/api/imap/signatures/delete', async (req, res) => {
420
+ try {
421
+ const body = await parseBody(req);
422
+ const { deleteSignature } = await import('../../services/email-db.mjs');
423
+ if (!body.id) return sendError(res, 400, 'id required');
424
+ deleteSignature(body.id);
425
+ sendJSON(res, 200, { ok: true });
426
+ } catch (e) { sendError(res, 500, e.message); }
427
+ });
428
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Google OAuth routes + Microsoft OAuth
3
+ */
4
+
5
+ import { sendJSON, sendError, parseBody } from '../index.mjs';
6
+ import { loadConfig } from '../../config.mjs';
7
+
8
+ let _googleOAuthPending = null;
9
+
10
+ export function register(router) {
11
+ router.post('/api/google/auth', async (req, res) => {
12
+ try {
13
+ const { buildAuthUrl } = await import('../../services/google-oauth.mjs');
14
+ const config = loadConfig();
15
+ const host = req.headers['host'] || 'localhost:3847';
16
+ const redirectUri = `http://${host}/api/google/callback`;
17
+ const { url, verifier, state } = buildAuthUrl(config, redirectUri);
18
+ _googleOAuthPending = { verifier, state, redirectUri };
19
+ sendJSON(res, 200, { ok: true, url });
20
+ } catch (e) { sendError(res, 500, e.message); }
21
+ });
22
+
23
+ router.get('/api/google/callback', async (req, res) => {
24
+ const url = new URL(req.url, 'http://localhost');
25
+ const code = url.searchParams.get('code');
26
+ const state = url.searchParams.get('state');
27
+ if (!code || !_googleOAuthPending || _googleOAuthPending.state !== state) {
28
+ res.writeHead(400, { 'Content-Type': 'text/html' });
29
+ res.end('<html><body><h2>OAuth error: invalid state or missing code.</h2><p>Please try again from the NHA UI.</p></body></html>');
30
+ return;
31
+ }
32
+ try {
33
+ const { exchangeCodeFromUI } = await import('../../services/google-oauth.mjs');
34
+ const config = loadConfig();
35
+ const { email } = await exchangeCodeFromUI(config, code, _googleOAuthPending.verifier, _googleOAuthPending.redirectUri);
36
+ _googleOAuthPending = null;
37
+ res.writeHead(200, { 'Content-Type': 'text/html' });
38
+ res.end(`<html><body style="font-family:sans-serif;text-align:center;padding:60px;background:#0a0a0a;color:#fff"><h2 style="color:#22c55e">&#10003; Google Connected!</h2><p style="color:#aaa">Signed in as <strong>${email}</strong></p><p style="color:#aaa">You can close this tab and return to NHA.</p><script>setTimeout(function(){window.close()},3000)</script></body></html>`);
39
+ } catch (e) {
40
+ _googleOAuthPending = null;
41
+ res.writeHead(200, { 'Content-Type': 'text/html' });
42
+ res.end(`<html><body style="font-family:sans-serif;text-align:center;padding:60px;background:#0a0a0a;color:#fff"><h2 style="color:#ef4444">&#10007; Error</h2><p style="color:#aaa">${e.message}</p></body></html>`);
43
+ }
44
+ });
45
+
46
+ router.post('/api/microsoft/auth', async (req, res) => {
47
+ try {
48
+ const { buildMicrosoftAuthUrl } = await import('../../services/microsoft-oauth.mjs');
49
+ const config = loadConfig();
50
+ const host = req.headers['host'] || 'localhost:3847';
51
+ const redirectUri = `http://${host}/api/microsoft/callback`;
52
+ const { url, state } = buildMicrosoftAuthUrl(config, redirectUri);
53
+ res._microsoftState = state;
54
+ res._microsoftRedirectUri = redirectUri;
55
+ sendJSON(res, 200, { ok: true, url });
56
+ } catch (e) { sendError(res, 500, e.message); }
57
+ });
58
+ }