mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 04:04:06 -08:00
feat(Email Trigger (IMAP) Node): Migrate from imap-simple
to @n8n/imap
(#8899)
Co-authored-by: Jonathan Bennetts <jonathan.bennetts@gmail.com>
This commit is contained in:
parent
28261047c3
commit
9f87cc25a0
|
@ -26,8 +26,8 @@
|
|||
"start:tunnel": "./packages/cli/bin/n8n start --tunnel",
|
||||
"start:windows": "cd packages/cli/bin && n8n",
|
||||
"test": "turbo run test",
|
||||
"test:backend": "pnpm --filter=!@n8n/chat --filter=!n8n-design-system --filter=!n8n-editor-ui --filter=!n8n-nodes-base test",
|
||||
"test:nodes": "pnpm --filter=n8n-nodes-base test",
|
||||
"test:backend": "pnpm --filter=!@n8n/chat --filter=!n8n-design-system --filter=!n8n-editor-ui --filter=!n8n-nodes-base --filter=!@n8n/n8n-nodes-langchain test",
|
||||
"test:nodes": "pnpm --filter=n8n-nodes-base --filter=@n8n/n8n-nodes-langchain test",
|
||||
"test:frontend": "pnpm --filter=@n8n/chat --filter=n8n-design-system --filter=n8n-editor-ui test",
|
||||
"watch": "turbo run watch --parallel",
|
||||
"webhook": "./packages/cli/bin/n8n webhook",
|
||||
|
|
15
packages/@n8n/imap/.eslintrc.js
Normal file
15
packages/@n8n/imap/.eslintrc.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
const sharedOptions = require('@n8n_io/eslint-config/shared');
|
||||
|
||||
/**
|
||||
* @type {import('@types/eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/base'],
|
||||
|
||||
...sharedOptions(__dirname),
|
||||
|
||||
rules: {
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
'n8n-local-rules/no-plain-errors': 'off',
|
||||
},
|
||||
};
|
2
packages/@n8n/imap/jest.config.js
Normal file
2
packages/@n8n/imap/jest.config.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
/** @type {import('jest').Config} */
|
||||
module.exports = require('../../../jest.config');
|
34
packages/@n8n/imap/package.json
Normal file
34
packages/@n8n/imap/package.json
Normal file
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "@n8n/imap",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
"typecheck": "tsc",
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"format": "prettier --write . --ignore-path ../../../.prettierignore",
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch",
|
||||
"test": "echo \"Error: no test created yet\""
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"module": "src/index.ts",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"iconv-lite": "0.6.3",
|
||||
"imap": "0.8.19",
|
||||
"quoted-printable": "1.0.1",
|
||||
"utf8": "3.0.0",
|
||||
"uuencode": "0.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/imap": "^0.8.40",
|
||||
"@types/quoted-printable": "^1.0.2",
|
||||
"@types/utf8": "^3.0.3",
|
||||
"@types/uuencode": "^0.0.3"
|
||||
}
|
||||
}
|
235
packages/@n8n/imap/src/ImapSimple.ts
Normal file
235
packages/@n8n/imap/src/ImapSimple.ts
Normal file
|
@ -0,0 +1,235 @@
|
|||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
import { EventEmitter } from 'events';
|
||||
import type Imap from 'imap';
|
||||
import { type ImapMessage } from 'imap';
|
||||
import * as qp from 'quoted-printable';
|
||||
import * as iconvlite from 'iconv-lite';
|
||||
import * as utf8 from 'utf8';
|
||||
import * as uuencode from 'uuencode';
|
||||
|
||||
import { getMessage } from './helpers/getMessage';
|
||||
import type { Message, MessagePart } from './types';
|
||||
|
||||
const IMAP_EVENTS = ['alert', 'mail', 'expunge', 'uidvalidity', 'update', 'close', 'end'] as const;
|
||||
|
||||
export class ImapSimple extends EventEmitter {
|
||||
/** flag to determine whether we should suppress ECONNRESET from bubbling up to listener */
|
||||
private ending = false;
|
||||
|
||||
constructor(private readonly imap: Imap) {
|
||||
super();
|
||||
|
||||
// pass most node-imap `Connection` events through 1:1
|
||||
IMAP_EVENTS.forEach((event) => {
|
||||
this.imap.on(event, this.emit.bind(this, event));
|
||||
});
|
||||
|
||||
// special handling for `error` event
|
||||
this.imap.on('error', (e: Error & { code?: string }) => {
|
||||
// if .end() has been called and an 'ECONNRESET' error is received, don't bubble
|
||||
if (e && this.ending && e.code?.toUpperCase() === 'ECONNRESET') {
|
||||
return;
|
||||
}
|
||||
this.emit('error', e);
|
||||
});
|
||||
}
|
||||
|
||||
/** disconnect from the imap server */
|
||||
end(): void {
|
||||
// set state flag to suppress 'ECONNRESET' errors that are triggered when .end() is called.
|
||||
// it is a known issue that has no known fix. This just temporarily ignores that error.
|
||||
// https://github.com/mscdex/node-imap/issues/391
|
||||
// https://github.com/mscdex/node-imap/issues/395
|
||||
this.ending = true;
|
||||
|
||||
// using 'close' event to unbind ECONNRESET error handler, because the node-imap
|
||||
// maintainer claims it is the more reliable event between 'end' and 'close'.
|
||||
// https://github.com/mscdex/node-imap/issues/394
|
||||
this.imap.once('close', () => {
|
||||
this.ending = false;
|
||||
});
|
||||
|
||||
this.imap.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the currently open mailbox, and retrieve the results
|
||||
*
|
||||
* Results are in the form:
|
||||
*
|
||||
* [{
|
||||
* attributes: object,
|
||||
* parts: [ { which: string, size: number, body: string }, ... ]
|
||||
* }, ...]
|
||||
*
|
||||
* See node-imap's ImapMessage signature for information about `attributes`, `which`, `size`, and `body`.
|
||||
* For any message part that is a `HEADER`, the body is automatically parsed into an object.
|
||||
*/
|
||||
async search(
|
||||
/** Criteria to use to search. Passed to node-imap's .search() 1:1 */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
searchCriteria: any[],
|
||||
/** Criteria to use to fetch the search results. Passed to node-imap's .fetch() 1:1 */
|
||||
fetchOptions: Imap.FetchOptions,
|
||||
) {
|
||||
return await new Promise<Message[]>((resolve, reject) => {
|
||||
this.imap.search(searchCriteria, (e, uids) => {
|
||||
if (e) {
|
||||
reject(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (uids.length === 0) {
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetch = this.imap.fetch(uids, fetchOptions);
|
||||
let messagesRetrieved = 0;
|
||||
const messages: Message[] = [];
|
||||
|
||||
const fetchOnMessage = async (message: Imap.ImapMessage, seqNo: number) => {
|
||||
const msg: Message = await getMessage(message);
|
||||
msg.seqNo = seqNo;
|
||||
messages[seqNo] = msg;
|
||||
|
||||
messagesRetrieved++;
|
||||
if (messagesRetrieved === uids.length) {
|
||||
resolve(messages.filter((m) => !!m));
|
||||
}
|
||||
};
|
||||
|
||||
const fetchOnError = (error: Error) => {
|
||||
fetch.removeListener('message', fetchOnMessage);
|
||||
fetch.removeListener('end', fetchOnEnd);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
const fetchOnEnd = () => {
|
||||
fetch.removeListener('message', fetchOnMessage);
|
||||
fetch.removeListener('error', fetchOnError);
|
||||
};
|
||||
|
||||
fetch.on('message', fetchOnMessage);
|
||||
fetch.once('error', fetchOnError);
|
||||
fetch.once('end', fetchOnEnd);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Download a "part" (either a portion of the message body, or an attachment) */
|
||||
async getPartData(
|
||||
/** The message returned from `search()` */
|
||||
message: Message,
|
||||
/** The message part to be downloaded, from the `message.attributes.struct` Array */
|
||||
part: MessagePart,
|
||||
) {
|
||||
return await new Promise<string>((resolve, reject) => {
|
||||
const fetch = this.imap.fetch(message.attributes.uid, {
|
||||
bodies: [part.partID],
|
||||
struct: true,
|
||||
});
|
||||
|
||||
const fetchOnMessage = async (msg: ImapMessage) => {
|
||||
const result = await getMessage(msg);
|
||||
if (result.parts.length !== 1) {
|
||||
reject(new Error('Got ' + result.parts.length + ' parts, should get 1'));
|
||||
return;
|
||||
}
|
||||
|
||||
const data = result.parts[0].body as string;
|
||||
|
||||
const encoding = part.encoding.toUpperCase();
|
||||
|
||||
if (encoding === 'BASE64') {
|
||||
resolve(Buffer.from(data, 'base64').toString());
|
||||
return;
|
||||
}
|
||||
|
||||
if (encoding === 'QUOTED-PRINTABLE') {
|
||||
if (part.params?.charset?.toUpperCase() === 'UTF-8') {
|
||||
resolve(Buffer.from(utf8.decode(qp.decode(data))).toString());
|
||||
} else {
|
||||
resolve(Buffer.from(qp.decode(data)).toString());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (encoding === '7BIT') {
|
||||
resolve(Buffer.from(data).toString('ascii'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (encoding === '8BIT' || encoding === 'BINARY') {
|
||||
const charset = part.params?.charset ?? 'utf-8';
|
||||
resolve(iconvlite.decode(Buffer.from(data), charset));
|
||||
return;
|
||||
}
|
||||
|
||||
if (encoding === 'UUENCODE') {
|
||||
const parts = data.toString().split('\n'); // remove newline characters
|
||||
const merged = parts.splice(1, parts.length - 4).join(''); // remove excess lines and join lines with empty string
|
||||
resolve(uuencode.decode(merged));
|
||||
return;
|
||||
}
|
||||
|
||||
// if it gets here, the encoding is not currently supported
|
||||
reject(new Error('Unknown encoding ' + part.encoding));
|
||||
};
|
||||
|
||||
const fetchOnError = (error: Error) => {
|
||||
fetch.removeListener('message', fetchOnMessage);
|
||||
fetch.removeListener('end', fetchOnEnd);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
const fetchOnEnd = () => {
|
||||
fetch.removeListener('message', fetchOnMessage);
|
||||
fetch.removeListener('error', fetchOnError);
|
||||
};
|
||||
|
||||
fetch.once('message', fetchOnMessage);
|
||||
fetch.once('error', fetchOnError);
|
||||
fetch.once('end', fetchOnEnd);
|
||||
});
|
||||
}
|
||||
|
||||
/** Adds the provided flag(s) to the specified message(s). */
|
||||
async addFlags(
|
||||
/** The messages uid */
|
||||
uid: number[],
|
||||
/** The flags to add to the message(s). */
|
||||
flags: string | string[],
|
||||
) {
|
||||
return await new Promise<void>((resolve, reject) => {
|
||||
this.imap.addFlags(uid, flags, (e) => (e ? reject(e) : resolve()));
|
||||
});
|
||||
}
|
||||
|
||||
/** Returns a list of mailboxes (folders). */
|
||||
async getBoxes() {
|
||||
return await new Promise<Imap.MailBoxes>((resolve, reject) => {
|
||||
this.imap.getBoxes((e, boxes) => (e ? reject(e) : resolve(boxes)));
|
||||
});
|
||||
}
|
||||
|
||||
/** Open a mailbox */
|
||||
async openBox(
|
||||
/** The name of the box to open */
|
||||
boxName: string,
|
||||
): Promise<Imap.Box> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
this.imap.openBox(boxName, (e, result) => (e ? reject(e) : resolve(result)));
|
||||
});
|
||||
}
|
||||
|
||||
/** Close a mailbox */
|
||||
async closeBox(
|
||||
/** If autoExpunge is true, any messages marked as Deleted in the currently open mailbox will be removed @default true */
|
||||
autoExpunge = true,
|
||||
) {
|
||||
return await new Promise<void>((resolve, reject) => {
|
||||
this.imap.closeBox(autoExpunge, (e) => (e ? reject(e) : resolve()));
|
||||
});
|
||||
}
|
||||
}
|
27
packages/@n8n/imap/src/errors.ts
Normal file
27
packages/@n8n/imap/src/errors.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
export abstract class ImapError extends Error {}
|
||||
|
||||
/** Error thrown when a connection attempt has timed out */
|
||||
export class ConnectionTimeoutError extends ImapError {
|
||||
constructor(
|
||||
/** timeout in milliseconds that the connection waited before timing out */
|
||||
readonly timeout?: number,
|
||||
) {
|
||||
let message = 'connection timed out';
|
||||
if (timeout) {
|
||||
message += `. timeout = ${timeout} ms`;
|
||||
}
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export class ConnectionClosedError extends ImapError {
|
||||
constructor() {
|
||||
super('Connection closed unexpectedly');
|
||||
}
|
||||
}
|
||||
|
||||
export class ConnectionEndedError extends ImapError {
|
||||
constructor() {
|
||||
super('Connection ended unexpectedly');
|
||||
}
|
||||
}
|
53
packages/@n8n/imap/src/helpers/getMessage.ts
Normal file
53
packages/@n8n/imap/src/helpers/getMessage.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import {
|
||||
parseHeader,
|
||||
type ImapMessage,
|
||||
type ImapMessageBodyInfo,
|
||||
type ImapMessageAttributes,
|
||||
} from 'imap';
|
||||
import type { Message, MessageBodyPart } from '../types';
|
||||
|
||||
/**
|
||||
* Given an 'ImapMessage' from the node-imap library, retrieves the `Message`
|
||||
*/
|
||||
export async function getMessage(
|
||||
/** an ImapMessage from the node-imap library */
|
||||
message: ImapMessage,
|
||||
): Promise<Message> {
|
||||
return await new Promise((resolve) => {
|
||||
let attributes: ImapMessageAttributes;
|
||||
const parts: MessageBodyPart[] = [];
|
||||
|
||||
const messageOnBody = (stream: NodeJS.ReadableStream, info: ImapMessageBodyInfo) => {
|
||||
let body: string = '';
|
||||
|
||||
const streamOnData = (chunk: Buffer) => {
|
||||
body += chunk.toString('utf8');
|
||||
};
|
||||
|
||||
stream.on('data', streamOnData);
|
||||
stream.once('end', () => {
|
||||
stream.removeListener('data', streamOnData);
|
||||
|
||||
parts.push({
|
||||
which: info.which,
|
||||
size: info.size,
|
||||
body: /^HEADER/g.test(info.which) ? parseHeader(body) : body,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const messageOnAttributes = (attrs: ImapMessageAttributes) => {
|
||||
attributes = attrs;
|
||||
};
|
||||
|
||||
const messageOnEnd = () => {
|
||||
message.removeListener('body', messageOnBody);
|
||||
message.removeListener('attributes', messageOnAttributes);
|
||||
resolve({ attributes, parts });
|
||||
};
|
||||
|
||||
message.on('body', messageOnBody);
|
||||
message.once('attributes', messageOnAttributes);
|
||||
message.once('end', messageOnEnd);
|
||||
});
|
||||
}
|
99
packages/@n8n/imap/src/index.ts
Normal file
99
packages/@n8n/imap/src/index.ts
Normal file
|
@ -0,0 +1,99 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
import Imap from 'imap';
|
||||
import { ImapSimple } from './ImapSimple';
|
||||
import { ConnectionClosedError, ConnectionEndedError, ConnectionTimeoutError } from './errors';
|
||||
import type { ImapSimpleOptions, MessagePart } from './types';
|
||||
|
||||
/**
|
||||
* Connect to an Imap server, returning an ImapSimple instance, which is a wrapper over node-imap to simplify it's api for common use cases.
|
||||
*/
|
||||
export async function connect(options: ImapSimpleOptions): Promise<ImapSimple> {
|
||||
const authTimeout = options.imap.authTimeout ?? 2000;
|
||||
options.imap.authTimeout = authTimeout;
|
||||
|
||||
const imap = new Imap(options.imap);
|
||||
|
||||
return await new Promise<ImapSimple>((resolve, reject) => {
|
||||
const cleanUp = () => {
|
||||
imap.removeListener('ready', imapOnReady);
|
||||
imap.removeListener('error', imapOnError);
|
||||
imap.removeListener('close', imapOnClose);
|
||||
imap.removeListener('end', imapOnEnd);
|
||||
};
|
||||
|
||||
const imapOnReady = () => {
|
||||
cleanUp();
|
||||
resolve(new ImapSimple(imap));
|
||||
};
|
||||
|
||||
const imapOnError = (e: Error & { source?: string }) => {
|
||||
if (e.source === 'timeout-auth') {
|
||||
e = new ConnectionTimeoutError(authTimeout);
|
||||
}
|
||||
|
||||
cleanUp();
|
||||
reject(e);
|
||||
};
|
||||
|
||||
const imapOnEnd = () => {
|
||||
cleanUp();
|
||||
reject(new ConnectionEndedError());
|
||||
};
|
||||
|
||||
const imapOnClose = () => {
|
||||
cleanUp();
|
||||
reject(new ConnectionClosedError());
|
||||
};
|
||||
|
||||
imap.once('ready', imapOnReady);
|
||||
imap.once('error', imapOnError);
|
||||
imap.once('close', imapOnClose);
|
||||
imap.once('end', imapOnEnd);
|
||||
|
||||
if (options.onMail) {
|
||||
imap.on('mail', options.onMail);
|
||||
}
|
||||
|
||||
if (options.onExpunge) {
|
||||
imap.on('expunge', options.onExpunge);
|
||||
}
|
||||
|
||||
if (options.onUpdate) {
|
||||
imap.on('update', options.onUpdate);
|
||||
}
|
||||
|
||||
imap.connect();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the `message.attributes.struct`, retrieve a flattened array of `parts` objects that describe the structure of
|
||||
* the different parts of the message's body. Useful for getting a simple list to iterate for the purposes of,
|
||||
* for example, finding all attachments.
|
||||
*
|
||||
* Code taken from http://stackoverflow.com/questions/25247207/how-to-read-and-save-attachments-using-node-imap
|
||||
*
|
||||
* @returns {Array} a flattened array of `parts` objects that describe the structure of the different parts of the
|
||||
* message's body
|
||||
*/
|
||||
export function getParts(
|
||||
/** The `message.attributes.struct` value from the message you wish to retrieve parts for. */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
struct: any,
|
||||
/** The list of parts to push to. */
|
||||
parts: MessagePart[] = [],
|
||||
): MessagePart[] {
|
||||
for (let i = 0; i < struct.length; i++) {
|
||||
if (Array.isArray(struct[i])) {
|
||||
getParts(struct[i], parts);
|
||||
} else if (struct[i].partID) {
|
||||
parts.push(struct[i] as MessagePart);
|
||||
}
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
export * from './ImapSimple';
|
||||
export * from './errors';
|
||||
export * from './types';
|
41
packages/@n8n/imap/src/types.ts
Normal file
41
packages/@n8n/imap/src/types.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import type { Config, ImapMessageBodyInfo, ImapMessageAttributes } from 'imap';
|
||||
|
||||
export interface ImapSimpleOptions {
|
||||
/** Options to pass to node-imap constructor. */
|
||||
imap: Config;
|
||||
|
||||
/** Server event emitted when new mail arrives in the currently open mailbox. */
|
||||
onMail?: ((numNewMail: number) => void) | undefined;
|
||||
|
||||
/** Server event emitted when a message was expunged externally. seqNo is the sequence number (instead of the unique UID) of the message that was expunged. If you are caching sequence numbers, all sequence numbers higher than this value MUST be decremented by 1 in order to stay synchronized with the server and to keep correct continuity. */
|
||||
onExpunge?: ((seqNo: number) => void) | undefined;
|
||||
|
||||
/** Server event emitted when message metadata (e.g. flags) changes externally. */
|
||||
onUpdate?:
|
||||
| ((seqNo: number, info: { num: number | undefined; text: unknown }) => void)
|
||||
| undefined;
|
||||
}
|
||||
|
||||
export interface MessagePart {
|
||||
partID: string;
|
||||
encoding: 'BASE64' | 'QUOTED-PRINTABLE' | '7BIT' | '8BIT' | 'BINARY' | 'UUENCODE';
|
||||
type: 'TEXT';
|
||||
subtype: string;
|
||||
params?: {
|
||||
charset?: string;
|
||||
};
|
||||
disposition?: {
|
||||
type: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MessageBodyPart extends ImapMessageBodyInfo {
|
||||
/** string type where which=='TEXT', complex Object where which=='HEADER' */
|
||||
body: string | object;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
attributes: ImapMessageAttributes;
|
||||
parts: MessageBodyPart[];
|
||||
seqNo?: number;
|
||||
}
|
10
packages/@n8n/imap/tsconfig.build.json
Normal file
10
packages/@n8n/imap/tsconfig.build.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"],
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"tsBuildInfoFile": "dist/build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["test/**"]
|
||||
}
|
12
packages/@n8n/imap/tsconfig.json
Normal file
12
packages/@n8n/imap/tsconfig.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"types": ["node", "jest"],
|
||||
"composite": true,
|
||||
"noEmit": true,
|
||||
"baseUrl": "src",
|
||||
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.ts", "test/**/*.ts"]
|
||||
}
|
|
@ -16,8 +16,8 @@ import type {
|
|||
} from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import type { ImapSimple, ImapSimpleOptions, Message } from 'imap-simple';
|
||||
import { connect as imapConnect, getParts } from 'imap-simple';
|
||||
import type { ImapSimple, ImapSimpleOptions, Message } from '@n8n/imap';
|
||||
import { connect as imapConnect, getParts } from '@n8n/imap';
|
||||
import type { Source as ParserSource } from 'mailparser';
|
||||
import { simpleParser } from 'mailparser';
|
||||
|
||||
|
@ -255,12 +255,9 @@ export class EmailReadImapV1 implements INodeType {
|
|||
if (!isEmpty(tlsOptions)) {
|
||||
config.imap.tlsOptions = tlsOptions;
|
||||
}
|
||||
const conn = imapConnect(config).then(async (entry) => {
|
||||
return entry;
|
||||
});
|
||||
(await conn).getBoxes((_err, _boxes) => {});
|
||||
const connection = await imapConnect(config);
|
||||
await connection.getBoxes();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
status: 'Error',
|
||||
message: error.message,
|
||||
|
@ -333,7 +330,7 @@ export class EmailReadImapV1 implements INodeType {
|
|||
.then(async (partData) => {
|
||||
// Return it in the format n8n expects
|
||||
return await this.helpers.prepareBinaryData(
|
||||
partData as Buffer,
|
||||
Buffer.from(partData),
|
||||
attachmentPart.disposition.params.filename as string,
|
||||
);
|
||||
});
|
||||
|
@ -370,7 +367,7 @@ export class EmailReadImapV1 implements INodeType {
|
|||
const results = await imapConnection.search(searchCriteria, fetchOptions);
|
||||
|
||||
const newEmails: INodeExecutionData[] = [];
|
||||
let newEmail: INodeExecutionData, messageHeader, messageBody;
|
||||
let newEmail: INodeExecutionData;
|
||||
let attachments: IBinaryData[];
|
||||
let propertyName: string;
|
||||
|
||||
|
@ -442,11 +439,9 @@ export class EmailReadImapV1 implements INodeType {
|
|||
},
|
||||
};
|
||||
|
||||
messageHeader = message.parts.filter((part) => {
|
||||
return part.which === 'HEADER';
|
||||
});
|
||||
const messageHeader = message.parts.filter((part) => part.which === 'HEADER');
|
||||
|
||||
messageBody = messageHeader[0].body;
|
||||
const messageBody = messageHeader[0].body as Record<string, string[]>;
|
||||
for (propertyName of Object.keys(messageBody as IDataObject)) {
|
||||
if (messageBody[propertyName].length) {
|
||||
if (topLevelProperties.includes(propertyName)) {
|
||||
|
@ -532,7 +527,7 @@ export class EmailReadImapV1 implements INodeType {
|
|||
tls: credentials.secure as boolean,
|
||||
authTimeout: 20000,
|
||||
},
|
||||
onmail: async () => {
|
||||
onMail: async () => {
|
||||
if (connection) {
|
||||
if (staticData.lastMessageUid !== undefined) {
|
||||
searchCriteria.push(['UID', `${staticData.lastMessageUid as number}:*`]);
|
||||
|
|
|
@ -16,8 +16,8 @@ import type {
|
|||
} from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import type { ImapSimple, ImapSimpleOptions, Message } from 'imap-simple';
|
||||
import { connect as imapConnect, getParts } from 'imap-simple';
|
||||
import type { ImapSimple, ImapSimpleOptions, Message, MessagePart } from '@n8n/imap';
|
||||
import { connect as imapConnect, getParts } from '@n8n/imap';
|
||||
import type { Source as ParserSource } from 'mailparser';
|
||||
import { simpleParser } from 'mailparser';
|
||||
import rfc2047 from 'rfc2047';
|
||||
|
@ -298,15 +298,14 @@ export class EmailReadImapV2 implements INodeType {
|
|||
|
||||
// Returns the email text
|
||||
|
||||
const getText = async (parts: IDataObject[], message: Message, subtype: string) => {
|
||||
const getText = async (parts: MessagePart[], message: Message, subtype: string) => {
|
||||
if (!message.attributes.struct) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const textParts = parts.filter((part) => {
|
||||
return (
|
||||
(part.type as string).toUpperCase() === 'TEXT' &&
|
||||
(part.subtype as string).toUpperCase() === subtype.toUpperCase()
|
||||
part.type.toUpperCase() === 'TEXT' && part.subtype.toUpperCase() === subtype.toUpperCase()
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -315,7 +314,7 @@ export class EmailReadImapV2 implements INodeType {
|
|||
}
|
||||
|
||||
try {
|
||||
return (await connection.getPartData(message, textParts[0])) as string;
|
||||
return await connection.getPartData(message, textParts[0]);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
|
@ -324,7 +323,7 @@ export class EmailReadImapV2 implements INodeType {
|
|||
// Returns the email attachments
|
||||
const getAttachment = async (
|
||||
imapConnection: ImapSimple,
|
||||
parts: IDataObject[],
|
||||
parts: MessagePart[],
|
||||
message: Message,
|
||||
): Promise<IBinaryData[]> => {
|
||||
if (!message.attributes.struct) {
|
||||
|
@ -332,12 +331,9 @@ export class EmailReadImapV2 implements INodeType {
|
|||
}
|
||||
|
||||
// Check if the message has attachments and if so get them
|
||||
const attachmentParts = parts.filter((part) => {
|
||||
return (
|
||||
part.disposition &&
|
||||
((part.disposition as IDataObject)?.type as string).toUpperCase() === 'ATTACHMENT'
|
||||
);
|
||||
});
|
||||
const attachmentParts = parts.filter(
|
||||
(part) => part.disposition?.type?.toUpperCase() === 'ATTACHMENT',
|
||||
);
|
||||
|
||||
const decodeFilename = (filename: string) => {
|
||||
const regex = /=\?([\w-]+)\?Q\?.*\?=/i;
|
||||
|
@ -359,7 +355,7 @@ export class EmailReadImapV2 implements INodeType {
|
|||
?.filename as string,
|
||||
);
|
||||
// Return it in the format n8n expects
|
||||
return await this.helpers.prepareBinaryData(partData as Buffer, fileName);
|
||||
return await this.helpers.prepareBinaryData(Buffer.from(partData), fileName);
|
||||
});
|
||||
|
||||
attachmentPromises.push(attachmentPromise);
|
||||
|
@ -394,7 +390,7 @@ export class EmailReadImapV2 implements INodeType {
|
|||
const results = await imapConnection.search(searchCriteria, fetchOptions);
|
||||
|
||||
const newEmails: INodeExecutionData[] = [];
|
||||
let newEmail: INodeExecutionData, messageHeader, messageBody;
|
||||
let newEmail: INodeExecutionData;
|
||||
let attachments: IBinaryData[];
|
||||
let propertyName: string;
|
||||
|
||||
|
@ -456,7 +452,7 @@ export class EmailReadImapV2 implements INodeType {
|
|||
) {
|
||||
staticData.lastMessageUid = message.attributes.uid;
|
||||
}
|
||||
const parts = getParts(message.attributes.struct as IDataObject[]) as IDataObject[];
|
||||
const parts = getParts(message.attributes.struct as IDataObject[]);
|
||||
|
||||
newEmail = {
|
||||
json: {
|
||||
|
@ -466,19 +462,16 @@ export class EmailReadImapV2 implements INodeType {
|
|||
},
|
||||
};
|
||||
|
||||
messageHeader = message.parts.filter((part) => {
|
||||
return part.which === 'HEADER';
|
||||
});
|
||||
const messageHeader = message.parts.filter((part) => part.which === 'HEADER');
|
||||
|
||||
messageBody = messageHeader[0].body as IDataObject;
|
||||
const messageBody = messageHeader[0].body as Record<string, string[]>;
|
||||
for (propertyName of Object.keys(messageBody)) {
|
||||
if ((messageBody[propertyName] as IDataObject[]).length) {
|
||||
if (messageBody[propertyName].length) {
|
||||
if (topLevelProperties.includes(propertyName)) {
|
||||
newEmail.json[propertyName] = (messageBody[propertyName] as string[])[0];
|
||||
newEmail.json[propertyName] = messageBody[propertyName][0];
|
||||
} else {
|
||||
(newEmail.json.metadata as IDataObject)[propertyName] = (
|
||||
messageBody[propertyName] as string[]
|
||||
)[0];
|
||||
(newEmail.json.metadata as IDataObject)[propertyName] =
|
||||
messageBody[propertyName][0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -559,7 +552,7 @@ export class EmailReadImapV2 implements INodeType {
|
|||
tls: credentials.secure,
|
||||
authTimeout: 20000,
|
||||
},
|
||||
onmail: async () => {
|
||||
onMail: async () => {
|
||||
if (connection) {
|
||||
if (staticData.lastMessageUid !== undefined) {
|
||||
searchCriteria.push(['UID', `${staticData.lastMessageUid as number}:*`]);
|
||||
|
@ -597,8 +590,8 @@ export class EmailReadImapV2 implements INodeType {
|
|||
}
|
||||
}
|
||||
},
|
||||
onupdate: async (seqno: number, info) => {
|
||||
this.logger.verbose(`Email Read Imap:update ${seqno}`, info as IDataObject);
|
||||
onUpdate: async (seqNo: number, info) => {
|
||||
this.logger.verbose(`Email Read Imap:update ${seqNo}`, info);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -812,7 +812,6 @@
|
|||
"@types/express": "^4.17.6",
|
||||
"@types/html-to-text": "^9.0.1",
|
||||
"@types/gm": "^1.25.0",
|
||||
"@types/imap-simple": "^4.2.0",
|
||||
"@types/js-nacl": "^1.3.0",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/lodash": "^4.14.195",
|
||||
|
@ -836,6 +835,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@kafkajs/confluent-schema-registry": "1.0.6",
|
||||
"@n8n/imap": "workspace:*",
|
||||
"@n8n/vm2": "3.9.20",
|
||||
"amqplib": "0.10.3",
|
||||
"aws4": "1.11.0",
|
||||
|
@ -854,7 +854,6 @@
|
|||
"gm": "1.25.0",
|
||||
"iconv-lite": "0.6.3",
|
||||
"ics": "2.40.0",
|
||||
"imap-simple": "4.3.0",
|
||||
"isbot": "3.6.13",
|
||||
"iso-639-1": "2.1.15",
|
||||
"js-nacl": "1.4.0",
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
},
|
||||
"include": ["credentials/**/*.ts", "nodes/**/*.ts", "test/**/*.ts", "utils/**/*.ts"],
|
||||
"references": [
|
||||
{ "path": "../@n8n/imap/tsconfig.build.json" },
|
||||
{ "path": "../workflow/tsconfig.build.json" },
|
||||
{ "path": "../core/tsconfig.build.json" }
|
||||
],
|
||||
|
|
|
@ -178,6 +178,37 @@ importers:
|
|||
specifier: 1.6.7
|
||||
version: 1.6.7(debug@3.2.7)
|
||||
|
||||
packages/@n8n/imap:
|
||||
dependencies:
|
||||
iconv-lite:
|
||||
specifier: 0.6.3
|
||||
version: 0.6.3
|
||||
imap:
|
||||
specifier: 0.8.19
|
||||
version: 0.8.19
|
||||
quoted-printable:
|
||||
specifier: 1.0.1
|
||||
version: 1.0.1
|
||||
utf8:
|
||||
specifier: 3.0.0
|
||||
version: 3.0.0
|
||||
uuencode:
|
||||
specifier: 0.0.4
|
||||
version: 0.0.4
|
||||
devDependencies:
|
||||
'@types/imap':
|
||||
specifier: ^0.8.40
|
||||
version: 0.8.40
|
||||
'@types/quoted-printable':
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2
|
||||
'@types/utf8':
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3
|
||||
'@types/uuencode':
|
||||
specifier: ^0.0.3
|
||||
version: 0.0.3
|
||||
|
||||
packages/@n8n/nodes-langchain:
|
||||
dependencies:
|
||||
'@aws-sdk/client-bedrock-runtime':
|
||||
|
@ -1232,6 +1263,9 @@ importers:
|
|||
'@kafkajs/confluent-schema-registry':
|
||||
specifier: 1.0.6
|
||||
version: 1.0.6
|
||||
'@n8n/imap':
|
||||
specifier: workspace:*
|
||||
version: link:../@n8n/imap
|
||||
'@n8n/vm2':
|
||||
specifier: 3.9.20
|
||||
version: 3.9.20
|
||||
|
@ -1286,9 +1320,6 @@ importers:
|
|||
ics:
|
||||
specifier: 2.40.0
|
||||
version: 2.40.0
|
||||
imap-simple:
|
||||
specifier: 4.3.0
|
||||
version: 4.3.0
|
||||
isbot:
|
||||
specifier: 3.6.13
|
||||
version: 3.6.13
|
||||
|
@ -1449,9 +1480,6 @@ importers:
|
|||
'@types/html-to-text':
|
||||
specifier: ^9.0.1
|
||||
version: 9.0.4
|
||||
'@types/imap-simple':
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.5
|
||||
'@types/js-nacl':
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
|
@ -9474,15 +9502,8 @@ packages:
|
|||
resolution: {integrity: sha512-K3e+NZlpCKd6Bd/EIdqjFJRFHbrq5TzPPLwREk5Iv/YoIjQrs6ljdAUCo+Lb2xFlGNOjGSE0dqsVD19cZL137w==}
|
||||
dev: true
|
||||
|
||||
/@types/imap-simple@4.2.5:
|
||||
resolution: {integrity: sha512-Sfu70sdFXzVIhivsflpanlED8gZr4VRzz2AVU9i1ARU8gskr9nDd4tVGkqYtxfwajQfZDklkXbeHSOZYEeJmTQ==}
|
||||
dependencies:
|
||||
'@types/imap': 0.8.35
|
||||
'@types/node': 18.16.16
|
||||
dev: true
|
||||
|
||||
/@types/imap@0.8.35:
|
||||
resolution: {integrity: sha512-4Tk8lGFvRFYCEaHxb2BZFZPs2XiYBSboEbb1Kq1oBqA2aRqPV0pjzyXGpMKi9jILsDPlrG7s0EFnnh8qb3h3ww==}
|
||||
/@types/imap@0.8.40:
|
||||
resolution: {integrity: sha512-kWFwOc88CGwWZlHqCnZiceS6EralsAHdjpQyk1+fIA875NQdIHvLpdD5NU3Pi1yZ8FKFdOF81UDNAo8/XS6HiQ==}
|
||||
dependencies:
|
||||
'@types/node': 18.16.16
|
||||
dev: true
|
||||
|
@ -9716,6 +9737,10 @@ packages:
|
|||
/@types/qs@6.9.7:
|
||||
resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==}
|
||||
|
||||
/@types/quoted-printable@1.0.2:
|
||||
resolution: {integrity: sha512-3B28oB1rRaZNb3N5dlxysm8lH1ujzvReDuYBiIO4jvpTIg9ksrILCNgPxSGVyTWE/qwuxzgHaVehwMK3CVqAtA==}
|
||||
dev: true
|
||||
|
||||
/@types/range-parser@1.2.4:
|
||||
resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==}
|
||||
|
||||
|
@ -9914,6 +9939,16 @@ packages:
|
|||
resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==}
|
||||
dev: true
|
||||
|
||||
/@types/utf8@3.0.3:
|
||||
resolution: {integrity: sha512-+lqLGxWZsEe4Z6OrzBI7Ym4SMUTaMS5yOrHZ0/IL0bpIye1Qbs4PpobJL2mLDbftUXlPFZR7fu6d1yM+bHLX1w==}
|
||||
dev: true
|
||||
|
||||
/@types/uuencode@0.0.3:
|
||||
resolution: {integrity: sha512-NaBWHPPQvcXqiSaMAGa2Ea/XaFcK/nHwGe2akwJBXRLkCNa2+izx/F1aKJrzFH+L68D88VLYIATTYP7B2k4zVA==}
|
||||
dependencies:
|
||||
'@types/node': 18.16.16
|
||||
dev: true
|
||||
|
||||
/@types/uuid@8.3.4:
|
||||
resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
|
||||
|
||||
|
@ -16367,17 +16402,6 @@ packages:
|
|||
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
/imap-simple@4.3.0:
|
||||
resolution: {integrity: sha512-SW3LtfEJFjlJKS/h2CmpX2IKpya2RXobR3ENJJW4iMQ3QYPxWxf5oeaz1K3P4eGUwfGEndkqt7uVDKnEyG9zeQ==}
|
||||
dependencies:
|
||||
iconv-lite: 0.4.24
|
||||
imap: 0.8.19
|
||||
nodeify: 1.0.1
|
||||
quoted-printable: 1.0.1
|
||||
utf8: 2.1.2
|
||||
uuencode: 0.0.4
|
||||
dev: false
|
||||
|
||||
/imap@0.8.19:
|
||||
resolution: {integrity: sha512-z5DxEA1uRnZG73UcPA4ES5NSCGnPuuouUx43OPX7KZx1yzq3N8/vx2mtXEShT5inxB3pRgnfG1hijfu7XN2YMw==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
|
@ -16856,10 +16880,6 @@ packages:
|
|||
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
||||
dev: true
|
||||
|
||||
/is-promise@1.0.1:
|
||||
resolution: {integrity: sha512-mjWH5XxnhMA8cFnDchr6qRP9S/kLntKuEfIYku+PaN1CnS8v+OG9O/BKpRCVRJvpIkgAZm0Pf5Is3iSSOILlcg==}
|
||||
dev: false
|
||||
|
||||
/is-promise@2.2.2:
|
||||
resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==}
|
||||
dev: true
|
||||
|
@ -20135,13 +20155,6 @@ packages:
|
|||
ssh2: 1.11.0
|
||||
dev: false
|
||||
|
||||
/nodeify@1.0.1:
|
||||
resolution: {integrity: sha512-n7C2NyEze8GCo/z73KdbjRsBiLbv6eBn1FxwYKQ23IqGo7pQY3mhQan61Sv7eEDJCiyUjTVrVkXTzJCo1dW7Aw==}
|
||||
dependencies:
|
||||
is-promise: 1.0.1
|
||||
promise: 1.3.0
|
||||
dev: false
|
||||
|
||||
/nodemailer@6.9.9:
|
||||
resolution: {integrity: sha512-dexTll8zqQoVJEZPwQAKzxxtFn0qTnjdQTchoU6Re9BUUGBJiOy3YMn/0ShTW6J5M0dfQ1NeDeRTTl4oIWgQMA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
@ -21751,12 +21764,6 @@ packages:
|
|||
retry: 0.12.0
|
||||
dev: false
|
||||
|
||||
/promise@1.3.0:
|
||||
resolution: {integrity: sha512-R9WrbTF3EPkVtWjp7B7umQGVndpsi+rsDAfrR4xAALQpFLa/+2OriecLhawxzvii2gd9+DZFwROWDuUUaqS5yA==}
|
||||
dependencies:
|
||||
is-promise: 1.0.1
|
||||
dev: false
|
||||
|
||||
/promise@7.3.1:
|
||||
resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==}
|
||||
dependencies:
|
||||
|
@ -25246,6 +25253,10 @@ packages:
|
|||
resolution: {integrity: sha512-QXo+O/QkLP/x1nyi54uQiG0XrODxdysuQvE5dtVqv7F5K2Qb6FsN+qbr6KhF5wQ20tfcV3VQp0/2x1e1MRSPWg==}
|
||||
dev: false
|
||||
|
||||
/utf8@3.0.0:
|
||||
resolution: {integrity: sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==}
|
||||
dev: false
|
||||
|
||||
/util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
|
|
Loading…
Reference in a new issue