Skip to content

Comments

feat: Add Email Trigger Node for IMAP email monitoring#26

Merged
Justin322322 merged 5 commits intomainfrom
feature/email-trigger-node
Sep 1, 2025
Merged

feat: Add Email Trigger Node for IMAP email monitoring#26
Justin322322 merged 5 commits intomainfrom
feature/email-trigger-node

Conversation

@Justin322322
Copy link
Owner

Summary

This pull request introduces a comprehensive Email Trigger Node that enables workflows to be automatically triggered by incoming emails from IMAP servers. The node provides robust email monitoring capabilities with extensive configuration options, making it ideal for automating email-based workflows such as support ticket processing, order confirmations, and any email-triggered automation.

Key Features

  • Full IMAP Support: Connect to any standard IMAP server (Gmail, Outlook, custom servers)
  • Flexible Email Filtering: Built-in criteria plus custom IMAP search rules via JSON configuration
  • Attachment Handling: Optional download and processing of email attachments
  • Multiple Output Formats: Choose between simple, resolved, or raw email formats
  • Post-Processing Actions: Automatically mark emails as read or leave them unread
  • Connection Management: Automatic reconnection, error handling, and TLS configuration
  • Security Options: Support for self-signed certificates and custom TLS settings
  • Comprehensive Testing: Full unit test coverage and integration tests

Technical Implementation

The EmailTriggerNode follows the established project architecture:

  • Service Layer: EmailTriggerService handles all IMAP operations using imap-simple and mailparser
  • Type Safety: Full TypeScript interfaces for configuration, email data, and execution results
  • Schema Validation: Comprehensive parameter definitions with defaults and validation
  • React Component: Visual representation in the workflow editor using the existing BaseNode pattern
  • Node Registry: Properly integrated into the central node management system

Configuration Options

The node supports 15+ configuration parameters including:

  • Basic connection settings (host, port, user, password, mailbox)
  • Security options (SSL/TLS, certificate validation)
  • Email processing (format, attachments, post-processing actions)
  • Advanced features (custom search rules, message tracking, reconnection)

Dependencies Added

  • imap-simple: IMAP client library for Node.js
  • mailparser: Email parsing and attachment extraction
  • @types/imap-simple: TypeScript definitions
  • @types/mailparser: TypeScript definitions

Screenshots / Videos (if UI changes)

The EmailTriggerNode integrates seamlessly with the existing workflow editor UI, displaying connection status, mailbox information, and configuration details through color-coded indicators and descriptive text.

Checklist

  • I ran npm run typecheck - All TypeScript errors resolved
  • I ran npm run lint - All ESLint errors resolved
  • I ran tests with npm test - All unit and integration tests pass
  • I built the project with npm run build - Build completes successfully
  • I updated docs (README.md / docs/) as needed - Comprehensive README added

Related Issues

This feature addresses the need for email-based workflow triggers, enabling users to automate processes based on incoming emails without requiring external webhook services.

Files Changed

New Files

  • nodes/EmailTriggerNode/EmailTriggerNode.types.ts - TypeScript interfaces and types
  • nodes/EmailTriggerNode/EmailTriggerNode.service.ts - Core IMAP service implementation
  • nodes/EmailTriggerNode/EmailTriggerNode.schema.ts - Node definition and configuration schema
  • nodes/EmailTriggerNode/EmailTriggerNode.tsx - React component for workflow editor
  • nodes/EmailTriggerNode/EmailTriggerNode.test.ts - Comprehensive unit tests
  • nodes/EmailTriggerNode/README.md - Detailed documentation and usage examples
  • nodes/EmailTriggerNode/index.ts - Barrel export file

Modified Files

  • types/workflow.ts - Added EMAIL to TriggerType enum
  • nodes/node-definitions.ts - Registered EmailTriggerNode in central registry
  • nodes/index.ts - Added EmailTriggerNode to main nodes export
  • tests/integration/node-registry.test.ts - Updated test expectations for new node
  • package.json - Added required dependencies

Testing

  • Unit Tests: 9 tests covering all EmailTriggerService methods
  • Integration Tests: Updated node registry tests to include new node
  • Type Checking: Full TypeScript compilation without errors
  • Linting: All ESLint rules satisfied with proper disable comments where needed
  • Build: Project builds successfully with new dependencies

Usage Examples

The node supports various email provider configurations:

// Gmail
{
  "host": "imap.gmail.com",
  "port": 993,
  "secure": true,
  "user": "user@gmail.com",
  "password": "app-password"
}

// Custom IMAP with filtering
{
  "host": "mail.company.com",
  "port": 993,
  "secure": true,
  "customEmailRules": "[\"FROM\", \"support@company.com\", \"SUBJECT\", \"urgent\"]"
}

Security Considerations

  • Supports app-specific passwords for providers like Gmail
  • Configurable SSL/TLS settings with certificate validation options
  • Secure credential handling recommendations in documentation
  • Production-ready with proper error handling and connection management

This implementation provides a robust foundation for email-based workflow automation while maintaining the project's high standards for code quality, testing, and documentation.

- Add EmailTriggerNode with full IMAP support
- Implement EmailTriggerService with connection management
- Add comprehensive configuration options and email parsing
- Include unit tests and integration tests
- Add detailed README documentation
- Update node registry and workflow types
- Add required dependencies (imap-simple, mailparser)
@vercel
Copy link

vercel bot commented Sep 1, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
nodey Ready Ready Preview Comment Sep 1, 2025 3:07pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 1, 2025

Summary by CodeRabbit

  • New Features
    • Added Email Trigger node to start workflows from incoming emails (IMAP). Supports SSL/TLS, mailbox selection, custom filtering rules, multiple output formats, and optional attachment downloads. Includes a visual node with status, icon, and color cues.
  • Improvements
    • More robust email sending with server-only enforcement, better validation, and clearer success/error results.
  • Documentation
    • New guide for configuring and using the Email Trigger node.
  • Tests
    • Comprehensive unit tests for email triggering; updated integration tests.
  • Chores
    • Added IMAP and mail parsing dependencies; minor lint/build config updates.

Walkthrough

Adds a server-only Email Trigger node: types, schema, React Flow node UI, IMAP-backed EmailTriggerService, tests, README, package deps, registry registration and export, webpack client fallbacks disabled, and ESLint/.mjs parser override.

Changes

Cohort / File(s) Summary
Email Trigger: Types & Schema
nodes/EmailTriggerNode/EmailTriggerNode.types.ts, nodes/EmailTriggerNode/EmailTriggerNode.schema.ts
New server-only flag and full EmailTrigger typings; NodeDefinition schema with IMAP params, validation, defaults, and server-aware service loading.
Email Trigger: UI Component & Barrel
nodes/EmailTriggerNode/EmailTriggerNode.tsx, nodes/EmailTriggerNode/index.ts
New memoized React Flow node component computing icon/color/description from config; barrel export re-exports component, types, and schema.
Server Service: IMAP Implementation
server/services/email-trigger.service.ts
New EmailTriggerService with connect/disconnect, fetchEmails, parseEmailMessage, markAsRead, UID tracking, TLS options, attachment handling, and connection state APIs.
Tests: Unit & Integration
nodes/EmailTriggerNode/EmailTriggerNode.test.ts, tests/integration/node-registry.test.ts
Vitest unit tests for EmailTriggerService behaviors and updated registry test expectations to include the new EMAIL trigger node.
Docs
nodes/EmailTriggerNode/README.md
New README covering configuration, examples, IMAP search rules, output shapes, security, troubleshooting, and usage examples.
Registry & Public API
nodes/index.ts, lib/node-definitions.ts
Re-exports EmailTriggerNode; registers EMAIL_TRIGGER_NODE_DEFINITION; NodeDefinition gains optional serverSideOnly flag; some imports changed to relative paths.
Email Sending & Types Adjustments
nodes/EmailNode/email-providers.ts, nodes/EmailNode/nodemailer-types.ts, nodes/EmailNode/EmailNode.test.ts
Server-only guards added for senders, simplified lazy nodemailer loader (server-only), transporter typing updates, test-time provider mocks, and small nodemailer-types change.
Build / Config / Lint
package.json, next.config.mjs, .eslintrc.json
Added imap-simple and mailparser deps and types; Next client webpack hook disables Node core fallbacks; ESLint override for .mjs files using espree and relaxed unsafe rules.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Registry as Node Registry
  participant UI as EmailTriggerNode (React)
  participant Engine as Workflow Engine
  participant Service as EmailTriggerService
  participant IMAP as IMAP Server

  User->>Registry: load node definitions
  Registry-->>User: includes EMAIL_TRIGGER_NODE_DEFINITION (serverSideOnly)
  User->>UI: render node with config
  Engine->>Service: connect(config)
  Service->>IMAP: establish TLS connection & open mailbox
  IMAP-->>Service: connected
  loop trigger cycle
    Engine->>Service: fetchEmails(config, lastMessageId?)
    Service->>IMAP: search UNSEEN + custom rules
    IMAP-->>Service: messages
    Service->>Service: parse messages (mailparser), download attachments?
    alt postProcessAction == read
      Service->>IMAP: mark as Seen
    end
    Service-->>Engine: return emails[], lastMessageId
  end
  Engine->>Service: disconnect()
  Service->>IMAP: close mailbox + end
  IMAP-->>Service: closed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

In my burrow I guard the IMAP gate,
TLS on my whiskers, fetching mail that's late.
I mark and parse, attachments in paw,
Hop! a trigger fires — workflows start to draw.
Nibble, hop, deliver — a rabbit's small hooray 🐇✉️

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/email-trigger-node

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/integration/node-registry.test.ts (2)

101-127: Defaults validation will fail for Email Trigger; update the allowlist

Email trigger defaults intentionally omit credentials; current test wrongly expects validation to pass. Include TriggerType.EMAIL among nodes that require user config.

-        const nodesRequiringConfig = [
-          ActionType.EMAIL,
-          ActionType.HTTP,
-          ActionType.DATABASE,
-          ActionType.TRANSFORM,
-          LogicType.IF,
-          LogicType.FILTER
-        ]
+        const nodesRequiringConfig = [
+          // Actions
+          ActionType.EMAIL,
+          ActionType.HTTP,
+          ActionType.DATABASE,
+          ActionType.TRANSFORM,
+          // Logic
+          LogicType.IF,
+          LogicType.FILTER,
+          // Triggers requiring credentials or external setup
+          TriggerType.EMAIL
+        ]
         if (!nodesRequiringConfig.includes(node.subType as ActionType | LogicType)) {          expect(errors).toEqual([])
         }

199-206: Cover TriggerType.EMAIL in enum coverage test

Add EMAIL to enum coverage to match the new node.

     it('should cover all TriggerType enum values', () => {
       const registeredTriggers = getNodesByType(NodeType.TRIGGER).map(n => n.subType)
       
       // Check that all important trigger types are covered
       expect(registeredTriggers).toContain(TriggerType.MANUAL)
       expect(registeredTriggers).toContain(TriggerType.SCHEDULE)
       expect(registeredTriggers).toContain(TriggerType.WEBHOOK)
+      expect(registeredTriggers).toContain(TriggerType.EMAIL)
     })
🧹 Nitpick comments (16)
nodes/index.ts (1)

126-129: Align getNodeDefinition signature with other helpers

Other helpers accept string | number for subtype; widen this one for consistency.

