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.
|
|
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",
|