n8n-nodes-jmap 0.1.0 → 0.2.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.
@@ -1,4 +1,4 @@
1
- import type { IExecuteFunctions, ILoadOptionsFunctions, IPollFunctions, IDataObject } from 'n8n-workflow';
1
+ import type { IExecuteFunctions, ILoadOptionsFunctions, IPollFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
2
2
  export interface IJmapSession {
3
3
  accounts: {
4
4
  [key: string]: IJmapAccount;
@@ -119,3 +119,15 @@ export declare function getThreads(this: IExecuteFunctions | ILoadOptionsFunctio
119
119
  * Download an attachment blob
120
120
  */
121
121
  export declare function downloadBlob(this: IExecuteFunctions, accountId: string, blobId: string, name: string, type: string): Promise<Buffer>;
122
+ /**
123
+ * Interface for attachment options
124
+ */
125
+ export interface IAttachmentOptions {
126
+ extractArchives?: boolean;
127
+ includeInline?: boolean;
128
+ mimeTypeFilter?: string;
129
+ }
130
+ /**
131
+ * Get attachments from an email and return them as binary data
132
+ */
133
+ export declare function getAttachments(this: IExecuteFunctions, accountId: string, emailId: string, options?: IAttachmentOptions): Promise<INodeExecutionData[]>;
@@ -1,4 +1,40 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
2
38
  Object.defineProperty(exports, "__esModule", { value: true });
3
39
  exports.JMAP_CAPABILITIES = void 0;
4
40
  exports.getJmapSession = getJmapSession;
@@ -20,7 +56,10 @@ exports.getLabels = getLabels;
20
56
  exports.deleteEmails = deleteEmails;
21
57
  exports.getThreads = getThreads;
22
58
  exports.downloadBlob = downloadBlob;
59
+ exports.getAttachments = getAttachments;
23
60
  const n8n_workflow_1 = require("n8n-workflow");
61
+ const adm_zip_1 = __importDefault(require("adm-zip"));
62
+ const tar = __importStar(require("tar"));
24
63
  // Standard JMAP capabilities
25
64
  exports.JMAP_CAPABILITIES = {
26
65
  CORE: 'urn:ietf:params:jmap:core',
@@ -64,7 +103,7 @@ async function makeJmapRequest(context, method, endpoint, body) {
64
103
  const url = endpoint.startsWith('http') ? endpoint : `${serverUrl}${endpoint}`;
65
104
  if (authType === 'jmapOAuth2Api') {
66
105
  // Use n8n's built-in OAuth2 authentication
67
- const response = await context.helpers.requestWithAuthentication.call(context, 'jmapOAuth2Api', {
106
+ const response = await context.helpers.httpRequestWithAuthentication.call(context, 'jmapOAuth2Api', {
68
107
  method,
69
108
  url,
70
109
  headers: {
@@ -80,29 +119,25 @@ async function makeJmapRequest(context, method, endpoint, body) {
80
119
  // Use Basic Auth or Bearer Token
81
120
  const credentials = await context.getCredentials('jmapApi');
82
121
  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,
122
+ const headers = {
123
+ 'Content-Type': 'application/json',
124
+ Accept: 'application/json',
92
125
  };
93
126
  if (authMethod === 'basicAuth') {
94
- options.auth = {
95
- user: credentials.email,
96
- pass: credentials.password,
97
- };
127
+ const authString = Buffer.from(`${credentials.email}:${credentials.password}`).toString('base64');
128
+ headers.Authorization = `Basic ${authString}`;
98
129
  }
99
130
  else if (authMethod === 'bearerToken') {
100
- options.headers = {
101
- ...options.headers,
102
- Authorization: `Bearer ${credentials.accessToken}`,
103
- };
131
+ headers.Authorization = `Bearer ${credentials.accessToken}`;
104
132
  }
105
- const response = await context.helpers.request(options);
133
+ const options = {
134
+ method,
135
+ url,
136
+ headers,
137
+ body,
138
+ json: true,
139
+ };
140
+ const response = await context.helpers.httpRequest(options);
106
141
  return response;
107
142
  }
108
143
  }
@@ -435,32 +470,285 @@ async function downloadBlob(accountId, blobId, name, type) {
435
470
  .replace('{name}', encodeURIComponent(name))
436
471
  .replace('{type}', encodeURIComponent(type));
437
472
  if (authType === 'jmapOAuth2Api') {
438
- const response = await this.helpers.requestWithAuthentication.call(this, 'jmapOAuth2Api', {
473
+ const response = await this.helpers.httpRequestWithAuthentication.call(this, 'jmapOAuth2Api', {
439
474
  method: 'GET',
440
475
  url: downloadUrl,
441
- encoding: null,
476
+ encoding: 'arraybuffer',
442
477
  });
443
- return response;
478
+ return Buffer.from(response);
444
479
  }
445
480
  else {
446
481
  const credentials = await this.getCredentials('jmapApi');
447
482
  const authMethod = credentials.authMethod || 'basicAuth';
483
+ const headers = {};
484
+ if (authMethod === 'basicAuth') {
485
+ const authString = Buffer.from(`${credentials.email}:${credentials.password}`).toString('base64');
486
+ headers.Authorization = `Basic ${authString}`;
487
+ }
488
+ else if (authMethod === 'bearerToken') {
489
+ headers.Authorization = `Bearer ${credentials.accessToken}`;
490
+ }
448
491
  const options = {
449
492
  method: 'GET',
450
- uri: downloadUrl,
451
- encoding: null,
493
+ url: downloadUrl,
494
+ headers,
495
+ encoding: 'arraybuffer',
452
496
  };
453
- if (authMethod === 'basicAuth') {
454
- options.auth = {
455
- user: credentials.email,
456
- pass: credentials.password,
457
- };
497
+ const response = await this.helpers.httpRequest(options);
498
+ return Buffer.from(response);
499
+ }
500
+ }
501
+ /**
502
+ * Check if a MIME type matches a filter pattern
503
+ */
504
+ function matchesMimeType(mimeType, filter) {
505
+ const normalizedMime = mimeType.toLowerCase();
506
+ const normalizedFilter = filter.toLowerCase().trim();
507
+ if (normalizedFilter.endsWith('/*')) {
508
+ const prefix = normalizedFilter.slice(0, -1);
509
+ return normalizedMime.startsWith(prefix);
510
+ }
511
+ return normalizedMime === normalizedFilter;
512
+ }
513
+ /**
514
+ * Check if a MIME type is a ZIP archive
515
+ */
516
+ function isZipMimeType(mimeType) {
517
+ const zipTypes = [
518
+ 'application/zip',
519
+ 'application/x-zip-compressed',
520
+ 'application/x-zip',
521
+ ];
522
+ return zipTypes.includes(mimeType.toLowerCase());
523
+ }
524
+ /**
525
+ * Check if a MIME type is a tar.gz archive
526
+ */
527
+ function isTarGzMimeType(mimeType) {
528
+ const tarGzTypes = [
529
+ 'application/gzip',
530
+ 'application/x-gzip',
531
+ 'application/x-tar',
532
+ 'application/x-compressed-tar',
533
+ ];
534
+ return tarGzTypes.includes(mimeType.toLowerCase());
535
+ }
536
+ /**
537
+ * Check if filename suggests tar.gz
538
+ */
539
+ function isTarGzFilename(filename) {
540
+ const lower = filename.toLowerCase();
541
+ return lower.endsWith('.tar.gz') || lower.endsWith('.tgz');
542
+ }
543
+ /**
544
+ * Get file extension from filename
545
+ */
546
+ function getFileExtension(filename) {
547
+ const parts = filename.split('.');
548
+ if (parts.length > 1) {
549
+ return parts[parts.length - 1].toLowerCase();
550
+ }
551
+ return '';
552
+ }
553
+ /**
554
+ * Guess MIME type from filename extension
555
+ */
556
+ function guessMimeType(filename) {
557
+ const ext = getFileExtension(filename).toLowerCase();
558
+ const mimeTypes = {
559
+ pdf: 'application/pdf',
560
+ doc: 'application/msword',
561
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
562
+ xls: 'application/vnd.ms-excel',
563
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
564
+ ppt: 'application/vnd.ms-powerpoint',
565
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
566
+ txt: 'text/plain',
567
+ csv: 'text/csv',
568
+ json: 'application/json',
569
+ xml: 'application/xml',
570
+ html: 'text/html',
571
+ htm: 'text/html',
572
+ jpg: 'image/jpeg',
573
+ jpeg: 'image/jpeg',
574
+ png: 'image/png',
575
+ gif: 'image/gif',
576
+ svg: 'image/svg+xml',
577
+ webp: 'image/webp',
578
+ mp3: 'audio/mpeg',
579
+ wav: 'audio/wav',
580
+ mp4: 'video/mp4',
581
+ avi: 'video/x-msvideo',
582
+ zip: 'application/zip',
583
+ tar: 'application/x-tar',
584
+ gz: 'application/gzip',
585
+ };
586
+ return mimeTypes[ext] || 'application/octet-stream';
587
+ }
588
+ /**
589
+ * Extract files from a ZIP archive
590
+ */
591
+ function extractZip(buffer) {
592
+ const files = [];
593
+ const zip = new adm_zip_1.default(buffer);
594
+ const entries = zip.getEntries();
595
+ for (const entry of entries) {
596
+ if (!entry.isDirectory) {
597
+ const data = entry.getData();
598
+ const name = entry.entryName.split('/').pop() || entry.entryName;
599
+ files.push({
600
+ name,
601
+ data,
602
+ mimeType: guessMimeType(name),
603
+ });
458
604
  }
459
- else if (authMethod === 'bearerToken') {
460
- options.headers = {
461
- Authorization: `Bearer ${credentials.accessToken}`,
462
- };
605
+ }
606
+ return files;
607
+ }
608
+ /**
609
+ * Extract files from a tar.gz archive
610
+ */
611
+ async function extractTarGz(buffer) {
612
+ const files = [];
613
+ return new Promise((resolve, reject) => {
614
+ const chunks = new Map();
615
+ const parser = new tar.Parser();
616
+ parser.on('entry', (entry) => {
617
+ if (entry.type === 'File') {
618
+ const entryChunks = [];
619
+ entry.on('data', (chunk) => {
620
+ entryChunks.push(chunk);
621
+ });
622
+ entry.on('end', () => {
623
+ const name = entry.path.split('/').pop() || entry.path;
624
+ chunks.set(name, entryChunks);
625
+ });
626
+ }
627
+ else {
628
+ entry.resume();
629
+ }
630
+ });
631
+ parser.on('end', () => {
632
+ for (const [name, entryChunks] of chunks) {
633
+ const data = Buffer.concat(entryChunks);
634
+ files.push({
635
+ name,
636
+ data,
637
+ mimeType: guessMimeType(name),
638
+ });
639
+ }
640
+ resolve(files);
641
+ });
642
+ parser.on('error', reject);
643
+ const { Gunzip } = require('zlib');
644
+ const gunzip = new Gunzip();
645
+ gunzip.pipe(parser);
646
+ gunzip.write(buffer);
647
+ gunzip.end();
648
+ });
649
+ }
650
+ /**
651
+ * Get attachments from an email and return them as binary data
652
+ */
653
+ async function getAttachments(accountId, emailId, options = {}) {
654
+ const { extractArchives = false, includeInline = false, mimeTypeFilter = '' } = options;
655
+ // Get email with attachments metadata
656
+ const emails = await getEmails.call(this, accountId, [emailId], [
657
+ 'id',
658
+ 'subject',
659
+ 'attachments',
660
+ ]);
661
+ if (emails.length === 0) {
662
+ throw new Error(`Email with ID ${emailId} not found`);
663
+ }
664
+ const email = emails[0];
665
+ const attachments = email.attachments || [];
666
+ if (attachments.length === 0) {
667
+ return [];
668
+ }
669
+ // Parse MIME type filters
670
+ const mimeFilters = mimeTypeFilter
671
+ ? mimeTypeFilter.split(',').map((f) => f.trim()).filter((f) => f)
672
+ : [];
673
+ const results = [];
674
+ let attachmentIndex = 0;
675
+ for (const attachment of attachments) {
676
+ // Filter by inline status
677
+ if (attachment.isInline && !includeInline) {
678
+ continue;
679
+ }
680
+ // Filter by MIME type
681
+ if (mimeFilters.length > 0) {
682
+ const matches = mimeFilters.some((filter) => matchesMimeType(attachment.type, filter));
683
+ if (!matches) {
684
+ continue;
685
+ }
463
686
  }
464
- return await this.helpers.request(options);
687
+ // Download the attachment
688
+ const buffer = await downloadBlob.call(this, accountId, attachment.blobId, attachment.name, attachment.type);
689
+ // Check if this is an archive that should be extracted
690
+ const isZip = isZipMimeType(attachment.type);
691
+ const isTarGz = isTarGzMimeType(attachment.type) || isTarGzFilename(attachment.name);
692
+ if (extractArchives && (isZip || isTarGz)) {
693
+ // Extract archive contents
694
+ let extractedFiles = [];
695
+ try {
696
+ if (isZip) {
697
+ extractedFiles = extractZip(buffer);
698
+ }
699
+ else if (isTarGz) {
700
+ extractedFiles = await extractTarGz(buffer);
701
+ }
702
+ }
703
+ catch (error) {
704
+ // If extraction fails, return the archive as-is
705
+ extractedFiles = [];
706
+ }
707
+ if (extractedFiles.length > 0) {
708
+ // Return each extracted file as a separate item
709
+ for (const file of extractedFiles) {
710
+ const binaryData = await this.helpers.prepareBinaryData(file.data, file.name, file.mimeType);
711
+ results.push({
712
+ json: {
713
+ emailId: email.id,
714
+ emailSubject: email.subject,
715
+ attachmentIndex,
716
+ originalFileName: attachment.name,
717
+ fileName: file.name,
718
+ mimeType: file.mimeType,
719
+ fileSize: file.data.length,
720
+ wasExtractedFromArchive: true,
721
+ sourceArchiveName: attachment.name,
722
+ },
723
+ binary: {
724
+ file: binaryData,
725
+ },
726
+ });
727
+ attachmentIndex++;
728
+ }
729
+ continue;
730
+ }
731
+ // If extraction failed or no files, fall through to return the archive as-is
732
+ }
733
+ // Return the attachment as-is (not an archive, or extraction disabled/failed)
734
+ const binaryData = await this.helpers.prepareBinaryData(buffer, attachment.name, attachment.type);
735
+ results.push({
736
+ json: {
737
+ emailId: email.id,
738
+ emailSubject: email.subject,
739
+ attachmentIndex,
740
+ originalFileName: attachment.name,
741
+ fileName: attachment.name,
742
+ mimeType: attachment.type,
743
+ fileSize: attachment.size,
744
+ wasExtractedFromArchive: false,
745
+ sourceArchiveName: null,
746
+ },
747
+ binary: {
748
+ file: binaryData,
749
+ },
750
+ });
751
+ attachmentIndex++;
465
752
  }
753
+ return results;
466
754
  }
@@ -115,6 +115,12 @@ class Jmap {
115
115
  description: 'Get an email by ID',
116
116
  action: 'Get an email',
117
117
  },
118
+ {
119
+ name: 'Get Attachments',
120
+ value: 'getAttachments',
121
+ description: 'Download attachments from an email',
122
+ action: 'Get attachments from an email',
123
+ },
118
124
  {
119
125
  name: 'Get Labels',
120
126
  value: 'getLabels',
@@ -221,7 +227,7 @@ class Jmap {
221
227
  default: 'get',
222
228
  },
223
229
  // ==================== EMAIL PARAMETERS ====================
224
- // Email ID (for get, delete, markAsRead, markAsUnread, move, reply, addLabel, removeLabel, getLabels)
230
+ // Email ID (for get, delete, markAsRead, markAsUnread, move, reply, addLabel, removeLabel, getLabels, getAttachments)
225
231
  {
226
232
  displayName: 'Email ID',
227
233
  name: 'emailId',
@@ -230,7 +236,7 @@ class Jmap {
230
236
  displayOptions: {
231
237
  show: {
232
238
  resource: ['email'],
233
- operation: ['get', 'delete', 'markAsRead', 'markAsUnread', 'move', 'reply', 'addLabel', 'removeLabel', 'getLabels'],
239
+ operation: ['get', 'delete', 'markAsRead', 'markAsUnread', 'move', 'reply', 'addLabel', 'removeLabel', 'getLabels', 'getAttachments'],
234
240
  },
235
241
  },
236
242
  default: '',
@@ -409,6 +415,44 @@ class Jmap {
409
415
  },
410
416
  ],
411
417
  },
418
+ // Options for getAttachments
419
+ {
420
+ displayName: 'Options',
421
+ name: 'attachmentOptions',
422
+ type: 'collection',
423
+ placeholder: 'Add Option',
424
+ default: {},
425
+ displayOptions: {
426
+ show: {
427
+ resource: ['email'],
428
+ operation: ['getAttachments'],
429
+ },
430
+ },
431
+ options: [
432
+ {
433
+ displayName: 'Extract Archives',
434
+ name: 'extractArchives',
435
+ type: 'boolean',
436
+ default: false,
437
+ description: 'Whether to extract ZIP and tar.gz archives and return their contents as individual files',
438
+ },
439
+ {
440
+ displayName: 'Include Inline Images',
441
+ name: 'includeInline',
442
+ type: 'boolean',
443
+ default: false,
444
+ description: 'Whether to include inline images (embedded in the email body) in addition to regular attachments',
445
+ },
446
+ {
447
+ displayName: 'Filter by MIME Type',
448
+ name: 'mimeTypeFilter',
449
+ type: 'string',
450
+ default: '',
451
+ placeholder: 'application/pdf, image/*',
452
+ description: 'Comma-separated list of MIME types to include. Supports wildcards (e.g., image/*). Leave empty to include all.',
453
+ },
454
+ ],
455
+ },
412
456
  // Limit for getMany
413
457
  {
414
458
  displayName: 'Limit',
@@ -576,6 +620,20 @@ class Jmap {
576
620
  count: labels.length,
577
621
  };
578
622
  }
623
+ if (operation === 'getAttachments') {
624
+ const emailId = this.getNodeParameter('emailId', i);
625
+ const attachmentOptions = this.getNodeParameter('attachmentOptions', i);
626
+ const attachmentResults = await GenericFunctions_1.getAttachments.call(this, accountId, emailId, attachmentOptions);
627
+ // getAttachments returns INodeExecutionData[] directly with binary data
628
+ // Add them directly to returnData instead of using responseData
629
+ for (const result of attachmentResults) {
630
+ returnData.push({
631
+ ...result,
632
+ pairedItem: { item: i },
633
+ });
634
+ }
635
+ continue; // Skip the normal responseData handling
636
+ }
579
637
  if (operation === 'getMany') {
580
638
  const mailbox = this.getNodeParameter('mailbox', i);
581
639
  const limit = this.getNodeParameter('limit', i);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-jmap",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "n8n community node for JMAP email protocol (RFC 8620/8621) - Works with Apache James, Twake Mail, Fastmail, and other JMAP-compatible servers",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",
@@ -46,7 +46,9 @@
46
46
  ]
47
47
  },
48
48
  "devDependencies": {
49
+ "@types/adm-zip": "^0.5.7",
49
50
  "@types/node": "^20.0.0",
51
+ "@types/tar": "^6.1.13",
50
52
  "@typescript-eslint/eslint-plugin": "^7.0.0",
51
53
  "@typescript-eslint/parser": "^7.0.0",
52
54
  "eslint": "^8.56.0",
@@ -57,5 +59,9 @@
57
59
  },
58
60
  "peerDependencies": {
59
61
  "n8n-workflow": "*"
62
+ },
63
+ "dependencies": {
64
+ "adm-zip": "^0.5.16",
65
+ "tar": "^7.5.2"
60
66
  }
61
67
  }