n8n-nodes-jmap 0.1.1 → 0.2.1

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[]>;
@@ -20,6 +20,7 @@ exports.getLabels = getLabels;
20
20
  exports.deleteEmails = deleteEmails;
21
21
  exports.getThreads = getThreads;
22
22
  exports.downloadBlob = downloadBlob;
23
+ exports.getAttachments = getAttachments;
23
24
  const n8n_workflow_1 = require("n8n-workflow");
24
25
  // Standard JMAP capabilities
25
26
  exports.JMAP_CAPABILITIES = {
@@ -459,3 +460,170 @@ async function downloadBlob(accountId, blobId, name, type) {
459
460
  return Buffer.from(response);
460
461
  }
461
462
  }
463
+ /**
464
+ * Check if a MIME type matches a filter pattern
465
+ */
466
+ function matchesMimeType(mimeType, filter) {
467
+ const normalizedMime = mimeType.toLowerCase();
468
+ const normalizedFilter = filter.toLowerCase().trim();
469
+ if (normalizedFilter.endsWith('/*')) {
470
+ const prefix = normalizedFilter.slice(0, -1);
471
+ return normalizedMime.startsWith(prefix);
472
+ }
473
+ return normalizedMime === normalizedFilter;
474
+ }
475
+ /**
476
+ * Check if a MIME type is a ZIP archive
477
+ */
478
+ function isZipMimeType(mimeType) {
479
+ const zipTypes = [
480
+ 'application/zip',
481
+ 'application/x-zip-compressed',
482
+ 'application/x-zip',
483
+ ];
484
+ return zipTypes.includes(mimeType.toLowerCase());
485
+ }
486
+ /**
487
+ * Check if a MIME type is a tar.gz archive
488
+ */
489
+ function isTarGzMimeType(mimeType) {
490
+ const tarGzTypes = [
491
+ 'application/gzip',
492
+ 'application/x-gzip',
493
+ 'application/x-tar',
494
+ 'application/x-compressed-tar',
495
+ ];
496
+ return tarGzTypes.includes(mimeType.toLowerCase());
497
+ }
498
+ /**
499
+ * Check if filename suggests tar.gz
500
+ */
501
+ function isTarGzFilename(filename) {
502
+ const lower = filename.toLowerCase();
503
+ return lower.endsWith('.tar.gz') || lower.endsWith('.tgz');
504
+ }
505
+ /**
506
+ * Extract files from a ZIP archive
507
+ * Note: Archive extraction is not yet available (planned for future release with self-hosted support)
508
+ * Returns empty array, causing the archive to be returned as-is
509
+ */
510
+ function extractZip(_buffer) {
511
+ // Archive extraction not yet implemented
512
+ // Will be available in a future release for self-hosted n8n
513
+ return [];
514
+ }
515
+ /**
516
+ * Extract files from a tar.gz archive
517
+ * Note: Archive extraction is not yet available (planned for future release with self-hosted support)
518
+ * Returns empty array, causing the archive to be returned as-is
519
+ */
520
+ async function extractTarGz(_buffer) {
521
+ // Archive extraction not yet implemented
522
+ // Will be available in a future release for self-hosted n8n
523
+ return [];
524
+ }
525
+ /**
526
+ * Get attachments from an email and return them as binary data
527
+ */
528
+ async function getAttachments(accountId, emailId, options = {}) {
529
+ const { extractArchives = false, includeInline = false, mimeTypeFilter = '' } = options;
530
+ // Get email with attachments metadata
531
+ const emails = await getEmails.call(this, accountId, [emailId], [
532
+ 'id',
533
+ 'subject',
534
+ 'attachments',
535
+ ]);
536
+ if (emails.length === 0) {
537
+ throw new Error(`Email with ID ${emailId} not found`);
538
+ }
539
+ const email = emails[0];
540
+ const attachments = email.attachments || [];
541
+ if (attachments.length === 0) {
542
+ return [];
543
+ }
544
+ // Parse MIME type filters
545
+ const mimeFilters = mimeTypeFilter
546
+ ? mimeTypeFilter.split(',').map((f) => f.trim()).filter((f) => f)
547
+ : [];
548
+ const results = [];
549
+ let attachmentIndex = 0;
550
+ for (const attachment of attachments) {
551
+ // Filter by inline status
552
+ if (attachment.isInline && !includeInline) {
553
+ continue;
554
+ }
555
+ // Filter by MIME type
556
+ if (mimeFilters.length > 0) {
557
+ const matches = mimeFilters.some((filter) => matchesMimeType(attachment.type, filter));
558
+ if (!matches) {
559
+ continue;
560
+ }
561
+ }
562
+ // Download the attachment
563
+ const buffer = await downloadBlob.call(this, accountId, attachment.blobId, attachment.name, attachment.type);
564
+ // Check if this is an archive that should be extracted
565
+ const isZip = isZipMimeType(attachment.type);
566
+ const isTarGz = isTarGzMimeType(attachment.type) || isTarGzFilename(attachment.name);
567
+ if (extractArchives && (isZip || isTarGz)) {
568
+ // Extract archive contents
569
+ let extractedFiles = [];
570
+ try {
571
+ if (isZip) {
572
+ extractedFiles = extractZip(buffer);
573
+ }
574
+ else if (isTarGz) {
575
+ extractedFiles = await extractTarGz(buffer);
576
+ }
577
+ }
578
+ catch (error) {
579
+ // If extraction fails, return the archive as-is
580
+ extractedFiles = [];
581
+ }
582
+ if (extractedFiles.length > 0) {
583
+ // Return each extracted file as a separate item
584
+ for (const file of extractedFiles) {
585
+ const binaryData = await this.helpers.prepareBinaryData(file.data, file.name, file.mimeType);
586
+ results.push({
587
+ json: {
588
+ emailId: email.id,
589
+ emailSubject: email.subject,
590
+ attachmentIndex,
591
+ originalFileName: attachment.name,
592
+ fileName: file.name,
593
+ mimeType: file.mimeType,
594
+ fileSize: file.data.length,
595
+ wasExtractedFromArchive: true,
596
+ sourceArchiveName: attachment.name,
597
+ },
598
+ binary: {
599
+ file: binaryData,
600
+ },
601
+ });
602
+ attachmentIndex++;
603
+ }
604
+ continue;
605
+ }
606
+ // If extraction failed or no files, fall through to return the archive as-is
607
+ }
608
+ // Return the attachment as-is (not an archive, or extraction disabled/failed)
609
+ const binaryData = await this.helpers.prepareBinaryData(buffer, attachment.name, attachment.type);
610
+ results.push({
611
+ json: {
612
+ emailId: email.id,
613
+ emailSubject: email.subject,
614
+ attachmentIndex,
615
+ originalFileName: attachment.name,
616
+ fileName: attachment.name,
617
+ mimeType: attachment.type,
618
+ fileSize: attachment.size,
619
+ wasExtractedFromArchive: false,
620
+ sourceArchiveName: null,
621
+ },
622
+ binary: {
623
+ file: binaryData,
624
+ },
625
+ });
626
+ attachmentIndex++;
627
+ }
628
+ return results;
629
+ }
@@ -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 (Coming Soon)',
434
+ name: 'extractArchives',
435
+ type: 'boolean',
436
+ default: false,
437
+ description: 'Extract ZIP and tar.gz archives. Note: This feature is planned for a future release and currently returns archives as-is.',
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.1",
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",