-export function getNodeDefinition(nodeType: NodeType, subType: string): NodeDefinition | undefined {
+export function getNodeDefinition(nodeType: NodeType, subType: string | number): NodeDefinition | undefined {
   const key = `${nodeType}-${subType}`
   return NODE_REGISTRY.get(key)
 }
nodes/EmailTriggerNode/index.ts (1)

1-4: Keep service export here; avoid re-export from root to prevent client bundling issues

This barrel is fine. The fix is to stop re-exporting this barrel from nodes/index.ts (see suggested change there).

If using Next.js, add a server-only guard at the top of EmailTriggerNode.service.ts:

import 'server-only'
nodes/EmailTriggerNode/README.md (1)

46-77: Update examples if you keep forceReconnect (or rename consistently)

Examples should reflect the chosen option name. If keeping forceReconnect, add it explicitly (or remove reconnectIntervalMinutes if present elsewhere).

   "mailbox": "INBOX",
   "postProcessAction": "read",
-  "downloadAttachments": true
+  "downloadAttachments": true,
+  "forceReconnect": 0
nodes/EmailTriggerNode/EmailTriggerNode.tsx (1)

34-41: Avoid showing undefined port; fall back to defaults in description.

If port is not set yet in config, the UI will render host:undefined. Use a sensible fallback (e.g., 993 when secure, 143 otherwise).

-    parts.push(`${config.host}:${config.port}`)
+    const port = typeof config.port === 'number' ? config.port : (secure ? 993 : 143)
+    parts.push(`${config.host}:${port}`)
nodes/EmailTriggerNode/EmailTriggerNode.schema.ts (1)

121-124: Clarify showIf semantics when providing multiple conditions.

If showIf is ANDed, downloadAttachments will never show unless format equals both 'simple' and 'resolved'. If it’s meant to be OR, encode accordingly (e.g., a single condition that accepts an array, or adjust renderer).

I can update the renderer or schema to express OR explicitly if needed.

nodes/EmailTriggerNode/EmailTriggerNode.service.ts (3)

73-83: Optional: de-duplicate search criteria.

If custom rules include UNSEEN, you’ll push it twice. Not harmful, but easy to avoid.

-        const customRules = JSON.parse(config.customEmailRules) as (string | string[])[]
-        searchCriteria.push(...customRules)
+        const customRules = JSON.parse(config.customEmailRules) as (string | string[])[]
+        for (const rule of customRules) {
+          if (rule === 'UNSEEN' && searchCriteria.includes('UNSEEN')) continue
+          searchCriteria.push(rule)
+        }

23-29: Prefer a project logger over console logging.

Use your centralized logger to capture onmail/onupdate events with levels and context.


220-231: Potential performance: avoid fetching all UIDs when only the last is needed.

Large mailboxes make search(['ALL']) expensive. Consider ['UID', '*'] or ['ALL'] with fetchOptions: { bodies: [], markSeen: false } and limit result set if the library supports it.

nodes/EmailTriggerNode/EmailTriggerNode.types.ts (8)

5-9: Support OAuth2/XOAUTH2 in addition to basic auth.

Many IMAP providers (notably Gmail) require OAuth2. Consider adding explicit auth fields while keeping backward compatibility.

Apply this diff:

   host: string
   port: number
-  secure: boolean
-  user: string
-  password: string
+  secure: boolean
+  user: string
+  password?: string
+  authMethod?: 'basic' | 'xoauth2'
+  xoauth2Token?: string

25-28: Clarify TLS settings; avoid double-negatives and add room for custom options.

allowUnauthorizedCerts is easy to misuse; enabled is ambiguous. Prefer explicit tlsOptions with rejectUnauthorized, and add startTLS if supported.

Apply this diff:

-  // TLS Settings
-  allowUnauthorizedCerts?: boolean
-  enabled?: boolean
+  // TLS Settings
+  // NOTE: `secure` above maps to IMAP TLS-on-connect. Add StartTLS and explicit options.
+  startTLS?: boolean
+  tlsOptions?: {
+    rejectUnauthorized?: boolean // default true
+    minVersion?: string
+    ca?: string
+    cert?: string
+    key?: string
+  }
+  /** @deprecated Use tlsOptions.rejectUnauthorized */
+  allowUnauthorizedCerts?: boolean
+  /** @deprecated Use `secure` or `startTLS` */
+  enabled?: boolean

17-21: Type customEmailRules as structured criteria, not JSON string.

Treating this as JSON text loses type-safety and defers errors to runtime.

Apply this diff:

-  customEmailRules?: string // JSON array of IMAP search criteria
+  // IMAP search criteria (e.g., ['UNSEEN', ['SINCE', '01-Aug-2025']])
+  customEmailRules?: string[] 

If you must accept JSON text from UI, keep a second field customEmailRulesJson?: string and parse/validate into string[] at the service boundary.


35-41: Use ISO string for timestamp for transport safety.

Date serializes to string anyway and can be inconsistent across boundaries (web workers, JSON).

Apply this diff:

-  timestamp: Date
+  timestamp: string // ISO 8601

43-57: Tighten message typing: headers multiplicity and id/uid semantics.

  • Headers can have multiple values; current Record<string, string> loses data.
  • Consider distinguishing RFC 2822 Message-ID from internal id.

Apply this diff:

-  id: string
+  /** Internal id for de-duplication (e.g., Message-ID or synthesized) */
+  id: string
+  /** RFC 2822 Message-ID header, if available */
+  messageId?: string
@@
-  headers: Record<string, string>
+  headers: Record<string, string | string[]>

Additionally, consider a discriminated union based on format to make raw required/others optional:

// outside this interface
export type EmailMessage =
  | ({ format: 'raw' } & BaseEmailMessage & { raw: string })
  | ({ format: 'resolved' | 'simple' } & BaseEmailMessage);

64-69: Avoid Buffer in shared types; use transport-friendly content.

Buffer ties types to Node and can bloat payloads. Prefer Uint8Array or base64.

Apply this diff:

-  content: Buffer
+  // Prefer one of these; service can map between them as needed
+  content?: Uint8Array
+  contentBase64?: string

If you keep Buffer internally, define a server-only type and map to a front-end-safe DTO.


15-16: Make format a discriminator for the result payload.

Today raw is optional even when format === 'raw'. Using a discriminated union prevents misuse at compile time.

I can provide a small refactor PR snippet to introduce BaseEmailMessage + union if you’d like.

Also applies to: 43-56


12-16: Post-processing action enum: allow future-safe values.

Consider an enum to centralize allowed actions and ease future additions (e.g., move/archive).

Apply this diff:

-  postProcessAction: 'read' | 'nothing'
+  postProcessAction: 'read' | 'nothing' // consider enum PostProcessAction
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between dec0bc2 and d46cb66.

📒 Files selected for processing (10)
  • nodes/EmailTriggerNode/EmailTriggerNode.schema.ts (1 hunks)
  • nodes/EmailTriggerNode/EmailTriggerNode.service.ts (1 hunks)
  • nodes/EmailTriggerNode/EmailTriggerNode.test.ts (1 hunks)
  • nodes/EmailTriggerNode/EmailTriggerNode.tsx (1 hunks)
  • nodes/EmailTriggerNode/EmailTriggerNode.types.ts (1 hunks)
  • nodes/EmailTriggerNode/README.md (1 hunks)
  • nodes/EmailTriggerNode/index.ts (1 hunks)
  • nodes/index.ts (3 hunks)
  • package.json (2 hunks)
  • tests/integration/node-registry.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
nodes/EmailTriggerNode/EmailTriggerNode.types.ts (1)
types/workflow.ts (1)
  • TriggerNodeData (56-59)
tests/integration/node-registry.test.ts (1)
nodes/index.ts (2)
  • getNodesByType (135-137)
  • NodeType (185-185)
nodes/EmailTriggerNode/EmailTriggerNode.service.ts (1)
nodes/EmailTriggerNode/EmailTriggerNode.types.ts (4)
  • EmailTriggerConfig (3-28)
  • EmailMessage (43-57)
  • EmailAttachment (64-69)
  • EmailAddress (59-62)
nodes/EmailTriggerNode/EmailTriggerNode.test.ts (2)
nodes/EmailTriggerNode/EmailTriggerNode.service.ts (2)
  • EmailTriggerService (8-240)
  • connect (12-53)
nodes/EmailTriggerNode/EmailTriggerNode.types.ts (1)
  • EmailTriggerConfig (3-28)
nodes/EmailTriggerNode/EmailTriggerNode.schema.ts (2)
nodes/index.ts (3)
  • ParameterDefinition (32-46)
  • NodeDefinition (50-72)
  • NodeType (185-185)
nodes/EmailTriggerNode/EmailTriggerNode.types.ts (1)
  • EmailTriggerConfig (3-28)
nodes/EmailTriggerNode/EmailTriggerNode.tsx (3)
nodes/EmailTriggerNode/EmailTriggerNode.types.ts (1)
  • EmailTriggerData (30-33)
types/workflow.ts (1)
  • WorkflowNodeData (71-71)
components/workflow/nodes/base-node.tsx (1)
  • BaseNode (22-146)
nodes/index.ts (1)
nodes/EmailTriggerNode/EmailTriggerNode.schema.ts (1)
  • EMAIL_TRIGGER_NODE_DEFINITION (29-256)
🪛 LanguageTool
nodes/EmailTriggerNode/README.md

[grammar] ~23-~23: There might be a mistake here.
Context: ...ype | Required | Default | Description | |-----------|------|----------|---------...

(QB_NEW_EN)


[grammar] ~24-~24: There might be a mistake here.
Context: ...----|----------|---------|-------------| | host | string | Yes | - | IMAP serve...

(QB_NEW_EN)


[grammar] ~25-~25: There might be a mistake here.
Context: ...- | IMAP server hostname or IP address | | port | number | Yes | - | IMAP serve...

(QB_NEW_EN)


[grammar] ~26-~26: There might be a mistake here.
Context: ...(usually 993 for SSL, 143 for non-SSL) | | secure | boolean | Yes | - | Whether...

(QB_NEW_EN)


[grammar] ~27-~27: There might be a mistake here.
Context: ... - | Whether to use SSL/TLS encryption | | user | string | Yes | - | Email acco...

(QB_NEW_EN)


[grammar] ~28-~28: There might be a mistake here.
Context: ...| Email account username/email address | | password | string | Yes | - | Email ...

(QB_NEW_EN)


[grammar] ~29-~29: There might be a mistake here.
Context: ...ount password or app-specific password | | mailbox | string | No | "INBOX" | Ma...

(QB_NEW_EN)


[grammar] ~188-~188: There might be a mistake here.
Context: ...simple: IMAP client library for Node.js - mailparser`: Email parsing and attachment extractio...

(QB_NEW_EN)


[grammar] ~189-~189: There might be a mistake here.
Context: ... Email parsing and attachment extraction - @types/imap-simple: TypeScript definitions for imap-simple...

(QB_NEW_EN)


[grammar] ~190-~190: There might be a mistake here.
Context: ...: TypeScript definitions for imap-simple - @types/mailparser: TypeScript definitions for mailparser ...

(QB_NEW_EN)


[grammar] ~195-~195: There might be a mistake here.
Context: ...Node**: Send emails as part of workflows - HTTP Node: Make API calls based on ema...

(QB_NEW_EN)


[grammar] ~196-~196: There might be a mistake here.
Context: ...*: Make API calls based on email content - Logic Nodes: Process email data and ma...

(QB_NEW_EN)


[grammar] ~197-~197: There might be a mistake here.
Context: ...*: Process email data and make decisions - Action Nodes: Perform actions based on...

(QB_NEW_EN)

🔇 Additional comments (6)
package.json (1)

56-57: Verify that @types/imap-simple (v4) aligns with imap-simple v5 API
Compare the exported signatures (connect, ImapSimpleOptions, Message) in node_modules/@types/imap-simple against the runtime library used in nodes/EmailTriggerNode.service.ts (lines 1–2) and the test imports in EmailTriggerNode.test.ts. If you detect any mismatches, either pin imap-simple to a known v4 release or provide local ambient type overrides.

tests/integration/node-registry.test.ts (1)

19-21: LGTM: node count updated to 11

The registry size expectation reflects the new Email Trigger node.

nodes/index.ts (2)

160-160: LGTM: imports Email Trigger definition for registration

Import path and naming align with the feature barrel.


173-173: LGTM: registers Email Trigger definition

Registration order looks consistent with other nodes.

nodes/EmailTriggerNode/EmailTriggerNode.test.ts (1)

137-151: Await end() in disconnect() and update test to catch unhandled rejections
Make disconnect() an async method that awaits this.client.end(), handling any errors. Then enhance the existing test to assert no unhandled promise rejections—e.g. spy on queueMicrotask (or register process.on('unhandledRejection')) and verify it’s never called when running await service.disconnect().

nodes/EmailTriggerNode/EmailTriggerNode.types.ts (1)

1-76: Overall: solid, idiomatic types; good separation of config/result/message.

Nice coverage of the node’s data surface and alignment with TriggerNodeData.

Comment on lines 58 to 61
await this.connection.closeBox(false)
this.connection.end()
} catch (error) {
console.error('Error disconnecting from IMAP:', error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Await end() to avoid unhandled rejections during disconnect.

If end() returns a Promise (or is mocked as such), not awaiting it can surface unhandled rejections.

-        this.connection.end()
+        await this.connection.end()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await this.connection.closeBox(false)
this.connection.end()
} catch (error) {
console.error('Error disconnecting from IMAP:', error)
await this.connection.closeBox(false)
await this.connection.end()
} catch (error) {
console.error('Error disconnecting from IMAP:', error)
🤖 Prompt for AI Agents
In nodes/EmailTriggerNode/EmailTriggerNode.service.ts around lines 58 to 61, the
code calls this.connection.end() without awaiting it, which can cause unhandled
promise rejections if end() returns a Promise; change the call to await
this.connection.end() and ensure it's inside the existing try/catch (or add a
dedicated try/catch) so any rejection from end() is caught and logged
consistently with the existing error handling.

Comment on lines 85 to 88
// Filter by last message ID if tracking is enabled
if (config.trackLastMessageId !== false && lastMessageId) {
searchCriteria.push(['UID', `${lastMessageId}:*`])
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

UID range should start after the last processed UID to prevent reprocessing.

lastMessageId:* can include the last processed message again (especially if you don’t mark as read). Increment to lastMessageId + 1.

-    if (config.trackLastMessageId !== false && lastMessageId) {
-      searchCriteria.push(['UID', `${lastMessageId}:*`])
+    if (config.trackLastMessageId !== false && lastMessageId) {
+      searchCriteria.push(['UID', `${lastMessageId + 1}:*`])
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Filter by last message ID if tracking is enabled
if (config.trackLastMessageId !== false && lastMessageId) {
searchCriteria.push(['UID', `${lastMessageId}:*`])
}
// Filter by last message ID if tracking is enabled
if (config.trackLastMessageId !== false && lastMessageId) {
searchCriteria.push(['UID', `${lastMessageId + 1}:*`])
}
🤖 Prompt for AI Agents
In nodes/EmailTriggerNode/EmailTriggerNode.service.ts around lines 85 to 88, the
UID range filter currently uses `${lastMessageId}:*` which can include the last
processed message; change the range to start at lastMessageId + 1 (e.g.,
`${lastMessageId + 1}:*`) so the search excludes the already-processed UID and
prevents reprocessing; ensure you convert the incremented value to a string and
handle numeric/string types safely when building the UID range.

Comment on lines 90 to 97
const messages = await this.connection.search(searchCriteria, {
bodies: ['HEADER', 'TEXT'],
markSeen: config.postProcessAction === 'read',
})

const emails: EmailMessage[] = []

for (const message of messages) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Body fetch doesn’t match parsing; results in “No message body found”.

You search with bodies: ['HEADER','TEXT'] but later look for part.which === ''. That will always fail. Fetch the full body with [''] (or align parser to use the fetched parts).

-    const messages = await this.connection.search(searchCriteria, {
-      bodies: ['HEADER', 'TEXT'],
+    const messages = await this.connection.search(searchCriteria, {
+      bodies: [''],
       markSeen: config.postProcessAction === 'read',
     })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const messages = await this.connection.search(searchCriteria, {
bodies: ['HEADER', 'TEXT'],
markSeen: config.postProcessAction === 'read',
})
const emails: EmailMessage[] = []
for (const message of messages) {
const messages = await this.connection.search(searchCriteria, {
bodies: [''],
markSeen: config.postProcessAction === 'read',
})
🤖 Prompt for AI Agents
In nodes/EmailTriggerNode/EmailTriggerNode.service.ts around lines 90 to 97, the
IMAP fetch requests use bodies: ['HEADER','TEXT'] but later parsing expects
parts with part.which === '' (full body), so the body lookup always fails;
change the fetch to request the full body (bodies: ['']) or alternatively update
the parser to inspect the same part identifiers you fetch (e.g., check for
part.which === 'TEXT' or 'HEADER'), and keep markSeen behavior unchanged.

Comment on lines 142 to 152
const attachments: EmailAttachment[] = []
if (config.downloadAttachments && parsed.attachments) {
for (const attachment of parsed.attachments) {
if (attachment.type === 'attachment') {
attachments.push({
filename: attachment.filename || 'attachment',
contentType: attachment.contentType || 'application/octet-stream',
size: attachment.size || 0,
content: attachment.content as Buffer,
})
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Attachment filter uses a non-existent property; attachments won’t be captured.

mailparser attachments expose contentDisposition ('attachment' | 'inline'), not type. Filter on contentDisposition === 'attachment'.

-    if (config.downloadAttachments && parsed.attachments) {
+    if (config.downloadAttachments && parsed.attachments) {
       for (const attachment of parsed.attachments) {
-        if (attachment.type === 'attachment') {
+        if ((attachment as any).contentDisposition === 'attachment' || !(attachment as any).contentDisposition) {
           attachments.push({
             filename: attachment.filename || 'attachment',
             contentType: attachment.contentType || 'application/octet-stream',
             size: attachment.size || 0,
             content: attachment.content as Buffer,
           })
         }
       }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const attachments: EmailAttachment[] = []
if (config.downloadAttachments && parsed.attachments) {
for (const attachment of parsed.attachments) {
if (attachment.type === 'attachment') {
attachments.push({
filename: attachment.filename || 'attachment',
contentType: attachment.contentType || 'application/octet-stream',
size: attachment.size || 0,
content: attachment.content as Buffer,
})
}
const attachments: EmailAttachment[] = []
if (config.downloadAttachments && parsed.attachments) {
for (const attachment of parsed.attachments) {
if ((attachment as any).contentDisposition === 'attachment' || !(attachment as any).contentDisposition) {
attachments.push({
filename: attachment.filename || 'attachment',
contentType: attachment.contentType || 'application/octet-stream',
size: attachment.size || 0,
content: attachment.content as Buffer,
})
}
}
}
🤖 Prompt for AI Agents
In nodes/EmailTriggerNode/EmailTriggerNode.service.ts around lines 142 to 152,
the attachment filter incorrectly checks attachment.type which doesn't exist on
mailparser attachments; change the filter to check attachment.contentDisposition
=== 'attachment' (or treat undefined as not an attachment) so actual attachments
are captured, leaving the rest of the attachment mapping intact (filename,
contentType, size, content).

Comment on lines +178 to +184
expect(mockConnection.search).toHaveBeenCalledWith(
['UNSEEN', 'UNSEEN', ['SINCE', '1-Jan-2024']],
{
bodies: ['HEADER', 'TEXT'],
markSeen: true,
}
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Test currently asserts duplicated UNSEEN; prefer deduped criteria and full-body fetch.

Service should not push a second UNSEEN, and should fetch '' body to satisfy parsing. Update expectations accordingly.

-      expect(mockConnection.search).toHaveBeenCalledWith(
-        ['UNSEEN', 'UNSEEN', ['SINCE', '1-Jan-2024']],
-        {
-          bodies: ['HEADER', 'TEXT'],
-          markSeen: true,
-        }
-      )
+      expect(mockConnection.search).toHaveBeenCalledWith(
+        ['UNSEEN', ['SINCE', '1-Jan-2024']],
+        {
+          bodies: [''],
+          markSeen: true,
+        }
+      )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(mockConnection.search).toHaveBeenCalledWith(
['UNSEEN', 'UNSEEN', ['SINCE', '1-Jan-2024']],
{
bodies: ['HEADER', 'TEXT'],
markSeen: true,
}
)
expect(mockConnection.search).toHaveBeenCalledWith(
['UNSEEN', ['SINCE', '1-Jan-2024']],
{
bodies: [''],
markSeen: true,
}
)
🤖 Prompt for AI Agents
In nodes/EmailTriggerNode/EmailTriggerNode.test.ts around lines 178 to 184, the
test incorrectly expects duplicated 'UNSEEN' and requests 'TEXT' body; update
the expectation to dedupe the search criteria and request the full message body
by asserting mockConnection.search was called with ['UNSEEN', ['SINCE',
'1-Jan-2024']] (no duplicate UNSEEN) and options { bodies: ['HEADER', ''],
markSeen: true } so the service only sends one UNSEEN and fetches the full body
for parsing.

Comment on lines +34 to +45
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `enabled` | boolean | No | true | Whether the trigger is active |
| `postProcessAction` | string | No | "nothing" | Action after processing: "read" or "nothing" |
| `downloadAttachments` | boolean | No | false | Whether to download email attachments |
| `format` | string | No | "simple" | Output format: "simple", "resolved", or "raw" |
| `attachmentPrefix` | string | No | "attachment_" | Prefix for attachment filenames |
| `trackLastMessageId` | boolean | No | true | Whether to track the last processed message ID |
| `allowUnauthorizedCerts` | boolean | No | false | Allow self-signed or invalid SSL certificates |
| `reconnectIntervalMinutes` | number | No | 5 | Minutes between reconnection attempts |
| `customEmailRules` | string | No | - | JSON string of custom IMAP search criteria |

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Docs/schema mismatches: defaults and option names diverge

The README lists defaults/option names that don’t match the schema:

  • postProcessAction default "nothing" vs schema default "read"
  • reconnectIntervalMinutes vs schema parameter name forceReconnect (no default in getDefaults)
  • secure marked Required: Yes with no default, but schema has required: false with defaultValue: true

Please align docs to the code.

- | `secure` | boolean | Yes | - | Whether to use SSL/TLS encryption |
+ | `secure` | boolean | No | true | Whether to use SSL/TLS encryption |
...
- | `postProcessAction` | string | No | "nothing" | Action after processing: "read" or "nothing" |
+ | `postProcessAction` | string | No | "read" | Action after processing: "read" or "nothing" |
...
- | `reconnectIntervalMinutes` | number | No | 5 | Minutes between reconnection attempts |
+ | `forceReconnect` | number | No | 0 | Force reconnection every N minutes (0 to disable)

If you prefer the reconnectIntervalMinutes name and default=5, update the schema instead:

  • Rename parameter name to reconnectIntervalMinutes
  • Add defaultValue: 5 in getDefaults()
  • Adjust validation message accordingly

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In nodes/EmailTriggerNode/README.md around lines 34 to 45, the README defaults
and option names must match the schema: change the postProcessAction default
from "nothing" to "read"; replace the reconnectIntervalMinutes row with the
schema parameter name forceReconnect (no default value) and adjust its Type to
match the schema; update the secure row to Required: No and Default: true to
reflect schema defaultValue; ensure any validation text or descriptions
reference the schema names and defaults exactly so docs and code are aligned.


// Import all nodes
export * from './EmailNode'
export * from './EmailTriggerNode'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Do not re-export server-only EmailTrigger service via the central barrel

Re-exporting the EmailTrigger barrel from the root nodes index can cause client bundles to import Node-only deps (imap-simple/mailparser). Curate the exports to UI/schema only here; keep the service export confined to the feature barrel for server-side imports.

-export * from './EmailTriggerNode'
+// Avoid leaking server-only service into client bundles from the root barrel.
+export { EmailTriggerNode } from './EmailTriggerNode'
+export { EMAIL_TRIGGER_NODE_DEFINITION } from './EmailTriggerNode'
+// If you need types, re-export them explicitly:
+// export type { EmailTriggerConfig, EmailMessage, EmailAttachment } from './EmailTriggerNode'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export * from './EmailTriggerNode'
// nodes/index.ts
// Avoid leaking server-only service into client bundles from the root barrel.
// Export only the definitions needed by the UI/schema.
export { EmailTriggerNode } from './EmailTriggerNode'
export { EMAIL_TRIGGER_NODE_DEFINITION } from './EmailTriggerNode'
// If you need types in the client, re-export them explicitly:
// export type { EmailTriggerConfig, EmailMessage, EmailAttachment } from './EmailTriggerNode'
🤖 Prompt for AI Agents
In nodes/index.ts around line 8, the file currently re-exports the server-only
EmailTrigger service which pulls in imap-simple/mailparser into client bundles;
remove the blanket "export * from './EmailTriggerNode'" line and instead export
only the UI/schema exports required by the client (e.g. the node's UI component
and schema types) from this barrel, and keep the server-only service
implementation export confined to the feature's server-side barrel (move or
re-add the full EmailTriggerNode export in the server/feature barrel).

Comment on lines +39 to +41
"imap-simple": "^5.1.0",
"lucide-react": "^0.370.0",
"mailparser": "^3.7.4",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Ensure imap-simple/mailparser remain server-only to avoid Next client bundling errors

Because nodes/index.ts re-exports the EmailTrigger barrel, client bundles that import from '@/nodes' may try to resolve Node built-ins (net, tls, fs) via imap-simple/mailparser and fail. I’ve proposed a registry export change in nodes/index.ts to prevent leaking server-only modules into client code. Please apply that to be safe.

Run to confirm no client component imports '@/nodes' in a way that drags server-only code:


🏁 Script executed:

#!/bin/bash
# Find client components importing from '@/nodes' or the trigger barrel
rg -nP -C2 '^[\'"]use client[\'"]' --type=ts --type=tsx | awk -F: '{print $1}' | sort -u | while read f; do
  echo "---- $f"; rg -nP "from ['\"]@/nodes(?:/EmailTriggerNode)?['\"]" "$f" -C2 || true
done

Length of output: 271


Use a simpler grep-based check to avoid shell‐regex quoting issues:


🏁 Script executed:

#!/bin/bash
# Find TS/TSX files with a "use client" directive and check for imports from @/nodes
grep -Rnl --include='*.ts' --include='*.tsx' "^[[:space:]]*['\"]use client['\"]" . | sort -u | while read -r f; do
  echo "---- $f"
  grep -nE "from [\"']@/nodes" "$f" || true
done

Length of output: 8190


🏁 Script executed:

#!/bin/bash
# List directory tree under nodes/ and inspect nodes/index.ts
if [ -d nodes ]; then
  echo "Directory tree under nodes/:"
  ls -R nodes
else
  echo "No 'nodes' directory found."
fi

echo
# Inspect the contents of nodes/index.ts if it exists
if [ -f nodes/index.ts ]; then
  echo "Contents of nodes/index.ts:"
  sed -n '1,200p' nodes/index.ts
else
  echo "nodes/index.ts not found."
fi

Length of output: 8449


Scope server-only exports to avoid client bundling errors
In nodes/index.ts, remove the line

export * from './EmailTriggerNode'

and relocate the EmailTriggerNode export (and its imap-simple/mailparser imports) into a separate server-only entrypoint so client bundles importing from '@/nodes' won’t pull in Node-built-in dependencies.

🤖 Prompt for AI Agents
In package.json around lines 39 to 41 (and in nodes/index.ts), avoid exporting
EmailTriggerNode from the shared nodes barrel so client bundles don't pull in
Node-only deps: remove the line `export * from './EmailTriggerNode'` from
nodes/index.ts and create a new server-only entrypoint (e.g.,
nodes/server-only.ts or nodes/email-node.server.ts) that imports imap-simple and
mailparser and re-exports EmailTriggerNode; update any server-side imports to
import from that new server-only path and do not add the server-only file to the
client bundle (use explicit import paths or conditional exports in
package.json/tsconfig paths to ensure the client barrel '@/nodes' remains free
of Node built-ins).

Comment on lines +27 to 30
expect(triggerNodes).toHaveLength(4) // MANUAL, SCHEDULE, WEBHOOK, EMAIL
expect(actionNodes).toHaveLength(5) // EMAIL, HTTP, DATABASE, TRANSFORM, DELAY
expect(logicNodes).toHaveLength(2) // IF, FILTER
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Also assert EMAIL trigger is present

You updated the trigger count to 4 but didn’t assert EMAIL’s presence below. Add explicit expectations for robustness.

-      expect(triggerNodes).toHaveLength(4) // MANUAL, SCHEDULE, WEBHOOK, EMAIL
+      expect(triggerNodes).toHaveLength(4) // MANUAL, SCHEDULE, WEBHOOK, EMAIL

…and in the trigger test below (Lines 32–41):

     it('should have all trigger nodes registered', () => {
       expect(isNodeRegistered(NodeType.TRIGGER, TriggerType.MANUAL)).toBe(true)
       expect(isNodeRegistered(NodeType.TRIGGER, TriggerType.SCHEDULE)).toBe(true)
       expect(isNodeRegistered(NodeType.TRIGGER, TriggerType.WEBHOOK)).toBe(true)
+      expect(isNodeRegistered(NodeType.TRIGGER, TriggerType.EMAIL)).toBe(true)
       
       expect(getNodeDefinition(NodeType.TRIGGER, TriggerType.MANUAL)).toBeDefined()
       expect(getNodeDefinition(NodeType.TRIGGER, TriggerType.SCHEDULE)).toBeDefined()
       expect(getNodeDefinition(NodeType.TRIGGER, TriggerType.WEBHOOK)).toBeDefined()
+      expect(getNodeDefinition(NodeType.TRIGGER, TriggerType.EMAIL)).toBeDefined()
     })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(triggerNodes).toHaveLength(4) // MANUAL, SCHEDULE, WEBHOOK, EMAIL
expect(actionNodes).toHaveLength(5) // EMAIL, HTTP, DATABASE, TRANSFORM, DELAY
expect(logicNodes).toHaveLength(2) // IF, FILTER
})
expect(triggerNodes).toHaveLength(4) // MANUAL, SCHEDULE, WEBHOOK, EMAIL
expect(actionNodes).toHaveLength(5) // EMAIL, HTTP, DATABASE, TRANSFORM, DELAY
expect(logicNodes).toHaveLength(2) // IF, FILTER
})
it('should have all trigger nodes registered', () => {
expect(isNodeRegistered(NodeType.TRIGGER, TriggerType.MANUAL)).toBe(true)
expect(isNodeRegistered(NodeType.TRIGGER, TriggerType.SCHEDULE)).toBe(true)
expect(isNodeRegistered(NodeType.TRIGGER, TriggerType.WEBHOOK)).toBe(true)
expect(isNodeRegistered(NodeType.TRIGGER, TriggerType.EMAIL)).toBe(true)
expect(getNodeDefinition(NodeType.TRIGGER, TriggerType.MANUAL)).toBeDefined()
expect(getNodeDefinition(NodeType.TRIGGER, TriggerType.SCHEDULE)).toBeDefined()
expect(getNodeDefinition(NodeType.TRIGGER, TriggerType.WEBHOOK)).toBeDefined()
expect(getNodeDefinition(NodeType.TRIGGER, TriggerType.EMAIL)).toBeDefined()
})
🤖 Prompt for AI Agents
In tests/integration/node-registry.test.ts around lines 27–30 (and the related
trigger test at lines 32–41), you updated the trigger count to 4 but did not
assert the EMAIL trigger is present; modify the assertions to explicitly check
that triggerNodes includes an EMAIL trigger (e.g., find a node with type or id
'EMAIL' and expect it toBeDefined/true) and add a matching expectation in the
trigger-specific test block so the EMAIL trigger’s presence is asserted directly
for robustness.

- Move EmailTriggerService to server-side only location
- Update Next.js webpack config to exclude Node.js modules
- Add ESLint configuration for .mjs files
- Fix server-side import handling with error catching
- Ensure EmailTriggerNode is properly server-side only
- Update all tests and integrations to work with new structure
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

♻️ Duplicate comments (1)
nodes/index.ts (1)

8-8: Do not re-export server-only EmailTrigger barrel from the public index

Prevents accidental client import of server-only deps (imap-simple/mailparser). Export only UI/schema/types.

-export * from './EmailTriggerNode'
+// Avoid leaking server-only service into client bundles from the root barrel.
+export { EMAIL_TRIGGER_NODE_DEFINITION } from './EmailTriggerNode/EmailTriggerNode.schema'
+export type {
+  EmailTriggerConfig,
+  EmailMessage,
+  EmailAttachment,
+} from './EmailTriggerNode/EmailTriggerNode.types'
🧹 Nitpick comments (13)
.eslintrc.json (1)

39-53: Broaden the .mjs override and set Node env to match typical config files

Most .mjs/.cjs files (e.g., next.config.mjs, tailwind.config.cjs) run in Node. Include .cjs and enable the Node environment to avoid false positives (globals, module APIs). Optionally, you could later dedupe by merging the JS and MJS overrides, but this keeps scope minimal.

Apply this diff within this override:

-      "files": ["*.mjs"],
+      "files": ["*.mjs", "*.cjs"],
       "parser": "espree",
       "parserOptions": {
         "ecmaVersion": "latest",
         "sourceType": "module"
       },
+      "env": {
+        "node": true
+      },
       "rules": {

Then run eslint against representative config files to confirm no new errors (e.g., next.config.mjs, tailwind.config.cjs).

next.config.mjs (1)

16-29: Consider aliasing server-only libs instead of blanket core fallbacks

If the intent is to keep imap-simple/mailparser out of client bundles, alias those packages to false on the client to reduce blast radius; other libs might rely on crypto/buffer in the browser.

-    config.resolve.fallback = {
+    config.resolve.alias = {
+      ...(config.resolve.alias ?? {}),
+      'imap-simple': false,
+      mailparser: false,
+    }
+    config.resolve.fallback = {
       ...(config.resolve.fallback ?? {}),
       net: false,
       tls: false,
       fs: false,
-      crypto: false,
-      stream: false,
-      buffer: false,
-      util: false,
-      url: false,
-      path: false,
-      os: false,
-      child_process: false,
+      // Keep only those you know you need to block
+      child_process: false,
     }
server/services/email-trigger.service.ts (3)

1-4: Deduplicate imports from imap-simple

Minor cleanup.

-import { ImapSimple, ImapSimpleOptions, Message } from 'imap-simple'
-import { connect } from 'imap-simple'
-import { simpleParser, ParsedMail } from 'mailparser'
+import { ImapSimple, ImapSimpleOptions, Message, connect } from 'imap-simple'
+import { simpleParser, ParsedMail } from 'mailparser'

220-231: Avoid O(n) mailbox scans to get last UID

For large mailboxes, searching ALL to compute max UID is expensive. Prefer STATUS to read UIDNEXT (then last UID = UIDNEXT - 1) or a targeted UID search.

I can propose a STATUS-based implementation tailored to imap-simple/node-imap if desired.


55-66: Disconnect path: ensure end() completes before clearing state

Rare, but calling end() is async-evented; consider awaiting a promise around 'end' or listening for 'end/close' to avoid races if reconnecting immediately.

nodes/index.ts (1)

175-177: Order-independent, but consider gated registration in client builds

If this module is used on the client, guard registration of server-only nodes to avoid any side-effects.

-registerNode(EMAIL_TRIGGER_NODE_DEFINITION)
+// Only register server-only nodes on the server
+if (typeof window === 'undefined') {
+  registerNode(EMAIL_TRIGGER_NODE_DEFINITION)
+}
nodes/EmailTriggerNode/EmailTriggerNode.schema.ts (7)

3-3: Drop unused import.

ParameterDefinition isn't referenced here.

-import type { NodeDefinition, ParameterDefinition } from '@/nodes'
+import type { NodeDefinition } from '@/nodes'

20-20: Strongly type the node definition with its config.

This tightens type-checking for defaults/validate.

-export const EMAIL_TRIGGER_NODE_DEFINITION: NodeDefinition = {
+export const EMAIL_TRIGGER_NODE_DEFINITION: NodeDefinition<EmailTriggerConfig> = {

159-165: Hide “Allow Unauthorized Certificates” unless TLS is enabled.

Prevents confusing/non-applicable config when secure=false.

   {
     name: 'allowUnauthorizedCerts',
     label: 'Allow Unauthorized Certificates',
     type: 'boolean',
     required: false,
     defaultValue: false,
-    description: 'Allow connections to servers with invalid/expired SSL certificates (not recommended for production)'
+    description: 'Allow connections to servers with invalid/expired SSL certificates (not recommended for production)',
+    showIf: [{ name: 'secure', equals: true }]
   },

186-189: Stricter port validation (integer 1–65535).

Covers NaN and non-integers.

-    if (!typed.port || typeof typed.port !== 'number' || typed.port < 1 || typed.port > 65535) {
+    if (!Number.isInteger(typed.port) || typed.port < 1 || typed.port > 65535) {
       errors.push('Valid port number is required (1-65535)')
     }

216-223: Validate customEmailRules shape (must be a JSON array).

Currently only checks JSON validity; enforce array contract.

-    if (typed.customEmailRules && typeof typed.customEmailRules === 'string') {
-      try {
-        JSON.parse(typed.customEmailRules)
-      } catch {
-        errors.push('Custom email rules must be valid JSON')
-      }
-    }
+    if (typed.customEmailRules && typeof typed.customEmailRules === 'string') {
+      try {
+        const parsed = JSON.parse(typed.customEmailRules)
+        if (!Array.isArray(parsed)) {
+          errors.push('Custom email rules must be a JSON array')
+        }
+      } catch {
+        errors.push('Custom email rules must be valid JSON')
+      }
+    }

119-129: Add attachmentPrefix validation and tighten forceReconnect.

  • Ensure prefix is a string when downloads are enabled.
  • Require integer minutes for forceReconnect.
+    // Validate attachmentPrefix
+    if (typed.downloadAttachments && typed.attachmentPrefix !== undefined && typeof typed.attachmentPrefix !== 'string') {
+      errors.push('Attachment prefix must be a string')
+    }
@@
-    if (typed.forceReconnect !== undefined) {
-      if (typeof typed.forceReconnect !== 'number' || typed.forceReconnect < 0) {
-        errors.push('Force reconnect interval must be a positive number or 0 to disable')
-      }
-    }
+    if (typed.forceReconnect !== undefined) {
+      if (!Number.isInteger(typed.forceReconnect) || typed.forceReconnect < 0) {
+        errors.push('Force reconnect interval must be an integer >= 0 (minutes)')
+      }
+    }

Also applies to: 225-230


94-104: Optionally clarify “Resolved (With Attachments)”.

Service currently includes attachments based on downloadAttachments, not format. Consider clarifying the label/description or wiring resolved to imply downloads.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d46cb66 and 1e99d2e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • .eslintrc.json (1 hunks)
  • next.config.mjs (1 hunks)
  • nodes/EmailTriggerNode/EmailTriggerNode.schema.ts (1 hunks)
  • nodes/EmailTriggerNode/EmailTriggerNode.test.ts (1 hunks)
  • nodes/EmailTriggerNode/EmailTriggerNode.types.ts (1 hunks)
  • nodes/EmailTriggerNode/index.ts (1 hunks)
  • nodes/index.ts (4 hunks)
  • server/services/email-trigger.service.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • nodes/EmailTriggerNode/index.ts
  • nodes/EmailTriggerNode/EmailTriggerNode.test.ts
  • nodes/EmailTriggerNode/EmailTriggerNode.types.ts
🧰 Additional context used
🧬 Code graph analysis (3)
server/services/email-trigger.service.ts (1)
nodes/EmailTriggerNode/EmailTriggerNode.types.ts (4)
  • EmailTriggerConfig (6-31)
  • EmailMessage (46-60)
  • EmailAttachment (67-72)
  • EmailAddress (62-65)
nodes/index.ts (1)
nodes/EmailTriggerNode/EmailTriggerNode.schema.ts (1)
  • EMAIL_TRIGGER_NODE_DEFINITION (20-249)
nodes/EmailTriggerNode/EmailTriggerNode.schema.ts (3)
server/services/email-trigger.service.ts (1)
  • EmailTriggerService (8-240)
nodes/index.ts (2)
  • NodeDefinition (50-75)
  • NodeType (188-188)
nodes/EmailTriggerNode/EmailTriggerNode.types.ts (2)
  • EMAIL_TRIGGER_NODE_IS_SERVER_ONLY (4-4)
  • EmailTriggerConfig (6-31)
🪛 GitHub Actions: CI
nodes/EmailTriggerNode/EmailTriggerNode.schema.ts

[error] 11-11: Cannot find module '@/server/services/email-trigger.service' required by EmailTriggerNode.schema.ts.

🔇 Additional comments (5)
server/services/email-trigger.service.ts (1)

23-29: Verify imap-simple supports onmail/onupdate in connect options

Ensure these callbacks are actually wired by the library; otherwise subscribe on the underlying imap instance after connect.

Would you like me to check the latest imap-simple docs and propose the correct event wiring?

nodes/index.ts (1)

72-75: LGTM: serverSideOnly flag is a clean way to gate UI/runtime behavior

This metadata will help the UI filter nodes for client use.

nodes/EmailTriggerNode/EmailTriggerNode.schema.ts (3)

27-68: IMAP connection parameter set looks good.

Clear labels, sensible defaults, and required markers align with expected UX.


1-2: Heads-up: path alias consistency.

You import from @/types/workflow successfully, but the dynamic require for @/server/... failed in CI. Ensure test/build resolvers (ts-jest/moduleNameMapper/tsconfig paths) handle all @/* imports if you reintroduce server imports later.


92-105: showIf uses OR semantics by default—no change needed.

Likely an incorrect or invalid review comment.

Comment on lines +14 to +19
if (!isServer) {
// Exclude Node.js-specific packages from client-side bundling
config.resolve.fallback = {
...config.resolve.fallback,
net: false,
tls: false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Prevent potential crash when resolve.fallback is undefined

Guard against undefined and avoid spreading a non-object in older/edge configs.

   if (!isServer) {
     // Exclude Node.js-specific packages from client-side bundling
-    config.resolve.fallback = {
-      ...config.resolve.fallback,
+    config.resolve = config.resolve || {}
+    config.resolve.fallback = {
+      ...(config.resolve.fallback ?? {}),
       net: false,
       tls: false,
       fs: false,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!isServer) {
// Exclude Node.js-specific packages from client-side bundling
config.resolve.fallback = {
...config.resolve.fallback,
net: false,
tls: false,
if (!isServer) {
// Exclude Node.js-specific packages from client-side bundling
config.resolve = config.resolve || {}
config.resolve.fallback = {
...(config.resolve.fallback ?? {}),
net: false,
tls: false,
fs: false,
}
}
🤖 Prompt for AI Agents
In next.config.mjs around lines 14 to 19, the code spreads
config.resolve.fallback without guarding against resolve or fallback being
undefined; ensure you first create or normalize config.resolve and
config.resolve.fallback to objects (e.g., set config.resolve = config.resolve ||
{} and use fallback = { ...(config.resolve.fallback || {}), net: false, tls:
false }) so you never spread a non-object and avoid crashes in older/edge
configs.

Comment on lines 5 to 18
// Import service only on server side to avoid client-side bundling
let EmailTriggerService: typeof import('@/server/services/email-trigger.service').EmailTriggerService | undefined
if (typeof window === 'undefined') {
try {
// Server-side only
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const serviceModule = require('@/server/services/email-trigger.service')
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
EmailTriggerService = serviceModule.EmailTriggerService
} catch (error) {
// Service not available (e.g., in test environment)
console.warn('EmailTriggerService not available:', error)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix CI: remove unused server-only require (breaks on path alias).

The dynamic require is unused in this file and triggers the CI error “Cannot find module '@/server/services/email-trigger.service'.” Remove it to unblock the pipeline. If you truly need it later, prefer a lazy relative import inside an execute function and ensure path alias resolution in test/tsconfig.

-// Import service only on server side to avoid client-side bundling
-let EmailTriggerService: typeof import('@/server/services/email-trigger.service').EmailTriggerService | undefined
-if (typeof window === 'undefined') {
-  try {
-    // Server-side only
-    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
-    const serviceModule = require('@/server/services/email-trigger.service')
-    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
-    EmailTriggerService = serviceModule.EmailTriggerService
-  } catch (error) {
-    // Service not available (e.g., in test environment)
-    console.warn('EmailTriggerService not available:', error)
-  }
-}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Import service only on server side to avoid client-side bundling
let EmailTriggerService: typeof import('@/server/services/email-trigger.service').EmailTriggerService | undefined
if (typeof window === 'undefined') {
try {
// Server-side only
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const serviceModule = require('@/server/services/email-trigger.service')
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
EmailTriggerService = serviceModule.EmailTriggerService
} catch (error) {
// Service not available (e.g., in test environment)
console.warn('EmailTriggerService not available:', error)
}
}
🧰 Tools
🪛 GitHub Actions: CI

[error] 11-11: Cannot find module '@/server/services/email-trigger.service' required by EmailTriggerNode.schema.ts.

🤖 Prompt for AI Agents
In nodes/EmailTriggerNode/EmailTriggerNode.schema.ts around lines 5-18, remove
the unused server-only dynamic require block that attempts to
require('@/server/services/email-trigger.service') (the block initializing
EmailTriggerService and the try/catch) because it breaks CI due to path-alias
resolution; if server-side usage is later required, replace it with a lazy
relative import inside the node's execute runtime (e.g., import or require from
a relative path within the function) and/or ensure tests resolve the alias by
updating test/tsconfig paths so the module can be found.


// Auto-register nodes
import { EMAIL_NODE_DEFINITION } from './EmailNode'
import { EMAIL_TRIGGER_NODE_DEFINITION } from './EmailTriggerNode'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Import schema directly to avoid pulling feature barrel (and service) into client

Keep the registry import lean.

-import { EMAIL_TRIGGER_NODE_DEFINITION } from './EmailTriggerNode'
+import { EMAIL_TRIGGER_NODE_DEFINITION } from './EmailTriggerNode/EmailTriggerNode.schema'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { EMAIL_TRIGGER_NODE_DEFINITION } from './EmailTriggerNode'
import { EMAIL_TRIGGER_NODE_DEFINITION } from './EmailTriggerNode/EmailTriggerNode.schema'
🤖 Prompt for AI Agents
In nodes/index.ts at line 163, the import currently pulls from the
EmailTriggerNode feature barrel which drags services into the client; replace
that barrel import with a direct import of the node schema/definition module
(the specific file that only exports EMAIL_TRIGGER_NODE_DEFINITION) so the
registry only loads the schema. Update the import path to point to the
schema-only file and remove any side-effecting exports from the barrel usage.

Comment on lines +73 to +83
const searchCriteria: (string | string[])[] = ['UNSEEN']

// Parse custom email rules if provided
if (config.customEmailRules) {
try {
const customRules = JSON.parse(config.customEmailRules) as (string | string[])[]
searchCriteria.push(...customRules)
} catch (error) {
throw new Error('Invalid custom email rules JSON')
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Custom rules should replace, not intersect with default UNSEEN

Docs/UX imply custom IMAP criteria fully override defaults. Intersecting with UNSEEN yields unexpected results (e.g., ["ALL"] still filters to unseen).

-    const searchCriteria: (string | string[])[] = ['UNSEEN']
-
-    // Parse custom email rules if provided
-    if (config.customEmailRules) {
-      try {
-        const customRules = JSON.parse(config.customEmailRules) as (string | string[])[]
-        searchCriteria.push(...customRules)
-      } catch (error) {
-        throw new Error('Invalid custom email rules JSON')
-      }
-    }
+    const searchCriteria: (string | string[])[] = []
+    if (config.customEmailRules) {
+      try {
+        const customRules = JSON.parse(config.customEmailRules) as (string | string[])[]
+        searchCriteria.push(...customRules)
+      } catch {
+        throw new Error('Invalid custom email rules JSON')
+      }
+    }
+    if (searchCriteria.length === 0) {
+      searchCriteria.push('UNSEEN')
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const searchCriteria: (string | string[])[] = ['UNSEEN']
// Parse custom email rules if provided
if (config.customEmailRules) {
try {
const customRules = JSON.parse(config.customEmailRules) as (string | string[])[]
searchCriteria.push(...customRules)
} catch (error) {
throw new Error('Invalid custom email rules JSON')
}
}
const searchCriteria: (string | string[])[] = []
if (config.customEmailRules) {
try {
const customRules = JSON.parse(config.customEmailRules) as (string | string[])[]
searchCriteria.push(...customRules)
} catch {
throw new Error('Invalid custom email rules JSON')
}
}
if (searchCriteria.length === 0) {
searchCriteria.push('UNSEEN')
}
🤖 Prompt for AI Agents
In server/services/email-trigger.service.ts around lines 73 to 83, the code
currently initializes searchCriteria to ['UNSEEN'] and then pushes parsed
customEmailRules onto it, which intersects with the default; change the logic so
that if config.customEmailRules is provided and parses successfully, assign
searchCriteria to the parsed array (replacing the default) instead of pushing;
keep the try/catch for JSON.parse and throw the same or clearer error on parse
failure.

Comment on lines +86 to +88
if (config.trackLastMessageId !== false && lastMessageId) {
searchCriteria.push(['UID', `${lastMessageId}:*`])
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Off-by-one and truthy check on lastMessageId causes duplicates/skips

Use explicit number check and start from lastMessageId + 1.

-    if (config.trackLastMessageId !== false && lastMessageId) {
-      searchCriteria.push(['UID', `${lastMessageId}:*`])
-    }
+    if (config.trackLastMessageId !== false && typeof lastMessageId === 'number') {
+      const start = Math.max(1, lastMessageId + 1)
+      searchCriteria.push(['UID', `${start}:*`])
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (config.trackLastMessageId !== false && lastMessageId) {
searchCriteria.push(['UID', `${lastMessageId}:*`])
}
if (config.trackLastMessageId !== false && typeof lastMessageId === 'number') {
const start = Math.max(1, lastMessageId + 1)
searchCriteria.push(['UID', `${start}:*`])
}
🤖 Prompt for AI Agents
In server/services/email-trigger.service.ts around lines 86 to 88, the current
truthy check for lastMessageId can cause off-by-one duplicates or skipped
messages; change the check to an explicit numeric validation (e.g., typeof
lastMessageId === 'number' && Number.isFinite(lastMessageId)) and build the UID
searchCriteria to start from lastMessageId + 1 (not lastMessageId) so the query
uses the correct inclusive range and avoids reprocessing the last message;
ensure you also handle cases where lastMessageId is undefined/null by omitting
the UID range.

Comment on lines +90 to +97
const messages = await this.connection.search(searchCriteria, {
bodies: ['HEADER', 'TEXT'],
markSeen: config.postProcessAction === 'read',
})

const emails: EmailMessage[] = []

for (const message of messages) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix fetch: parser expects whole message ('') but search fetches only HEADER/TEXT

parseEmailMessage looks for part.which === '' and will throw “No message body found” with current fetch options.

-    const messages = await this.connection.search(searchCriteria, {
-      bodies: ['HEADER', 'TEXT'],
-      markSeen: config.postProcessAction === 'read',
-    })
+    const messages = await this.connection.search(searchCriteria, {
+      bodies: [''],
+      markSeen: config.postProcessAction === 'read',
+    })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const messages = await this.connection.search(searchCriteria, {
bodies: ['HEADER', 'TEXT'],
markSeen: config.postProcessAction === 'read',
})
const emails: EmailMessage[] = []
for (const message of messages) {
const messages = await this.connection.search(searchCriteria, {
bodies: [''],
markSeen: config.postProcessAction === 'read',
})
const emails: EmailMessage[] = []
for (const message of messages) {
// …rest of processing…
}
🤖 Prompt for AI Agents
In server/services/email-trigger.service.ts around lines 90 to 97, the code
fetches only HEADER/TEXT but parseEmailMessage expects the full message
(part.which === ''), causing a “No message body found” error; update the search
fetch options to request the entire raw message (e.g., use bodies: [''] or the
equivalent RFC822/full-body fetch) so parseEmailMessage receives the whole
message body, and keep markSeen logic intact.

Comment on lines +157 to +176
const parseAddresses = (addresses: ParsedMail['from'] | ParsedMail['to'] | ParsedMail['cc'] | ParsedMail['bcc']): EmailAddress[] => {
if (!addresses) return []

// Handle different address formats from mailparser
if (Array.isArray(addresses)) {
return addresses.map((addr): EmailAddress => ({
name: typeof addr === 'object' && addr && 'name' in addr ? String(addr.name || '') : undefined,
address: typeof addr === 'object' && addr && 'address' in addr ? String(addr.address || '') : '',
})).filter((addr) => addr.address)
}

if (typeof addresses === 'object' && addresses && 'address' in addresses) {
return [{
name: 'name' in addresses ? String(addresses.name || '') : undefined,
address: String(addresses.address || ''),
}]
}

return []
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Address parsing is incorrect for mailparser AddressObject

mailparser provides AddressObject with a .value array; current logic returns empty arrays.

-    const parseAddresses = (addresses: ParsedMail['from'] | ParsedMail['to'] | ParsedMail['cc'] | ParsedMail['bcc']): EmailAddress[] => {
-      if (!addresses) return []
-
-      // Handle different address formats from mailparser
-      if (Array.isArray(addresses)) {
-        return addresses.map((addr): EmailAddress => ({
-          name: typeof addr === 'object' && addr && 'name' in addr ? String(addr.name || '') : undefined,
-          address: typeof addr === 'object' && addr && 'address' in addr ? String(addr.address || '') : '',
-        })).filter((addr) => addr.address)
-      }
-
-      if (typeof addresses === 'object' && addresses && 'address' in addresses) {
-        return [{
-          name: 'name' in addresses ? String(addresses.name || '') : undefined,
-          address: String(addresses.address || ''),
-        }]
-      }
-
-      return []
-    }
+    const parseAddresses = (
+      addresses: ParsedMail['from'] | ParsedMail['to'] | ParsedMail['cc'] | ParsedMail['bcc']
+    ): EmailAddress[] => {
+      if (!addresses) return []
+      // mailparser: AddressObject => { value: Array<{ name?: string; address: string }> }
+      const list =
+        Array.isArray(addresses)
+          ? addresses
+          : (typeof addresses === 'object' && 'value' in (addresses as any) && Array.isArray((addresses as any).value))
+            ? (addresses as any).value
+            : (typeof addresses === 'object' && 'address' in (addresses as any))
+              ? [addresses as any]
+              : []
+      return list
+        .map((addr: any): EmailAddress => ({ name: addr.name || undefined, address: addr.address || '' }))
+        .filter(a => a.address)
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const parseAddresses = (addresses: ParsedMail['from'] | ParsedMail['to'] | ParsedMail['cc'] | ParsedMail['bcc']): EmailAddress[] => {
if (!addresses) return []
// Handle different address formats from mailparser
if (Array.isArray(addresses)) {
return addresses.map((addr): EmailAddress => ({
name: typeof addr === 'object' && addr && 'name' in addr ? String(addr.name || '') : undefined,
address: typeof addr === 'object' && addr && 'address' in addr ? String(addr.address || '') : '',
})).filter((addr) => addr.address)
}
if (typeof addresses === 'object' && addresses && 'address' in addresses) {
return [{
name: 'name' in addresses ? String(addresses.name || '') : undefined,
address: String(addresses.address || ''),
}]
}
return []
}
const parseAddresses = (
addresses: ParsedMail['from'] | ParsedMail['to'] | ParsedMail['cc'] | ParsedMail['bcc']
): EmailAddress[] => {
if (!addresses) return []
// mailparser: AddressObject => { value: Array<{ name?: string; address: string }> }
const list =
Array.isArray(addresses)
? addresses
: (typeof addresses === 'object' && 'value' in (addresses as any) && Array.isArray((addresses as any).value))
? (addresses as any).value
: (typeof addresses === 'object' && 'address' in (addresses as any))
? [addresses as any]
: []
return list
.map((addr: any): EmailAddress => ({ name: addr.name || undefined, address: addr.address || '' }))
.filter(a => a.address)
}
🤖 Prompt for AI Agents
In server/services/email-trigger.service.ts around lines 157 to 176, the
parseAddresses function fails to handle mailparser's AddressObject (which uses a
.value array) and therefore returns empty arrays; update the function to detect
when addresses is an object containing a .value array and map that array into
EmailAddress entries (extracting name and address with safe null/undefined
checks), retain the existing handling for Array and single address object cases,
and ensure you filter out entries with empty address strings and preserve the
name as undefined when empty.

- Convert @/ alias imports to relative paths for better module resolution
- Fix EmailNode test failures by implementing proper email provider mocking
- Resolve TypeScript/ESLint errors in email-providers.ts
- Install @types/nodemailer for proper type definitions
- All tests now passing (280/280)
- Restructure EmailNode.service.ts to use dynamic imports for email providers
- Add server-side only checks to prevent nodemailer loading in browser
- Maintain test functionality while ensuring build compatibility
- All tests passing (15/15) and build successful
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
nodes/EmailNode/email-providers.ts (1)

27-33: Attachments in config are ignored by the SMTP path.

EmailNode.types.ts exposes attachments: string[], but they aren’t sent. This is a functional gap for users expecting attachments to be delivered.

You can extend mailOptions like:

const mailOptions: MailOptions = {
  from: from || emailService.auth.user,
  to: to.join(', '),
  subject,
  text: body,
  attachments: config.attachments?.map(path => ({ path })) ?? []
}

If attachments are intentionally deferred, please document that in the node’s README and schema validation.

🧹 Nitpick comments (12)
nodes/EmailNode/email-providers.ts (4)

3-5: Type-safe nodemailer options; drop unused type and remove any/ESLint disables.

Import TransportOptions from nodemailer, remove the unused Transporter from local types, and avoid the any-cast on options.

Apply this diff:

-import { Transporter, MailOptions, SendMailResult } from './nodemailer-types'
-import type { Transporter as NodemailerTransporter } from 'nodemailer'
+import { MailOptions, SendMailResult } from './nodemailer-types'
+import type { Transporter as NodemailerTransporter, TransportOptions } from 'nodemailer'
@@
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    } as any)
+    })

Also applies to: 24-25


117-128: ESM-friendly dynamic import for nodemailer + clearer error; type the transporter factory.

Using require can break in ESM builds and obscures the failure mode when nodemailer isn’t installed. Prefer dynamic import with a helpful error and proper typing.

Apply this diff:

-// eslint-disable-next-line @typescript-eslint/no-require-imports
-let nodemailer: typeof import('nodemailer') | null = null
+let nodemailerMod: typeof import('nodemailer') | null = null
@@
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-async function getNodemailerTransporter(options: any): Promise<NodemailerTransporter> {
-  if (!nodemailer) {
-    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
-    nodemailer = require('nodemailer') as typeof import('nodemailer')
-  }
-  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
-  return nodemailer.createTransport(options)
+async function getNodemailerTransporter(options: TransportOptions): Promise<NodemailerTransporter> {
+  try {
+    if (!nodemailerMod) {
+      // Support both CJS and ESM builds of nodemailer
+      const mod = await import('nodemailer')
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+      nodemailerMod = (mod as any).default ?? (mod as any)
+    }
+    return nodemailerMod.createTransport(options)
+  } catch (e) {
+    throw new Error('Nodemailer is required to send emails. Install "nodemailer" (and types).', { cause: e as Error })
+  }
 }

36-37: Remove unnecessary ESLint disable; keep explicit cast.

The cast is already explicit; the disable can be dropped.

Apply this diff:

-    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
-    const info: SendMailResult = await transporter.sendMail(mailOptions) as SendMailResult
+    const info = (await transporter.sendMail(mailOptions)) as SendMailResult

13-25: Stale comment about “graceful fallback.”

The header says there’s a fallback when packages aren’t installed, but sendWithNodemailer now hard-requires nodemailer. Update the comment or wire simulateEmailSending as a fallback.

nodes/EmailNode/EmailNode.test.ts (1)

27-28: Remove duplicate comment.

This repeats the intent stated above.

Apply this diff:

-// Mock the email provider functions to avoid actual email sending in tests
-
nodes/EmailNode/nodemailer-types.ts (1)

48-52: Avoid self-recursive default typing; use a non-recursive interop shape.

default?: NodemailerModule makes the type recursively reference itself and can confuse tooling. Model ESM/CJS interop explicitly.

Apply:

 export interface NodemailerModule {
   createTransporter: (options: TransportOptions) => Transporter
   createTransport?: (options: TransportOptions) => Transporter
-  default?: NodemailerModule
+  default?: {
+    createTransport?: (options: TransportOptions) => Transporter
+    createTransporter?: (options: TransportOptions) => Transporter
+  }
 }
nodes/EmailTriggerNode/EmailTriggerNode.schema.ts (6)

1-4: Switch to relative type imports; avoid @ alias here.

Stays consistent with other PR changes and fixes test/SSR path issues.

-import { NodeType, TriggerType } from '@/types/workflow'
+import { NodeType, TriggerType } from '../../types/workflow'
-import type { NodeDefinition, ParameterDefinition } from '@/nodes'
+import type { NodeDefinition, ParameterDefinition } from '../index'

5-17: Remove unused EmailTriggerService type scaffold from schema file.

Types and constructors for the service don’t belong in the schema module and are unused. Keep schema/UI purely declarative.

-// Import service only on server side to avoid client-side bundling
-interface EmailTriggerServiceType {
-  new (): {
-    connect(config: EmailTriggerConfig): Promise<void>
-    disconnect(): Promise<void>
-    fetchEmails(config: EmailTriggerConfig, lastMessageId?: number): Promise<unknown[]>
-    markAsRead(messageId: number): Promise<void>
-    getLastMessageId(): Promise<number | undefined>
-    isConnectedToServer(): boolean
-    getConnection(): unknown
-  }
-}
-
-let EmailTriggerService: EmailTriggerServiceType | undefined

20-36: Drop dynamic require from schema; load the service where execution happens.

Even with guards and relative paths, pulling server code into a schema file couples client/UI bundles to server modules. Do the dynamic import inside the server executor or a server-only executeNode.

-// Try to load EmailTriggerService dynamically, but don't fail if it's not available
-try {
-  if (typeof window === 'undefined') {
-    // Server-side only - use relative path for better test compatibility
-    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
-    const serviceModule = require('../../server/services/email-trigger.service')
-    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
-    EmailTriggerService = serviceModule.EmailTriggerService
-  }
-} catch (error) {
-  // Service not available (e.g., in test environment or when dependencies are missing)
-  // This is expected and shouldn't cause test failures
-  if (process.env.NODE_ENV !== 'test') {
-    console.warn('EmailTriggerService not available:', error)
-  }
-  EmailTriggerService = undefined
-}
+// Note: service is intentionally not imported here; this module remains UI/schema-only.

71-78: Avoid constraining IMAP username to an email format.

Many IMAP servers use non-email usernames. Use text to prevent false negatives.

-  type: 'email',
+  type: 'text',

131-135: Confirm showIf semantics (OR vs AND).

Two entries for format === 'simple' and format === 'resolved' imply OR. If your UI uses AND, the control will never show.

If OR isn’t supported, consider a single negative condition or split into two controls behind separate conditions.


234-241: Validate customEmailRules shape, not just JSON parse.

Ensure it’s an array of strings/tuples accepted by imap-simple; reject objects/other types.

-      try {
-        JSON.parse(typed.customEmailRules)
-      } catch {
+      try {
+        const v = JSON.parse(typed.customEmailRules)
+        if (!Array.isArray(v)) {
+          errors.push('Custom email rules must be a JSON array')
+        }
+      } catch {
         errors.push('Custom email rules must be valid JSON')
-      }
+      }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1e99d2e and 146b9c7.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • lib/node-definitions.ts (1 hunks)
  • nodes/EmailNode/EmailNode.test.ts (1 hunks)
  • nodes/EmailNode/email-providers.ts (3 hunks)
  • nodes/EmailNode/nodemailer-types.ts (1 hunks)
  • nodes/EmailTriggerNode/EmailTriggerNode.schema.ts (1 hunks)
  • nodes/EmailTriggerNode/EmailTriggerNode.test.ts (1 hunks)
  • package.json (2 hunks)
  • server/services/workflow-executor.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • package.json
  • nodes/EmailTriggerNode/EmailTriggerNode.test.ts
🧰 Additional context used
🧬 Code graph analysis (3)
nodes/EmailNode/EmailNode.test.ts (1)
nodes/EmailNode/EmailNode.types.ts (1)
  • EmailNodeConfig (3-24)
nodes/EmailTriggerNode/EmailTriggerNode.schema.ts (3)
nodes/EmailTriggerNode/EmailTriggerNode.types.ts (2)
  • EmailTriggerConfig (6-31)
  • EMAIL_TRIGGER_NODE_IS_SERVER_ONLY (4-4)
server/services/email-trigger.service.ts (1)
  • EmailTriggerService (8-240)
nodes/index.ts (2)
  • NodeDefinition (50-75)
  • NodeType (188-188)
nodes/EmailNode/email-providers.ts (2)
nodes/EmailNode/EmailNode.types.ts (2)
  • EmailNodeConfig (3-24)
  • EmailExecutionResult (31-39)
nodes/EmailNode/nodemailer-types.ts (1)
  • SendMailResult (19-29)
🔇 Additional comments (4)
nodes/EmailNode/EmailNode.test.ts (1)

7-25: Ensure mocks are hoisted before module import to avoid real code paths.

Depending on Vitest config, vi.mock is hoisted, but being explicit avoids flakiness. Move the mock above imports that transitively load ./email-providers, or dynamically import the SUT after the mock.

Minimal pattern:

vi.mock('./email-providers', /* same factory */)
const { executeEmailNode } = await import('./EmailNode.service')
lib/node-definitions.ts (1)

7-10: Good move to relative imports; reduces alias brittleness in tests/SSR.

No behavioral change; this should improve reliability in environments without path aliases.

server/services/workflow-executor.ts (1)

14-19: Revert to path-alias imports
The project’s tsconfig.json defines the "@/*" alias and both runtime and tests rely on it, so mixing in relative imports will break module resolution. Revert these in server/services/workflow-executor.ts:

-import { executeEmailNode } from '../../nodes/EmailNode'
+import { executeEmailNode } from '@/nodes/EmailNode'
-import { executeHttpNode } from '../../nodes/HttpNode'
+import { executeHttpNode } from '@/nodes/HttpNode'
-import { executeManualNode } from '../../nodes/ManualNode'
+import { executeManualNode } from '@/nodes/ManualNode'
-import { executeIfNode } from '../../nodes/IfNode'
+import { executeIfNode } from '@/nodes/IfNode'
-import { executeFilterNode } from '../../nodes/FilterNode'
+import { executeFilterNode } from '@/nodes/FilterNode'
-import { executeDatabaseNode } from '../../nodes/DatabaseNode'
+import { executeDatabaseNode } from '@/nodes/DatabaseNode'
-import { executeTransformNode } from '../../nodes/TransformNode'
+import { executeTransformNode } from '@/nodes/TransformNode'
-import { executeDelayNode } from '../../nodes/DelayNode'
+import { executeDelayNode } from '@/nodes/DelayNode'

Likely an incorrect or invalid review comment.

nodes/EmailTriggerNode/EmailTriggerNode.schema.ts (1)

38-45: Server-only flag: LGTM.

serverSideOnly wired to the constant is correct and matches the node’s intent.

Comment on lines +80 to +85
name: 'password',
label: 'Password',
type: 'password',
required: true,
description: 'Password or app password for IMAP authentication'
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Do not collect raw secrets in node config; prefer credentials store.

Storing passwords in node config risks leakage in logs/exports. Use a credential field tied to a CredentialType (e.g., IMAP) and fetch secrets server-side.

If supported, change parameters (and import CredentialType) like:

+import type { CredentialType } from '../../types/credentials'
...
-{
-  name: 'user',
-  label: 'Username',
-  type: 'email',
-  required: true,
-  description: 'Email address for IMAP authentication',
-  placeholder: 'your-email@example.com'
-},
-{
-  name: 'password',
-  label: 'Password',
-  type: 'password',
-  required: true,
-  description: 'Password or app password for IMAP authentication'
-},
+{
+  name: 'credentials',
+  label: 'IMAP Credentials',
+  type: 'credential',
+  required: true,
+  credentialType: 'IMAP' as unknown as CredentialType, // adjust to actual enum/string
+  description: 'Stored credentials used server-side for IMAP authentication'
+},

Check available credential types:


🏁 Script executed:

#!/bin/bash
rg -nP -C2 'enum\s+CredentialType|export\s+type\s+CredentialType|CredentialType\.' types

Length of output: 322


🏁 Script executed:

#!/bin/bash
# Search for the definition of CREDENTIAL_TYPES
rg -nP -n "CREDENTIAL_TYPES" -n .

Length of output: 346


Replace raw secrets with credentials store
In nodes/EmailTriggerNode/EmailTriggerNode.schema.ts (around lines 80–85), remove the standalone password field and instead add a single credentials entry of type credential. Import type { CredentialType } from '../../types/credentials' and set credentialType: 'email' as CredentialType (or extend CREDENTIAL_TYPES in types/credentials.ts to include 'imap' if you need a distinct IMAP credential). This change ensures passwords are fetched server-side and not exposed in node configs.

🤖 Prompt for AI Agents
In nodes/EmailTriggerNode/EmailTriggerNode.schema.ts around lines 80 to 85,
replace the raw password field with a credentials entry: remove the standalone
password property and add a field with type 'credential' using credentialType:
'email' as CredentialType; also add the import line for type { CredentialType }
from '../../types/credentials'. If you require a distinct IMAP credential,
extend CREDENTIAL_TYPES in types/credentials.ts to include 'imap' and use
credentialType: 'imap' as CredentialType instead. Ensure the new credentials
field retains the original label/description context and that server-side code
retrieves the secret from the credentials store rather than from node config.

Comment on lines +27 to 29
import { WebhookNodeService } from '../../nodes/WebhookNode/WebhookNode.service'
import { NodeExecutionContext } from '../../nodes/types'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Wire up the new Email Trigger; current EMAIL case is a stub.

With the IMAP EmailTriggerNode added, the TriggerType.EMAIL case still returns a mock. Either delegate to the node’s executeNode (if provided) or to a server-side EmailTriggerService-backed executor.

Suggested direction (outside this hunk):

-      case TriggerType.EMAIL:
-        // In a real implementation, this would monitor an email inbox
-        return { triggered: true, from: 'test@example.com' }
+      case TriggerType.EMAIL: {
+        // Delegate to server-side service-backed executor
+        const { executeEmailTrigger } = await import('../../nodes/EmailTriggerNode') // e.g., service wrapper
+        const result = await executeEmailTrigger(context)
+        if (!result.success) {
+          throw new Error(result.error || 'Email trigger execution failed')
+        }
+        return result.output
+      }

Verification script to find/confirm an exported executor:


🏁 Script executed:

#!/bin/bash
rg -nP -C2 'executeEmailTrigger\s*\(' nodes/EmailTriggerNode

Length of output: 62


🏁 Script executed:

#!/bin/bash
# Search for EmailTriggerNodeService class
rg -nP -C2 'class\s+.*EmailTriggerNodeService' nodes
# Search for all references to EmailTrigger within nodes
rg -n 'EmailTrigger' -C2 nodes

Length of output: 8632


🏁 Script executed:

#!/bin/bash
# Show the EMAIL trigger case in the workflow executor
rg -nP -C5 'case TriggerType\\.EMAIL' server/services/workflow-executor.ts

Length of output: 76


🏁 Script executed:

#!/bin/bash
# Locate the workflow executor file
fd workflow-executor.ts .
# Fallback: locate any executor file under server/services
fd executor.ts server/services

Length of output: 136


🏁 Script executed:

#!/bin/bash
# Print the first 200 lines of the workflow executor to locate the EMAIL case
sed -n '1,200p' server/services/workflow-executor.ts

Length of output: 7170


🏁 Script executed:

#!/bin/bash
# Locate EMAIL trigger handling in WorkflowExecutor
rg -nP -C3 'case TriggerType\\.EMAIL' server/services/workflow-executor.ts

Length of output: 76


🏁 Script executed:

#!/bin/bash
# Print lines 200–350 to locate executeTriggerNode and EMAIL case
sed -n '200,350p' server/services/workflow-executor.ts

Length of output: 4852


Implement EMAIL trigger delegation
In WorkflowExecutor.executeTriggerNode (server/services/workflow-executor.ts), the case TriggerType.EMAIL still returns a hard-coded mock. Replace it with a real executor call—e.g. import your EmailTriggerNodeService (or directly use EmailTriggerService from server/services/email-trigger.service.ts) and invoke its execution method with context, throwing on failure.

🤖 Prompt for AI Agents
In server/services/workflow-executor.ts around lines 27 to 29, the
TriggerType.EMAIL branch currently returns a hard-coded mock; replace it by
importing the Email trigger service (either EmailTriggerNodeService from the
node implementation or EmailTriggerService from
server/services/email-trigger.service.ts) and call its execute (or equivalent)
method with the existing context; await the call, check its result and throw an
error if execution fails instead of returning the mock. Ensure you add the
import at the top, remove the mock return, await the real executor invocation
with context, and propagate errors by throwing when the executor indicates
failure.

coderabbitai[bot]
coderabbitai bot previously requested changes Sep 1, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (8)
nodes/EmailNode/email-providers.ts (6)

21-31: Tighten Nodemailer typing; drop any-casts

Use Nodemailer’s SMTPTransport.Options for the transporter config, remove the eslint disables and any-casts.

Apply:

@@
-import type { Transporter as NodemailerTransporter } from 'nodemailer'
+import type { Transporter as NodemailerTransporter } from 'nodemailer'
+import type SMTPTransport from 'nodemailer/lib/smtp-transport'
@@
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    const transporter: NodemailerTransporter = await getNodemailerTransporter({
+    const transporter: NodemailerTransporter = await getNodemailerTransporter({
       host: emailService.host,
       port: emailService.port || 587,
       secure: emailService.secure || false,
       auth: {
         user: emailService.auth.user,
         pass: emailService.auth.pass
       }
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    } as any)
+    })
@@
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-async function getNodemailerTransporter(options: any): Promise<NodemailerTransporter> {
+async function getNodemailerTransporter(options: SMTPTransport.Options): Promise<NodemailerTransporter> {
@@
-  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
   return nodemailer.createTransport(options)

Also applies to: 130-147, 4-4


33-38: Pass recipients as array instead of joined string

Nodemailer accepts arrays; avoids formatting surprises and lets Nodemailer handle address parsing.

-    const mailOptions: MailOptions = {
+    const mailOptions: MailOptions = {
       from: from || emailService.auth.user,
-      to: to.join(', '),
+      to,
       subject,
       text: body
     }

41-43: Align result typing with Nodemailer’s SentMessageInfo

Casting to a custom SendMailResult plus disabling linting hides type issues. Prefer Nodemailer’s own type.

-    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
-    const info: SendMailResult = await transporter.sendMail(mailOptions) as SendMailResult
+    const info = await transporter.sendMail(mailOptions)

If you need strong typing, import import type { SentMessageInfo } from 'nodemailer' and annotate const info: SentMessageInfo = ....


58-61: Thread AbortSignal and add a timeout to SendGrid fetch

Hook into upstream AbortSignal and fail fast. Improves cancellation and prevents hung requests.

-export async function sendWithSendGrid(config: EmailNodeConfig): Promise<EmailExecutionResult> {
+export async function sendWithSendGrid(config: EmailNodeConfig, signal?: AbortSignal): Promise<EmailExecutionResult> {
@@
-    const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
+    const controller = !signal ? new AbortController() : undefined
+    const timeout = setTimeout(() => controller?.abort(), 15000)
+    try {
+      const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
         method: 'POST',
         headers: {
           'Authorization': `Bearer ${emailService.apiKey}`,
           'Content-Type': 'application/json'
         },
+        signal: signal ?? controller?.signal,
         body: JSON.stringify({
           personalizations: [{
             to: to.map(email => ({ email }))
           }],
           from: { email: from || emailService.auth.user },
           subject,
           content: [{
             type: 'text/plain',
             value: body
           }]
         })
-    })
+      })
+      if (!response.ok) {
+        const errBody = await response.text().catch(() => '')
+        throw new Error(`SendGrid API error: ${response.status} ${response.statusText}${errBody ? ` - ${errBody}` : ''}`)
+      }
+    } finally {
+      clearTimeout(timeout)
+    }

Follow-up: pass signal from the service (see corresponding comment there).

Also applies to: 69-87, 4-4


106-125: Reduce noisy console logs in simulation

Gate logs behind a debug flag or use a logger with levels to avoid polluting output in production/test.


141-143: Preserve underlying load error message

A generic message hampers diagnostics. Include the original error message.

-    } catch (error) {
-      throw new Error('Failed to load nodemailer module')
+    } catch (error) {
+      throw new Error(`Failed to load nodemailer module: ${error instanceof Error ? error.message : 'unknown error'}`)
     }
nodes/EmailNode/EmailNode.service.ts (2)

35-45: Propagate AbortSignal to SendGrid path

Allows cancellation/timeouts to flow into the HTTP request.

-      const { sendWithNodemailer, sendWithSendGrid } = await import('./email-providers')
+      const { sendWithNodemailer, sendWithSendGrid } = await import('./email-providers')
@@
-      if (emailConfig.emailService.type === 'sendgrid') {
-        result = await sendWithSendGrid(emailConfig)
+      if (emailConfig.emailService.type === 'sendgrid') {
+        result = await sendWithSendGrid(emailConfig, signal)
       } else {
         // Default to nodemailer for SMTP/Gmail/Outlook
         result = await sendWithNodemailer(emailConfig, emailConfig.emailService.type)
       }

35-45: Attachments not wired through

Config exposes attachments, but neither SendGrid nor Nodemailer paths use them yet. If intentional, ignore; if not, we can add basic support (paths or base64).

Happy to draft attachment mapping for both providers.

Also applies to: 19-31

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 146b9c7 and 9e2f1f9.

📒 Files selected for processing (2)
  • nodes/EmailNode/EmailNode.service.ts (1 hunks)
  • nodes/EmailNode/email-providers.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
nodes/EmailNode/EmailNode.service.ts (3)
nodes/types.ts (1)
  • NodeExecutionContext (5-13)
nodes/EmailNode/EmailNode.types.ts (2)
  • EmailExecutionResult (31-39)
  • EmailNodeConfig (3-24)
nodes/EmailNode/email-providers.ts (2)
  • sendWithSendGrid (57-104)
  • sendWithNodemailer (10-55)
nodes/EmailNode/email-providers.ts (2)
nodes/EmailNode/EmailNode.types.ts (2)
  • EmailNodeConfig (3-24)
  • EmailExecutionResult (31-39)
nodes/EmailNode/nodemailer-types.ts (1)
  • SendMailResult (19-29)
🔇 Additional comments (3)
nodes/EmailNode/email-providers.ts (1)

11-14: Good server-only guard

Prevents client-side execution paths early. LGTM.

nodes/EmailNode/EmailNode.service.ts (2)

7-10: Abort-aware early exit looks good

Cleanly bails out when the signal is already aborted.


47-55: Confirm client-side “success” semantics

Returning sent: true on the client (simulation) can mislead downstream nodes/UX into assuming a real email was sent. Consider marking success: false with a clear simulation flag, or sent: false and provider: 'Simulated'.

Would you like a follow-up patch to flip this to an explicit dry-run result?

Comment on lines +12 to 18
// Validate config
if (!config) {
return { success: false, error: 'Configuration is required' }
}

private static async sendWithSMTP(config: EmailNodeConfig): Promise<EmailExecutionResult> {
return await sendWithNodemailer(config, 'SMTP')
}
}
const emailConfig = config as EmailNodeConfig

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Validate emailService and auth before execution

Missing checks can yield runtime errors (e.g., accessing properties of undefined) or opaque provider failures. Add minimal guards.

   const emailConfig = config as EmailNodeConfig

   // Validate required fields
   if (!emailConfig.to || emailConfig.to.length === 0) {
     return { success: false, error: 'At least one recipient is required' }
   }

   if (!emailConfig.subject || emailConfig.subject.trim() === '') {
     return { success: false, error: 'Subject is required' }
   }

   if (!emailConfig.body || emailConfig.body.trim() === '') {
     return { success: false, error: 'Email body is required' }
   }
+  if (!emailConfig.emailService || !emailConfig.emailService.type) {
+    return { success: false, error: 'Email service configuration is required' }
+  }
+  if (emailConfig.emailService.type === 'sendgrid') {
+    if (!emailConfig.emailService.apiKey) {
+      return { success: false, error: 'SendGrid API key is required' }
+    }
+  } else {
+    if (!emailConfig.emailService.auth?.user || !emailConfig.emailService.auth?.pass) {
+      return { success: false, error: 'SMTP credentials (user/pass) are required' }
+    }
+  }

Also applies to: 19-31

@Justin322322 Justin322322 merged commit fe20c00 into main Sep 1, 2025
4 checks passed
@Justin322322 Justin322322 deleted the feature/email-trigger-node branch September 1, 2025 15:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant