n8n-nodes-jmap 0.1.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,466 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JMAP_CAPABILITIES = void 0;
4
+ exports.getJmapSession = getJmapSession;
5
+ exports.jmapApiRequest = jmapApiRequest;
6
+ exports.getPrimaryAccountId = getPrimaryAccountId;
7
+ exports.getMailboxes = getMailboxes;
8
+ exports.findMailboxByName = findMailboxByName;
9
+ exports.findMailboxByRole = findMailboxByRole;
10
+ exports.queryEmails = queryEmails;
11
+ exports.getEmails = getEmails;
12
+ exports.sendEmail = sendEmail;
13
+ exports.createDraft = createDraft;
14
+ exports.getIdentities = getIdentities;
15
+ exports.updateEmailKeywords = updateEmailKeywords;
16
+ exports.moveEmail = moveEmail;
17
+ exports.addLabel = addLabel;
18
+ exports.removeLabel = removeLabel;
19
+ exports.getLabels = getLabels;
20
+ exports.deleteEmails = deleteEmails;
21
+ exports.getThreads = getThreads;
22
+ exports.downloadBlob = downloadBlob;
23
+ const n8n_workflow_1 = require("n8n-workflow");
24
+ // Standard JMAP capabilities
25
+ exports.JMAP_CAPABILITIES = {
26
+ CORE: 'urn:ietf:params:jmap:core',
27
+ MAIL: 'urn:ietf:params:jmap:mail',
28
+ SUBMISSION: 'urn:ietf:params:jmap:submission',
29
+ VACATION_RESPONSE: 'urn:ietf:params:jmap:vacationresponse',
30
+ JAMES_SHARES: 'urn:apache:james:params:jmap:mail:shares',
31
+ JAMES_QUOTA: 'urn:apache:james:params:jmap:mail:quota',
32
+ };
33
+ /**
34
+ * Get the authentication type from node parameters
35
+ */
36
+ function getAuthType(context) {
37
+ try {
38
+ return context.getNodeParameter('authentication', 0);
39
+ }
40
+ catch {
41
+ return 'jmapOAuth2Api'; // Default to OAuth2
42
+ }
43
+ }
44
+ /**
45
+ * Get JMAP server URL based on credential type
46
+ */
47
+ async function getServerUrl(context) {
48
+ const authType = getAuthType(context);
49
+ if (authType === 'jmapOAuth2Api') {
50
+ const credentials = await context.getCredentials('jmapOAuth2Api');
51
+ return credentials.jmapServerUrl.replace(/\/$/, '');
52
+ }
53
+ else {
54
+ const credentials = await context.getCredentials('jmapApi');
55
+ return credentials.serverUrl.replace(/\/$/, '');
56
+ }
57
+ }
58
+ /**
59
+ * Make an authenticated JMAP request
60
+ */
61
+ async function makeJmapRequest(context, method, endpoint, body) {
62
+ const authType = getAuthType(context);
63
+ const serverUrl = await getServerUrl(context);
64
+ const url = endpoint.startsWith('http') ? endpoint : `${serverUrl}${endpoint}`;
65
+ if (authType === 'jmapOAuth2Api') {
66
+ // Use n8n's built-in OAuth2 authentication
67
+ const response = await context.helpers.requestWithAuthentication.call(context, 'jmapOAuth2Api', {
68
+ method,
69
+ url,
70
+ headers: {
71
+ 'Content-Type': 'application/json',
72
+ Accept: 'application/json',
73
+ },
74
+ body,
75
+ json: true,
76
+ });
77
+ return response;
78
+ }
79
+ else {
80
+ // Use Basic Auth or Bearer Token
81
+ const credentials = await context.getCredentials('jmapApi');
82
+ const authMethod = credentials.authMethod || 'basicAuth';
83
+ const options = {
84
+ method,
85
+ uri: url,
86
+ headers: {
87
+ 'Content-Type': 'application/json',
88
+ Accept: 'application/json',
89
+ },
90
+ body,
91
+ json: true,
92
+ };
93
+ if (authMethod === 'basicAuth') {
94
+ options.auth = {
95
+ user: credentials.email,
96
+ pass: credentials.password,
97
+ };
98
+ }
99
+ else if (authMethod === 'bearerToken') {
100
+ options.headers = {
101
+ ...options.headers,
102
+ Authorization: `Bearer ${credentials.accessToken}`,
103
+ };
104
+ }
105
+ const response = await context.helpers.request(options);
106
+ return response;
107
+ }
108
+ }
109
+ /**
110
+ * Get JMAP session from the server
111
+ */
112
+ async function getJmapSession() {
113
+ try {
114
+ const response = await makeJmapRequest(this, 'GET', '/session');
115
+ return response;
116
+ }
117
+ catch (error) {
118
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), error, {
119
+ message: 'Failed to get JMAP session',
120
+ });
121
+ }
122
+ }
123
+ /**
124
+ * Make a JMAP API request
125
+ */
126
+ async function jmapApiRequest(methodCalls, using = [exports.JMAP_CAPABILITIES.CORE, exports.JMAP_CAPABILITIES.MAIL]) {
127
+ const body = {
128
+ using,
129
+ methodCalls,
130
+ };
131
+ try {
132
+ const response = await makeJmapRequest(this, 'POST', '', body);
133
+ return response;
134
+ }
135
+ catch (error) {
136
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), error, {
137
+ message: 'JMAP API request failed',
138
+ });
139
+ }
140
+ }
141
+ /**
142
+ * Get the primary account ID for mail
143
+ */
144
+ async function getPrimaryAccountId() {
145
+ const session = await getJmapSession.call(this);
146
+ const mailCapability = exports.JMAP_CAPABILITIES.MAIL;
147
+ if (session.primaryAccounts && session.primaryAccounts[mailCapability]) {
148
+ return session.primaryAccounts[mailCapability];
149
+ }
150
+ const accountIds = Object.keys(session.accounts);
151
+ if (accountIds.length > 0) {
152
+ return accountIds[0];
153
+ }
154
+ throw new Error('No JMAP account found');
155
+ }
156
+ /**
157
+ * Get all mailboxes for an account
158
+ */
159
+ async function getMailboxes(accountId) {
160
+ const response = await jmapApiRequest.call(this, [['Mailbox/get', { accountId }, 'c1']]);
161
+ const methodResponse = response.methodResponses[0];
162
+ if (methodResponse[0] === 'Mailbox/get') {
163
+ return methodResponse[1].list;
164
+ }
165
+ throw new Error('Failed to get mailboxes');
166
+ }
167
+ /**
168
+ * Find a mailbox by name
169
+ */
170
+ async function findMailboxByName(accountId, name) {
171
+ const mailboxes = await getMailboxes.call(this, accountId);
172
+ return mailboxes.find((mb) => mb.name === name);
173
+ }
174
+ /**
175
+ * Find a mailbox by role
176
+ */
177
+ async function findMailboxByRole(accountId, role) {
178
+ const mailboxes = await getMailboxes.call(this, accountId);
179
+ return mailboxes.find((mb) => mb.role === role);
180
+ }
181
+ /**
182
+ * Query emails with filters
183
+ */
184
+ async function queryEmails(accountId, filter = {}, sort = [{ property: 'receivedAt', isAscending: false }], limit = 50, position = 0) {
185
+ const response = await jmapApiRequest.call(this, [
186
+ [
187
+ 'Email/query',
188
+ { accountId, filter, sort, limit, position },
189
+ 'c1',
190
+ ],
191
+ ]);
192
+ const methodResponse = response.methodResponses[0];
193
+ if (methodResponse[0] === 'Email/query') {
194
+ const result = methodResponse[1];
195
+ return {
196
+ ids: result.ids,
197
+ total: result.total,
198
+ };
199
+ }
200
+ throw new Error('Failed to query emails');
201
+ }
202
+ /**
203
+ * Get emails by IDs
204
+ */
205
+ async function getEmails(accountId, ids, properties = [
206
+ 'id', 'blobId', 'threadId', 'mailboxIds', 'keywords', 'size',
207
+ 'receivedAt', 'from', 'to', 'cc', 'bcc', 'replyTo', 'subject',
208
+ 'sentAt', 'hasAttachment', 'preview', 'bodyStructure', 'bodyValues',
209
+ 'textBody', 'htmlBody', 'attachments',
210
+ ], fetchTextBodyValues = true, fetchHTMLBodyValues = true) {
211
+ const response = await jmapApiRequest.call(this, [
212
+ [
213
+ 'Email/get',
214
+ {
215
+ accountId,
216
+ ids,
217
+ properties,
218
+ fetchTextBodyValues,
219
+ fetchHTMLBodyValues,
220
+ maxBodyValueBytes: 1048576,
221
+ },
222
+ 'c1',
223
+ ],
224
+ ]);
225
+ const methodResponse = response.methodResponses[0];
226
+ if (methodResponse[0] === 'Email/get') {
227
+ return methodResponse[1].list;
228
+ }
229
+ throw new Error('Failed to get emails');
230
+ }
231
+ /**
232
+ * Create and send an email
233
+ */
234
+ async function sendEmail(accountId, email, identityId) {
235
+ const draftsMailbox = await findMailboxByRole.call(this, accountId, 'drafts');
236
+ if (!draftsMailbox) {
237
+ throw new Error('Drafts mailbox not found');
238
+ }
239
+ const emailCreate = {
240
+ ...email,
241
+ mailboxIds: { [draftsMailbox.id]: true },
242
+ keywords: { $draft: true },
243
+ };
244
+ const response = await jmapApiRequest.call(this, [
245
+ ['Email/set', { accountId, create: { draft: emailCreate } }, 'c1'],
246
+ [
247
+ 'EmailSubmission/set',
248
+ {
249
+ accountId,
250
+ create: { send: { emailId: '#draft', identityId } },
251
+ onSuccessDestroyEmail: ['#send'],
252
+ },
253
+ 'c2',
254
+ ],
255
+ ], [exports.JMAP_CAPABILITIES.CORE, exports.JMAP_CAPABILITIES.MAIL, exports.JMAP_CAPABILITIES.SUBMISSION]);
256
+ for (const methodResponse of response.methodResponses) {
257
+ if (methodResponse[0] === 'error') {
258
+ throw new Error(`JMAP error: ${JSON.stringify(methodResponse[1])}`);
259
+ }
260
+ }
261
+ return response.methodResponses[1][1];
262
+ }
263
+ /**
264
+ * Create a draft email
265
+ */
266
+ async function createDraft(accountId, email) {
267
+ const draftsMailbox = await findMailboxByRole.call(this, accountId, 'drafts');
268
+ if (!draftsMailbox) {
269
+ throw new Error('Drafts mailbox not found');
270
+ }
271
+ const emailCreate = {
272
+ ...email,
273
+ mailboxIds: { [draftsMailbox.id]: true },
274
+ keywords: { $draft: true },
275
+ };
276
+ const response = await jmapApiRequest.call(this, [['Email/set', { accountId, create: { draft: emailCreate } }, 'c1']]);
277
+ const methodResponse = response.methodResponses[0];
278
+ if (methodResponse[0] === 'error') {
279
+ throw new Error(`JMAP error: ${JSON.stringify(methodResponse[1])}`);
280
+ }
281
+ if (methodResponse[0] === 'Email/set') {
282
+ const result = methodResponse[1];
283
+ const created = result.created;
284
+ if (created && created.draft) {
285
+ return created.draft;
286
+ }
287
+ }
288
+ return methodResponse[1];
289
+ }
290
+ /**
291
+ * Get identities
292
+ */
293
+ async function getIdentities(accountId) {
294
+ const response = await jmapApiRequest.call(this, [['Identity/get', { accountId }, 'c1']], [exports.JMAP_CAPABILITIES.CORE, exports.JMAP_CAPABILITIES.SUBMISSION]);
295
+ const methodResponse = response.methodResponses[0];
296
+ if (methodResponse[0] === 'Identity/get') {
297
+ return methodResponse[1].list;
298
+ }
299
+ throw new Error('Failed to get identities');
300
+ }
301
+ /**
302
+ * Update email keywords
303
+ */
304
+ async function updateEmailKeywords(accountId, emailId, keywords) {
305
+ const response = await jmapApiRequest.call(this, [['Email/set', { accountId, update: { [emailId]: { keywords } } }, 'c1']]);
306
+ const methodResponse = response.methodResponses[0];
307
+ if (methodResponse[0] === 'Email/set') {
308
+ return methodResponse[1];
309
+ }
310
+ throw new Error('Failed to update email');
311
+ }
312
+ /**
313
+ * Move email to a different mailbox
314
+ */
315
+ async function moveEmail(accountId, emailId, targetMailboxId) {
316
+ const response = await jmapApiRequest.call(this, [
317
+ [
318
+ 'Email/set',
319
+ {
320
+ accountId,
321
+ update: { [emailId]: { mailboxIds: { [targetMailboxId]: true } } },
322
+ },
323
+ 'c1',
324
+ ],
325
+ ]);
326
+ const methodResponse = response.methodResponses[0];
327
+ if (methodResponse[0] === 'Email/set') {
328
+ return methodResponse[1];
329
+ }
330
+ throw new Error('Failed to move email');
331
+ }
332
+ /**
333
+ * Add a label (mailbox) to an email
334
+ */
335
+ async function addLabel(accountId, emailId, mailboxId) {
336
+ const response = await jmapApiRequest.call(this, [
337
+ [
338
+ 'Email/set',
339
+ {
340
+ accountId,
341
+ update: { [emailId]: { [`mailboxIds/${mailboxId}`]: true } },
342
+ },
343
+ 'c1',
344
+ ],
345
+ ]);
346
+ const methodResponse = response.methodResponses[0];
347
+ if (methodResponse[0] === 'Email/set') {
348
+ return methodResponse[1];
349
+ }
350
+ throw new Error('Failed to add label');
351
+ }
352
+ /**
353
+ * Remove a label (mailbox) from an email
354
+ */
355
+ async function removeLabel(accountId, emailId, mailboxId) {
356
+ const response = await jmapApiRequest.call(this, [
357
+ [
358
+ 'Email/set',
359
+ {
360
+ accountId,
361
+ update: { [emailId]: { [`mailboxIds/${mailboxId}`]: null } },
362
+ },
363
+ 'c1',
364
+ ],
365
+ ]);
366
+ const methodResponse = response.methodResponses[0];
367
+ if (methodResponse[0] === 'Email/set') {
368
+ return methodResponse[1];
369
+ }
370
+ throw new Error('Failed to remove label');
371
+ }
372
+ /**
373
+ * Get labels (mailboxes) for an email with their names
374
+ */
375
+ async function getLabels(accountId, emailId) {
376
+ const emails = await getEmails.call(this, accountId, [emailId], ['id', 'mailboxIds']);
377
+ if (emails.length === 0) {
378
+ throw new Error('Email not found');
379
+ }
380
+ const email = emails[0];
381
+ const mailboxIds = email.mailboxIds;
382
+ if (!mailboxIds || Object.keys(mailboxIds).length === 0) {
383
+ return [];
384
+ }
385
+ const allMailboxes = await getMailboxes.call(this, accountId);
386
+ const labels = [];
387
+ for (const mailboxId of Object.keys(mailboxIds)) {
388
+ const mailbox = allMailboxes.find((mb) => mb.id === mailboxId);
389
+ if (mailbox) {
390
+ labels.push({
391
+ id: mailbox.id,
392
+ name: mailbox.name,
393
+ role: mailbox.role || null,
394
+ totalEmails: mailbox.totalEmails,
395
+ unreadEmails: mailbox.unreadEmails,
396
+ });
397
+ }
398
+ else {
399
+ labels.push({ id: mailboxId, name: null, role: null });
400
+ }
401
+ }
402
+ return labels;
403
+ }
404
+ /**
405
+ * Delete emails
406
+ */
407
+ async function deleteEmails(accountId, emailIds) {
408
+ const response = await jmapApiRequest.call(this, [['Email/set', { accountId, destroy: emailIds }, 'c1']]);
409
+ const methodResponse = response.methodResponses[0];
410
+ if (methodResponse[0] === 'Email/set') {
411
+ return methodResponse[1];
412
+ }
413
+ throw new Error('Failed to delete emails');
414
+ }
415
+ /**
416
+ * Get threads
417
+ */
418
+ async function getThreads(accountId, ids) {
419
+ const response = await jmapApiRequest.call(this, [['Thread/get', { accountId, ids }, 'c1']]);
420
+ const methodResponse = response.methodResponses[0];
421
+ if (methodResponse[0] === 'Thread/get') {
422
+ return methodResponse[1].list;
423
+ }
424
+ throw new Error('Failed to get threads');
425
+ }
426
+ /**
427
+ * Download an attachment blob
428
+ */
429
+ async function downloadBlob(accountId, blobId, name, type) {
430
+ const session = await getJmapSession.call(this);
431
+ const authType = getAuthType(this);
432
+ let downloadUrl = session.downloadUrl
433
+ .replace('{accountId}', accountId)
434
+ .replace('{blobId}', blobId)
435
+ .replace('{name}', encodeURIComponent(name))
436
+ .replace('{type}', encodeURIComponent(type));
437
+ if (authType === 'jmapOAuth2Api') {
438
+ const response = await this.helpers.requestWithAuthentication.call(this, 'jmapOAuth2Api', {
439
+ method: 'GET',
440
+ url: downloadUrl,
441
+ encoding: null,
442
+ });
443
+ return response;
444
+ }
445
+ else {
446
+ const credentials = await this.getCredentials('jmapApi');
447
+ const authMethod = credentials.authMethod || 'basicAuth';
448
+ const options = {
449
+ method: 'GET',
450
+ uri: downloadUrl,
451
+ encoding: null,
452
+ };
453
+ if (authMethod === 'basicAuth') {
454
+ options.auth = {
455
+ user: credentials.email,
456
+ pass: credentials.password,
457
+ };
458
+ }
459
+ else if (authMethod === 'bearerToken') {
460
+ options.headers = {
461
+ Authorization: `Bearer ${credentials.accessToken}`,
462
+ };
463
+ }
464
+ return await this.helpers.request(options);
465
+ }
466
+ }
@@ -0,0 +1,11 @@
1
+ import { IExecuteFunctions, ILoadOptionsFunctions, INodeExecutionData, INodePropertyOptions, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ export declare class Jmap implements INodeType {
3
+ description: INodeTypeDescription;
4
+ methods: {
5
+ loadOptions: {
6
+ getMailboxes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
7
+ getIdentities(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
8
+ };
9
+ };
10
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
11
+ }