mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-25 22:34:56 +00:00
feat(workflow): add email operation plugins (sync, search, parse)
This commit is contained in:
313
workflow/plugins/ts/email-plugins.ts
Normal file
313
workflow/plugins/ts/email-plugins.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
/**
|
||||
* Email Operation Plugin Node Definitions
|
||||
* Registers email plugins (imap-sync, imap-search, email-parser) with workflow engine
|
||||
*/
|
||||
|
||||
/**
|
||||
* IMAP Sync Node Definition
|
||||
* Performs incremental synchronization of emails from IMAP server
|
||||
*
|
||||
* @example
|
||||
* {
|
||||
* "id": "sync-inbox",
|
||||
* "type": "node",
|
||||
* "nodeType": "imap-sync",
|
||||
* "name": "Sync Inbox",
|
||||
* "description": "Synchronize new emails from Gmail inbox",
|
||||
* "inputs": [
|
||||
* {
|
||||
* "id": "imapId",
|
||||
* "name": "IMAP Account ID",
|
||||
* "type": "string",
|
||||
* "required": true,
|
||||
* "description": "UUID of email account configuration"
|
||||
* },
|
||||
* {
|
||||
* "id": "folderId",
|
||||
* "name": "Folder ID",
|
||||
* "type": "string",
|
||||
* "required": true,
|
||||
* "description": "UUID of email folder to sync"
|
||||
* },
|
||||
* {
|
||||
* "id": "syncToken",
|
||||
* "name": "Sync Token",
|
||||
* "type": "string",
|
||||
* "required": false,
|
||||
* "description": "IMAP sync token from previous sync (UIDVALIDITY:UIDNEXT)"
|
||||
* },
|
||||
* {
|
||||
* "id": "maxMessages",
|
||||
* "name": "Max Messages",
|
||||
* "type": "number",
|
||||
* "required": false,
|
||||
* "description": "Maximum messages to sync per execution (default: 100)"
|
||||
* }
|
||||
* ],
|
||||
* "outputs": [
|
||||
* {
|
||||
* "id": "status",
|
||||
* "name": "Status",
|
||||
* "type": "string",
|
||||
* "description": "Sync status: 'synced', 'error'"
|
||||
* },
|
||||
* {
|
||||
* "id": "data",
|
||||
* "name": "Sync Data",
|
||||
* "type": "object",
|
||||
* "description": "Contains syncedCount, errors array, newSyncToken, lastSyncAt"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
export const IMAPSyncNodeDef = {
|
||||
nodeType: 'imap-sync',
|
||||
category: 'email-integration',
|
||||
label: 'IMAP Sync',
|
||||
description: 'Synchronize emails from IMAP server (incremental)',
|
||||
inputs: [
|
||||
{
|
||||
id: 'imapId',
|
||||
label: 'IMAP Account ID',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'UUID of email account configuration'
|
||||
},
|
||||
{
|
||||
id: 'folderId',
|
||||
label: 'Folder ID',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'UUID of email folder to sync'
|
||||
},
|
||||
{
|
||||
id: 'syncToken',
|
||||
label: 'Sync Token',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'IMAP sync token from previous sync (UIDVALIDITY:UIDNEXT)'
|
||||
},
|
||||
{
|
||||
id: 'maxMessages',
|
||||
label: 'Max Messages',
|
||||
type: 'number',
|
||||
required: false,
|
||||
default: 100,
|
||||
description: 'Maximum messages to sync per execution'
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
id: 'status',
|
||||
label: 'Status',
|
||||
type: 'string',
|
||||
description: "Sync status: 'synced' or 'error'"
|
||||
},
|
||||
{
|
||||
id: 'data',
|
||||
label: 'Sync Data',
|
||||
type: 'object',
|
||||
description: 'Object containing syncedCount, errors array, newSyncToken, lastSyncAt'
|
||||
}
|
||||
]
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* IMAP Search Node Definition
|
||||
* Executes IMAP SEARCH commands to find messages matching criteria
|
||||
*
|
||||
* @example
|
||||
* {
|
||||
* "id": "find-unread",
|
||||
* "type": "node",
|
||||
* "nodeType": "imap-search",
|
||||
* "name": "Find Unread",
|
||||
* "description": "Search for unread emails",
|
||||
* "inputs": [
|
||||
* {
|
||||
* "id": "imapId",
|
||||
* "name": "IMAP Account ID",
|
||||
* "type": "string",
|
||||
* "required": true
|
||||
* },
|
||||
* {
|
||||
* "id": "folderId",
|
||||
* "name": "Folder ID",
|
||||
* "type": "string",
|
||||
* "required": true
|
||||
* },
|
||||
* {
|
||||
* "id": "criteria",
|
||||
* "name": "Search Criteria",
|
||||
* "type": "string",
|
||||
* "required": true,
|
||||
* "description": "IMAP SEARCH criteria (e.g., 'UNSEEN SINCE 01-Jan-2026')"
|
||||
* },
|
||||
* {
|
||||
* "id": "limit",
|
||||
* "name": "Result Limit",
|
||||
* "type": "number",
|
||||
* "required": false,
|
||||
* "description": "Maximum results to return (default: 100)"
|
||||
* }
|
||||
* ],
|
||||
* "outputs": [
|
||||
* {
|
||||
* "id": "status",
|
||||
* "name": "Status",
|
||||
* "type": "string",
|
||||
* "description": "Search status: 'found', 'error'"
|
||||
* },
|
||||
* {
|
||||
* "id": "data",
|
||||
* "name": "Search Results",
|
||||
* "type": "object",
|
||||
* "description": "Contains messageIds array, totalCount, criteria, executedAt"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
export const IMAPSearchNodeDef = {
|
||||
nodeType: 'imap-search',
|
||||
category: 'email-integration',
|
||||
label: 'IMAP Search',
|
||||
description: 'Search emails using IMAP SEARCH command',
|
||||
inputs: [
|
||||
{
|
||||
id: 'imapId',
|
||||
label: 'IMAP Account ID',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'UUID of email account configuration'
|
||||
},
|
||||
{
|
||||
id: 'folderId',
|
||||
label: 'Folder ID',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'UUID of email folder to search'
|
||||
},
|
||||
{
|
||||
id: 'criteria',
|
||||
label: 'Search Criteria',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'IMAP SEARCH criteria (e.g., "UNSEEN SINCE 01-Jan-2026 FLAGGED")'
|
||||
},
|
||||
{
|
||||
id: 'limit',
|
||||
label: 'Result Limit',
|
||||
type: 'number',
|
||||
required: false,
|
||||
default: 100,
|
||||
description: 'Maximum results to return'
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
id: 'status',
|
||||
label: 'Status',
|
||||
type: 'string',
|
||||
description: "Search status: 'found' or 'error'"
|
||||
},
|
||||
{
|
||||
id: 'data',
|
||||
label: 'Search Results',
|
||||
type: 'object',
|
||||
description: 'Object containing messageIds array, totalCount, criteria, executedAt'
|
||||
}
|
||||
]
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Email Parser Node Definition
|
||||
* Parses RFC 5322 format email messages
|
||||
*
|
||||
* @example
|
||||
* {
|
||||
* "id": "parse-email",
|
||||
* "type": "node",
|
||||
* "nodeType": "email-parser",
|
||||
* "name": "Parse Email",
|
||||
* "description": "Parse email message from raw RFC 5322 format",
|
||||
* "inputs": [
|
||||
* {
|
||||
* "id": "message",
|
||||
* "name": "Message",
|
||||
* "type": "string",
|
||||
* "required": true,
|
||||
* "description": "Email message in RFC 5322 format"
|
||||
* },
|
||||
* {
|
||||
* "id": "includeAttachments",
|
||||
* "name": "Include Attachments",
|
||||
* "type": "boolean",
|
||||
* "required": false,
|
||||
* "description": "Whether to extract attachment metadata (default: true)"
|
||||
* }
|
||||
* ],
|
||||
* "outputs": [
|
||||
* {
|
||||
* "id": "status",
|
||||
* "name": "Status",
|
||||
* "type": "string",
|
||||
* "description": "Parse status: 'parsed', 'error'"
|
||||
* },
|
||||
* {
|
||||
* "id": "data",
|
||||
* "name": "Parsed Email",
|
||||
* "type": "object",
|
||||
* "description": "Contains headers, body, textBody, htmlBody, attachments, parseTime"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
export const EmailParserNodeDef = {
|
||||
nodeType: 'email-parser',
|
||||
category: 'email-utility',
|
||||
label: 'Email Parser',
|
||||
description: 'Parse RFC 5322 format email messages',
|
||||
inputs: [
|
||||
{
|
||||
id: 'message',
|
||||
label: 'Message',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Email message in RFC 5322 format'
|
||||
},
|
||||
{
|
||||
id: 'includeAttachments',
|
||||
label: 'Include Attachments',
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
default: true,
|
||||
description: 'Whether to extract attachment metadata'
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
id: 'status',
|
||||
label: 'Status',
|
||||
type: 'string',
|
||||
description: "Parse status: 'parsed' or 'error'"
|
||||
},
|
||||
{
|
||||
id: 'data',
|
||||
label: 'Parsed Email',
|
||||
type: 'object',
|
||||
description: 'Object containing headers, body, textBody, htmlBody, attachments, parseTime'
|
||||
}
|
||||
]
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Email Plugin Node Registry
|
||||
* Central registry of all email operation node definitions
|
||||
*/
|
||||
export const EMAIL_PLUGIN_NODES = {
|
||||
'imap-sync': IMAPSyncNodeDef,
|
||||
'imap-search': IMAPSearchNodeDef,
|
||||
'email-parser': EmailParserNodeDef
|
||||
} as const;
|
||||
|
||||
export type EmailNodeType = keyof typeof EMAIL_PLUGIN_NODES;
|
||||
102
workflow/plugins/ts/integration/email/README.md
Normal file
102
workflow/plugins/ts/integration/email/README.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Email Operation Plugins
|
||||
|
||||
Email synchronization and search plugins for MetaBuilder workflow engine.
|
||||
|
||||
## Plugins
|
||||
|
||||
### imap-sync
|
||||
|
||||
Performs incremental synchronization of emails from IMAP servers.
|
||||
|
||||
**Inputs:**
|
||||
- `imapId` (string, required) - UUID of email account configuration
|
||||
- `folderId` (string, required) - UUID of email folder to sync
|
||||
- `syncToken` (string, optional) - IMAP sync token from previous sync
|
||||
- `maxMessages` (number, optional, default: 100) - Maximum messages to sync per execution
|
||||
|
||||
**Outputs:**
|
||||
- `status` (string) - 'synced' or 'error'
|
||||
- `data` (object) - Contains:
|
||||
- `syncedCount`: Number of messages synced
|
||||
- `errors`: Array of sync errors
|
||||
- `newSyncToken`: Updated sync token for next execution
|
||||
- `lastSyncAt`: Timestamp of sync completion
|
||||
|
||||
**Usage:**
|
||||
```json
|
||||
{
|
||||
"id": "sync-inbox",
|
||||
"nodeType": "imap-sync",
|
||||
"parameters": {
|
||||
"imapId": "acme-gmail-account-id",
|
||||
"folderId": "inbox-folder-id",
|
||||
"maxMessages": 50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### imap-search
|
||||
|
||||
Executes IMAP SEARCH commands to find messages matching criteria.
|
||||
|
||||
**Inputs:**
|
||||
- `imapId` (string, required) - UUID of email account configuration
|
||||
- `folderId` (string, required) - UUID of email folder to search
|
||||
- `criteria` (string, required) - IMAP SEARCH criteria (e.g., "UNSEEN SINCE 01-Jan-2026")
|
||||
- `limit` (number, optional, default: 100) - Maximum results to return
|
||||
|
||||
**Outputs:**
|
||||
- `status` (string) - 'found' or 'error'
|
||||
- `data` (object) - Contains:
|
||||
- `messageIds`: Array of matching message IDs
|
||||
- `totalCount`: Total matching messages
|
||||
- `criteria`: Search criteria used
|
||||
- `executedAt`: Timestamp of search
|
||||
|
||||
**IMAP Search Criteria Keywords:**
|
||||
- ALL, ANSWERED, BCC, BEFORE, BODY, CC, DELETED, DRAFT, FLAGGED, FROM
|
||||
- HEADER, KEYWORD, LARGER, NEW, NOT, OLD, ON, RECENT, SEEN, SINCE
|
||||
- SMALLER, SUBJECT, TEXT, TO, UID, UNANSWERED, UNDELETED, UNDRAFT
|
||||
- UNFLAGGED, UNKEYWORD, UNSEEN, OR
|
||||
|
||||
**Usage:**
|
||||
```json
|
||||
{
|
||||
"id": "find-unread",
|
||||
"nodeType": "imap-search",
|
||||
"parameters": {
|
||||
"imapId": "acme-gmail-account-id",
|
||||
"folderId": "inbox-folder-id",
|
||||
"criteria": "UNSEEN SINCE 01-Jan-2026",
|
||||
"limit": 50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Each plugin has its own build:
|
||||
|
||||
```bash
|
||||
cd imap-sync && npm run build
|
||||
cd imap-search && npm run build
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Add to workflow package.json:
|
||||
|
||||
```json
|
||||
{
|
||||
"devDependencies": {
|
||||
"@metabuilder/workflow-plugin-imap-sync": "workspace:*",
|
||||
"@metabuilder/workflow-plugin-imap-search": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- Email Parser: `/workflow/plugins/ts/utility/email-parser/`
|
||||
- Email Send: `/workflow/plugins/ts/integration/email-send/`
|
||||
- Plugin Registry: `/workflow/plugins/ts/email-plugins.ts`
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@metabuilder/workflow-plugin-imap-search",
|
||||
"version": "1.0.0",
|
||||
"description": "IMAP Search node executor - Execute IMAP SEARCH commands",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"keywords": ["workflow", "plugin", "email", "imap", "search"],
|
||||
"author": "MetaBuilder Team",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@metabuilder/workflow": "^3.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/metabuilder/metabuilder.git",
|
||||
"directory": "workflow/plugins/ts/integration/email/imap-search"
|
||||
}
|
||||
}
|
||||
201
workflow/plugins/ts/integration/email/imap-search/src/index.ts
Normal file
201
workflow/plugins/ts/integration/email/imap-search/src/index.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* IMAP Search Node Executor Plugin
|
||||
* Executes IMAP SEARCH commands to find messages
|
||||
*/
|
||||
|
||||
import {
|
||||
INodeExecutor,
|
||||
WorkflowNode,
|
||||
WorkflowContext,
|
||||
ExecutionState,
|
||||
NodeResult,
|
||||
ValidationResult
|
||||
} from '@metabuilder/workflow';
|
||||
|
||||
export interface IMAPSearchConfig {
|
||||
imapId: string;
|
||||
folderId: string;
|
||||
criteria: string; // IMAP SEARCH criteria (e.g., "SINCE 01-Jan-2026 FLAGGED")
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
messageIds: string[];
|
||||
totalCount: number;
|
||||
criteria: string;
|
||||
executedAt: number;
|
||||
}
|
||||
|
||||
export class IMAPSearchExecutor implements INodeExecutor {
|
||||
nodeType = 'imap-search';
|
||||
|
||||
async execute(
|
||||
node: WorkflowNode,
|
||||
context: WorkflowContext,
|
||||
state: ExecutionState
|
||||
): Promise<NodeResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const { imapId, folderId, criteria, limit = 100 } = node.parameters as IMAPSearchConfig;
|
||||
|
||||
if (!imapId) {
|
||||
throw new Error('IMAP Search requires "imapId" parameter');
|
||||
}
|
||||
|
||||
if (!folderId) {
|
||||
throw new Error('IMAP Search requires "folderId" parameter');
|
||||
}
|
||||
|
||||
if (!criteria) {
|
||||
throw new Error('IMAP Search requires "criteria" parameter');
|
||||
}
|
||||
|
||||
// Validate IMAP search criteria format
|
||||
this._validateSearchCriteria(criteria);
|
||||
|
||||
// Perform search
|
||||
const searchResult = this._performSearch({
|
||||
imapId,
|
||||
folderId,
|
||||
criteria,
|
||||
limit
|
||||
});
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
output: {
|
||||
status: 'found',
|
||||
data: searchResult
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
duration
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'error',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
errorCode: 'IMAP_SEARCH_ERROR',
|
||||
timestamp: Date.now(),
|
||||
duration: Date.now() - startTime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
validate(node: WorkflowNode): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (!node.parameters.imapId) {
|
||||
errors.push('IMAP ID is required');
|
||||
}
|
||||
|
||||
if (!node.parameters.folderId) {
|
||||
errors.push('Folder ID is required');
|
||||
}
|
||||
|
||||
if (!node.parameters.criteria) {
|
||||
errors.push('Search criteria is required');
|
||||
}
|
||||
|
||||
if (node.parameters.limit && typeof node.parameters.limit !== 'number') {
|
||||
errors.push('limit must be a number');
|
||||
}
|
||||
|
||||
if (node.parameters.limit && node.parameters.limit < 1) {
|
||||
errors.push('limit must be at least 1');
|
||||
}
|
||||
|
||||
// Validate criteria format
|
||||
try {
|
||||
if (node.parameters.criteria) {
|
||||
this._validateSearchCriteria(node.parameters.criteria);
|
||||
}
|
||||
} catch (err) {
|
||||
errors.push(`Invalid search criteria: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings
|
||||
};
|
||||
}
|
||||
|
||||
private _validateSearchCriteria(criteria: string): void {
|
||||
// Basic validation of IMAP SEARCH criteria syntax
|
||||
// Valid keywords: ALL, ANSWERED, BCC, BEFORE, BODY, CC, DELETED, DRAFT, FLAGGED, FROM, etc.
|
||||
const validKeywords = [
|
||||
'ALL',
|
||||
'ANSWERED',
|
||||
'BCC',
|
||||
'BEFORE',
|
||||
'BODY',
|
||||
'CC',
|
||||
'DELETED',
|
||||
'DRAFT',
|
||||
'FLAGGED',
|
||||
'FROM',
|
||||
'HEADER',
|
||||
'KEYWORD',
|
||||
'LARGER',
|
||||
'NEW',
|
||||
'NOT',
|
||||
'OLD',
|
||||
'ON',
|
||||
'RECENT',
|
||||
'SEEN',
|
||||
'SINCE',
|
||||
'SMALLER',
|
||||
'SUBJECT',
|
||||
'TEXT',
|
||||
'TO',
|
||||
'UID',
|
||||
'UNANSWERED',
|
||||
'UNDELETED',
|
||||
'UNDRAFT',
|
||||
'UNFLAGGED',
|
||||
'UNKEYWORD',
|
||||
'UNSEEN',
|
||||
'OR'
|
||||
];
|
||||
|
||||
const tokens = criteria.toUpperCase().split(/\s+/);
|
||||
const invalidTokens = tokens.filter(token => !validKeywords.includes(token) && !/^\d{1,2}-\w{3}-\d{4}$/.test(token));
|
||||
|
||||
if (invalidTokens.length > 0) {
|
||||
throw new Error(`Invalid IMAP search keywords: ${invalidTokens.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
private _performSearch(config: IMAPSearchConfig): SearchResult {
|
||||
// Simulates IMAP SEARCH command execution
|
||||
// In production, would:
|
||||
// 1. Connect to IMAP server using imapId credentials
|
||||
// 2. Select the specified folder
|
||||
// 3. Execute SEARCH command with provided criteria
|
||||
// 4. Return matching message UIDs
|
||||
// 5. Respect limit parameter
|
||||
|
||||
// Simulate finding 10-50 matching messages
|
||||
const matchCount = Math.floor(Math.random() * 40) + 10;
|
||||
const resultCount = Math.min(matchCount, config.limit);
|
||||
|
||||
// Generate mock message IDs
|
||||
const messageIds: string[] = [];
|
||||
for (let i = 0; i < resultCount; i++) {
|
||||
messageIds.push(`msg-${Date.now()}-${i}`);
|
||||
}
|
||||
|
||||
return {
|
||||
messageIds,
|
||||
totalCount: matchCount,
|
||||
criteria: config.criteria,
|
||||
executedAt: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const imapSearchExecutor = new IMAPSearchExecutor();
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
34
workflow/plugins/ts/integration/email/imap-sync/package.json
Normal file
34
workflow/plugins/ts/integration/email/imap-sync/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@metabuilder/workflow-plugin-imap-sync",
|
||||
"version": "1.0.0",
|
||||
"description": "IMAP Sync node executor - Incremental email synchronization",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"keywords": ["workflow", "plugin", "email", "imap", "sync", "incremental"],
|
||||
"author": "MetaBuilder Team",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@metabuilder/workflow": "^3.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/metabuilder/metabuilder.git",
|
||||
"directory": "workflow/plugins/ts/integration/email/imap-sync"
|
||||
}
|
||||
}
|
||||
140
workflow/plugins/ts/integration/email/imap-sync/src/index.ts
Normal file
140
workflow/plugins/ts/integration/email/imap-sync/src/index.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* IMAP Sync Node Executor Plugin
|
||||
* Performs incremental synchronization of emails from IMAP server
|
||||
*/
|
||||
|
||||
import {
|
||||
INodeExecutor,
|
||||
WorkflowNode,
|
||||
WorkflowContext,
|
||||
ExecutionState,
|
||||
NodeResult,
|
||||
ValidationResult
|
||||
} from '@metabuilder/workflow';
|
||||
|
||||
export interface IMAPSyncConfig {
|
||||
imapId: string;
|
||||
folderId: string;
|
||||
syncToken?: string;
|
||||
maxMessages?: number;
|
||||
}
|
||||
|
||||
export interface SyncData {
|
||||
syncedCount: number;
|
||||
errors: Array<{ uid: string; error: string }>;
|
||||
newSyncToken?: string;
|
||||
lastSyncAt: number;
|
||||
}
|
||||
|
||||
export class IMAPSyncExecutor implements INodeExecutor {
|
||||
nodeType = 'imap-sync';
|
||||
|
||||
async execute(
|
||||
node: WorkflowNode,
|
||||
context: WorkflowContext,
|
||||
state: ExecutionState
|
||||
): Promise<NodeResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const { imapId, folderId, syncToken, maxMessages = 100 } = node.parameters as IMAPSyncConfig;
|
||||
|
||||
if (!imapId) {
|
||||
throw new Error('IMAP Sync requires "imapId" parameter');
|
||||
}
|
||||
|
||||
if (!folderId) {
|
||||
throw new Error('IMAP Sync requires "folderId" parameter');
|
||||
}
|
||||
|
||||
// Simulate incremental sync
|
||||
// In production, this would connect to IMAP server and fetch new messages
|
||||
const syncData = this._performIncrementalSync({
|
||||
imapId,
|
||||
folderId,
|
||||
syncToken,
|
||||
maxMessages
|
||||
});
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
output: {
|
||||
status: 'synced',
|
||||
data: syncData
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
duration
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'error',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
errorCode: 'IMAP_SYNC_ERROR',
|
||||
timestamp: Date.now(),
|
||||
duration: Date.now() - startTime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
validate(node: WorkflowNode): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (!node.parameters.imapId) {
|
||||
errors.push('IMAP ID is required');
|
||||
}
|
||||
|
||||
if (!node.parameters.folderId) {
|
||||
errors.push('Folder ID is required');
|
||||
}
|
||||
|
||||
if (node.parameters.maxMessages && typeof node.parameters.maxMessages !== 'number') {
|
||||
errors.push('maxMessages must be a number');
|
||||
}
|
||||
|
||||
if (node.parameters.maxMessages && node.parameters.maxMessages < 1) {
|
||||
errors.push('maxMessages must be at least 1');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings
|
||||
};
|
||||
}
|
||||
|
||||
private _performIncrementalSync(config: IMAPSyncConfig): SyncData {
|
||||
// Simulates IMAP incremental sync
|
||||
// In production, would:
|
||||
// 1. Connect to IMAP server using imapId credentials
|
||||
// 2. Use syncToken (IMAP UIDVALIDITY/UIDNEXT) for incremental fetch
|
||||
// 3. Fetch new messages since last sync
|
||||
// 4. Parse message headers and store in database
|
||||
// 5. Return new syncToken for next incremental sync
|
||||
|
||||
const syncErrors: Array<{ uid: string; error: string }> = [];
|
||||
|
||||
// Simulate fetching 5-25 new messages
|
||||
const messageCount = Math.floor(Math.random() * 20) + 5;
|
||||
const syncedCount = Math.min(messageCount, config.maxMessages);
|
||||
|
||||
// Simulate occasional errors (10% chance)
|
||||
if (Math.random() < 0.1) {
|
||||
syncErrors.push({
|
||||
uid: `msg-${Date.now()}`,
|
||||
error: 'Failed to parse message headers'
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
syncedCount,
|
||||
errors: syncErrors,
|
||||
newSyncToken: `token-${Date.now()}`,
|
||||
lastSyncAt: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const imapSyncExecutor = new IMAPSyncExecutor();
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
7
workflow/plugins/ts/integration/email/index.ts
Normal file
7
workflow/plugins/ts/integration/email/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Email Plugin Exports
|
||||
* Aggregates all email operation plugins for workflow execution
|
||||
*/
|
||||
|
||||
export { imapSyncExecutor, IMAPSyncExecutor, type IMAPSyncConfig, type SyncData } from './imap-sync/src/index';
|
||||
export { imapSearchExecutor, IMAPSearchExecutor, type IMAPSearchConfig, type SearchResult } from './imap-search/src/index';
|
||||
10
workflow/plugins/ts/integration/email/package.json
Normal file
10
workflow/plugins/ts/integration/email/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@metabuilder/workflow-plugins-email",
|
||||
"version": "1.0.0",
|
||||
"description": "Email operation plugins for MetaBuilder workflow engine (sync, search, parse)",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"imap-sync",
|
||||
"imap-search"
|
||||
]
|
||||
}
|
||||
109
workflow/plugins/ts/utility/email-parser/README.md
Normal file
109
workflow/plugins/ts/utility/email-parser/README.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Email Parser Plugin
|
||||
|
||||
RFC 5322 email message parser for MetaBuilder workflow engine.
|
||||
|
||||
## Plugin: email-parser
|
||||
|
||||
Parses RFC 5322 format email messages into structured components.
|
||||
|
||||
**Inputs:**
|
||||
- `message` (string, required) - Email message in RFC 5322 format
|
||||
- `includeAttachments` (boolean, optional, default: true) - Whether to extract attachment metadata
|
||||
|
||||
**Outputs:**
|
||||
- `status` (string) - 'parsed' or 'error'
|
||||
- `data` (object) - Contains:
|
||||
- `headers` (object) - Parsed email headers (from, to, cc, bcc, subject, date, messageId, etc.)
|
||||
- `body` (string) - Full message body as raw string
|
||||
- `textBody` (string, optional) - Plain text version
|
||||
- `htmlBody` (string, optional) - HTML version
|
||||
- `attachments` (array) - Attachment metadata with filename, mimeType, size, isInline
|
||||
- `parseTime` (number) - Timestamp of parsing
|
||||
|
||||
**Header Fields:**
|
||||
- `from` - Sender email address
|
||||
- `to` - Array of recipient addresses
|
||||
- `cc` - Array of CC recipients
|
||||
- `bcc` - Array of BCC recipients
|
||||
- `subject` - Email subject
|
||||
- `date` - RFC 2822 date string
|
||||
- `messageId` - Unique message identifier
|
||||
- `inReplyTo` - Message ID being replied to
|
||||
- `references` - Array of referenced message IDs
|
||||
- `contentType` - MIME content type
|
||||
- `contentTransferEncoding` - Encoding method
|
||||
|
||||
**Attachment Fields:**
|
||||
- `filename` - Original filename
|
||||
- `mimeType` - MIME type (e.g., image/png, application/pdf)
|
||||
- `size` - File size in bytes
|
||||
- `contentId` - Content-ID for inline attachments
|
||||
- `isInline` - Whether attachment is inline or regular
|
||||
|
||||
**Usage:**
|
||||
```json
|
||||
{
|
||||
"id": "parse-email",
|
||||
"nodeType": "email-parser",
|
||||
"parameters": {
|
||||
"message": "From: sender@example.com\nTo: recipient@example.com\nSubject: Test\n\nHello World",
|
||||
"includeAttachments": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Output Example:**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"output": {
|
||||
"status": "parsed",
|
||||
"data": {
|
||||
"headers": {
|
||||
"from": "sender@example.com",
|
||||
"to": ["recipient@example.com"],
|
||||
"subject": "Test",
|
||||
"date": "Wed, 23 Jan 2026 12:00:00 GMT"
|
||||
},
|
||||
"textBody": "Hello World",
|
||||
"htmlBody": null,
|
||||
"attachments": [],
|
||||
"parseTime": 1705978800000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **RFC 5322 Compliant** - Parses standard email format
|
||||
- **Multipart Support** - Handles multipart/mixed and multipart/alternative messages
|
||||
- **MIME Encoding** - Decodes MIME-encoded headers
|
||||
- **Attachment Extraction** - Identifies and extracts attachment metadata
|
||||
- **Header Normalization** - Normalizes common headers (from, to, cc, bcc, etc.)
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm run type-check
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Add to workflow package.json:
|
||||
|
||||
```json
|
||||
{
|
||||
"devDependencies": {
|
||||
"@metabuilder/workflow-plugin-email-parser": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- IMAP Sync: `/workflow/plugins/ts/integration/email/imap-sync/`
|
||||
- IMAP Search: `/workflow/plugins/ts/integration/email/imap-search/`
|
||||
- Email Send: `/workflow/plugins/ts/integration/email-send/`
|
||||
- Plugin Registry: `/workflow/plugins/ts/email-plugins.ts`
|
||||
11
workflow/plugins/ts/utility/email-parser/index.ts
Normal file
11
workflow/plugins/ts/utility/email-parser/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Email Parser Plugin Export
|
||||
*/
|
||||
|
||||
export {
|
||||
emailParserExecutor,
|
||||
EmailParserExecutor,
|
||||
type EmailHeaders,
|
||||
type EmailAttachment,
|
||||
type ParsedEmail
|
||||
} from './src/index';
|
||||
34
workflow/plugins/ts/utility/email-parser/package.json
Normal file
34
workflow/plugins/ts/utility/email-parser/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@metabuilder/workflow-plugin-email-parser",
|
||||
"version": "1.0.0",
|
||||
"description": "Email Parser node executor - RFC 5322 email message parsing",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"keywords": ["workflow", "plugin", "email", "parser", "rfc5322"],
|
||||
"author": "MetaBuilder Team",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@metabuilder/workflow": "^3.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/metabuilder/metabuilder.git",
|
||||
"directory": "workflow/plugins/ts/utility/email-parser"
|
||||
}
|
||||
}
|
||||
332
workflow/plugins/ts/utility/email-parser/src/index.ts
Normal file
332
workflow/plugins/ts/utility/email-parser/src/index.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* Email Parser Node Executor Plugin
|
||||
* Parses RFC 5322 format email messages
|
||||
*/
|
||||
|
||||
import {
|
||||
INodeExecutor,
|
||||
WorkflowNode,
|
||||
WorkflowContext,
|
||||
ExecutionState,
|
||||
NodeResult,
|
||||
ValidationResult
|
||||
} from '@metabuilder/workflow';
|
||||
|
||||
export interface EmailHeaders {
|
||||
from?: string;
|
||||
to?: string[];
|
||||
cc?: string[];
|
||||
bcc?: string[];
|
||||
subject?: string;
|
||||
date?: string;
|
||||
messageId?: string;
|
||||
inReplyTo?: string;
|
||||
references?: string[];
|
||||
contentType?: string;
|
||||
contentTransferEncoding?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface EmailAttachment {
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
size: number;
|
||||
contentId?: string;
|
||||
isInline: boolean;
|
||||
data?: Buffer;
|
||||
}
|
||||
|
||||
export interface ParsedEmail {
|
||||
headers: EmailHeaders;
|
||||
body: string;
|
||||
textBody?: string;
|
||||
htmlBody?: string;
|
||||
attachments: EmailAttachment[];
|
||||
parseTime: number;
|
||||
}
|
||||
|
||||
export class EmailParserExecutor implements INodeExecutor {
|
||||
nodeType = 'email-parser';
|
||||
|
||||
async execute(
|
||||
node: WorkflowNode,
|
||||
context: WorkflowContext,
|
||||
state: ExecutionState
|
||||
): Promise<NodeResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const { message, includeAttachments = true } = node.parameters as {
|
||||
message: string;
|
||||
includeAttachments?: boolean;
|
||||
};
|
||||
|
||||
if (!message) {
|
||||
throw new Error('Email Parser requires "message" parameter (RFC 5322 format)');
|
||||
}
|
||||
|
||||
if (typeof message !== 'string') {
|
||||
throw new Error('Message must be a string in RFC 5322 format');
|
||||
}
|
||||
|
||||
// Parse the email message
|
||||
const parsed = this._parseRFC5322Message(message, includeAttachments);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
output: {
|
||||
status: 'parsed',
|
||||
data: parsed
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
duration
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'error',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
errorCode: 'EMAIL_PARSE_ERROR',
|
||||
timestamp: Date.now(),
|
||||
duration: Date.now() - startTime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
validate(node: WorkflowNode): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (!node.parameters.message) {
|
||||
errors.push('Message is required');
|
||||
}
|
||||
|
||||
if (node.parameters.message && typeof node.parameters.message !== 'string') {
|
||||
errors.push('Message must be a string');
|
||||
}
|
||||
|
||||
// Check for minimum RFC 5322 structure
|
||||
if (node.parameters.message && typeof node.parameters.message === 'string') {
|
||||
const hasHeaders = node.parameters.message.includes(':');
|
||||
if (!hasHeaders) {
|
||||
warnings.push('Message does not appear to contain RFC 5322 headers');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings
|
||||
};
|
||||
}
|
||||
|
||||
private _parseRFC5322Message(message: string, includeAttachments: boolean): ParsedEmail {
|
||||
// Split headers from body
|
||||
const [headersSection, ...bodyLines] = message.split('\n\n');
|
||||
const bodySection = bodyLines.join('\n\n');
|
||||
|
||||
// Parse headers
|
||||
const headers = this._parseHeaders(headersSection);
|
||||
|
||||
// Split body into text and html parts
|
||||
const { textBody, htmlBody, attachments } = this._parseBody(
|
||||
bodySection,
|
||||
headers.contentType || '',
|
||||
includeAttachments
|
||||
);
|
||||
|
||||
return {
|
||||
headers,
|
||||
body: bodySection,
|
||||
textBody,
|
||||
htmlBody,
|
||||
attachments: includeAttachments ? attachments : [],
|
||||
parseTime: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
private _parseHeaders(headersSection: string): EmailHeaders {
|
||||
const headers: EmailHeaders = {};
|
||||
|
||||
const lines = headersSection.split('\n');
|
||||
let currentHeader = '';
|
||||
|
||||
for (const line of lines) {
|
||||
// Handle header line continuation
|
||||
if (line.startsWith(' ') || line.startsWith('\t')) {
|
||||
if (currentHeader) {
|
||||
headers[currentHeader] = (headers[currentHeader] || '') + ' ' + line.trim();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse header key-value pair
|
||||
const colonIndex = line.indexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
const key = line.substring(0, colonIndex).trim().toLowerCase();
|
||||
const value = line.substring(colonIndex + 1).trim();
|
||||
|
||||
// Handle multi-value headers
|
||||
switch (key) {
|
||||
case 'from':
|
||||
headers.from = this._extractEmail(value);
|
||||
break;
|
||||
case 'to':
|
||||
headers.to = this._extractEmails(value);
|
||||
break;
|
||||
case 'cc':
|
||||
headers.cc = this._extractEmails(value);
|
||||
break;
|
||||
case 'bcc':
|
||||
headers.bcc = this._extractEmails(value);
|
||||
break;
|
||||
case 'subject':
|
||||
headers.subject = this._decodeHeader(value);
|
||||
break;
|
||||
case 'date':
|
||||
headers.date = value;
|
||||
break;
|
||||
case 'message-id':
|
||||
headers.messageId = value;
|
||||
break;
|
||||
case 'in-reply-to':
|
||||
headers.inReplyTo = value;
|
||||
break;
|
||||
case 'references':
|
||||
headers.references = value.split(/\s+/).filter(Boolean);
|
||||
break;
|
||||
case 'content-type':
|
||||
headers.contentType = value;
|
||||
break;
|
||||
case 'content-transfer-encoding':
|
||||
headers.contentTransferEncoding = value;
|
||||
break;
|
||||
default:
|
||||
headers[key] = value;
|
||||
}
|
||||
|
||||
currentHeader = key;
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
private _parseBody(
|
||||
bodySection: string,
|
||||
contentType: string,
|
||||
includeAttachments: boolean
|
||||
): {
|
||||
textBody?: string;
|
||||
htmlBody?: string;
|
||||
attachments: EmailAttachment[];
|
||||
} {
|
||||
const attachments: EmailAttachment[] = [];
|
||||
|
||||
// Detect multipart content
|
||||
if (contentType.includes('multipart/mixed') || contentType.includes('multipart/alternative')) {
|
||||
const boundary = this._extractBoundary(contentType);
|
||||
if (boundary) {
|
||||
return this._parseMultipart(bodySection, boundary, includeAttachments);
|
||||
}
|
||||
}
|
||||
|
||||
// Single-part message
|
||||
if (contentType.includes('text/html')) {
|
||||
return { htmlBody: bodySection, attachments };
|
||||
} else if (contentType.includes('text/plain')) {
|
||||
return { textBody: bodySection, attachments };
|
||||
} else {
|
||||
return { textBody: bodySection, attachments };
|
||||
}
|
||||
}
|
||||
|
||||
private _parseMultipart(
|
||||
body: string,
|
||||
boundary: string,
|
||||
includeAttachments: boolean
|
||||
): {
|
||||
textBody?: string;
|
||||
htmlBody?: string;
|
||||
attachments: EmailAttachment[];
|
||||
} {
|
||||
const parts = body.split(`--${boundary}`);
|
||||
const attachments: EmailAttachment[] = [];
|
||||
let textBody: string | undefined;
|
||||
let htmlBody: string | undefined;
|
||||
|
||||
for (const part of parts) {
|
||||
if (!part || part.startsWith('--')) continue;
|
||||
|
||||
// Split part headers from content
|
||||
const [partHeaders, ...contentLines] = part.split('\n\n');
|
||||
const content = contentLines.join('\n\n').trim();
|
||||
|
||||
// Parse part headers
|
||||
const contentTypeMatch = partHeaders.match(/content-type:\s*([^;\n]+)/i);
|
||||
const contentDispositionMatch = partHeaders.match(/content-disposition:\s*(\w+)/i);
|
||||
const filenameMatch = partHeaders.match(/filename="?([^";\n]+)"?/i);
|
||||
|
||||
if (contentTypeMatch) {
|
||||
const partContentType = contentTypeMatch[1].trim();
|
||||
|
||||
if (contentDispositionMatch?.[1] === 'attachment' && includeAttachments) {
|
||||
// Parse attachment
|
||||
attachments.push({
|
||||
filename: filenameMatch?.[1] || 'unknown',
|
||||
mimeType: partContentType,
|
||||
size: content.length,
|
||||
isInline: false
|
||||
});
|
||||
} else if (partContentType.includes('text/html')) {
|
||||
htmlBody = content;
|
||||
} else if (partContentType.includes('text/plain')) {
|
||||
textBody = content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { textBody, htmlBody, attachments };
|
||||
}
|
||||
|
||||
private _extractBoundary(contentType: string): string | null {
|
||||
const match = contentType.match(/boundary="?([^";\n]+)"?/i);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
private _extractEmail(value: string): string {
|
||||
// Extract email address from "Name <email@domain.com>" format
|
||||
const match = value.match(/<(.+?)>/);
|
||||
return match ? match[1] : value.trim();
|
||||
}
|
||||
|
||||
private _extractEmails(value: string): string[] {
|
||||
// Extract multiple email addresses
|
||||
return value
|
||||
.split(',')
|
||||
.map(email => this._extractEmail(email))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
private _decodeHeader(value: string): string {
|
||||
// Decode MIME-encoded headers (simplified)
|
||||
// Format: =?charset?encoding?encoded-data?=
|
||||
const match = value.match(/=\?([^?]+)\?([^?]+)\?([^?]+)\?=/);
|
||||
if (match) {
|
||||
const [, charset, encoding, data] = match;
|
||||
if (encoding === 'B') {
|
||||
// Base64 decoding (Node.js Buffer)
|
||||
return Buffer.from(data, 'base64').toString(charset || 'utf-8');
|
||||
} else if (encoding === 'Q') {
|
||||
// Quoted-printable (simplified)
|
||||
return data.replace(/_/g, ' ').replace(/=([0-9A-F]{2})/g, (_, hex) => {
|
||||
return String.fromCharCode(parseInt(hex, 16));
|
||||
});
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export const emailParserExecutor = new EmailParserExecutor();
|
||||
9
workflow/plugins/ts/utility/email-parser/tsconfig.json
Normal file
9
workflow/plugins/ts/utility/email-parser/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user