n8n-nodes-jmap 0.1.1 → 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',
@@ -459,3 +498,257 @@ async function downloadBlob(accountId, blobId, name, type) {
459
498
  return Buffer.from(response);
460
499
  }
461
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
+ });
604
+ }
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
+ }
686
+ }
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++;
752
+ }
753
+ return results;
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.1",
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
  }