Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-argument": "off"
}
},
{
"files": ["*.mjs"],
"parser": "espree",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-argument": "off"
}
}
]
}
Expand Down
6 changes: 3 additions & 3 deletions lib/node-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import {
ActionType,
LogicType,
WorkflowNode,
} from '@/types/workflow'
import { getNodeDefinition, NodeDefinition as ImportedNodeDefinition } from '@/nodes'
import { CredentialType } from '@/types/credentials'
} from '../types/workflow'
import { getNodeDefinition, NodeDefinition as ImportedNodeDefinition } from '../nodes'
import { CredentialType } from '../types/credentials'

// Minimal, n8n-inspired parameter schema for nodes.
// This powers defaults and validation and can later drive dynamic UIs.
Expand Down
20 changes: 20 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,26 @@ const nextConfig = {
images: {
formats: ['image/webp', 'image/avif'],
},
webpack: (config, { isServer }) => {
if (!isServer) {
// Exclude Node.js-specific packages from client-side bundling
config.resolve.fallback = {
...config.resolve.fallback,
net: false,
tls: false,
Comment on lines +14 to +19
Copy link
Copy Markdown
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.

fs: false,
crypto: false,
stream: false,
buffer: false,
util: false,
url: false,
path: false,
os: false,
child_process: false,
}
}
return config
},
}

export default nextConfig
204 changes: 44 additions & 160 deletions nodes/EmailNode/EmailNode.service.ts
Original file line number Diff line number Diff line change
@@ -1,181 +1,65 @@
import { v4 as uuidv4 } from 'uuid'
import { EmailNodeConfig, EmailExecutionResult } from './EmailNode.types'
import { NodeExecutionContext, NodeExecutionResult } from '../types'
import { sendWithNodemailer, sendWithSendGrid } from './email-providers'
import { NodeExecutionContext } from '../types'

// Email service implementations
class EmailService {
static async sendEmail(config: EmailNodeConfig): Promise<EmailExecutionResult> {
const { emailService } = config

switch (emailService.type) {
case 'sendgrid':
return await sendWithSendGrid(config)
case 'gmail':
return await this.sendWithGmail(config)
case 'outlook':
return await this.sendWithOutlook(config)
case 'smtp':
return await this.sendWithSMTP(config)
default:
throw new Error(`Unsupported email service: ${emailService.type}`)
}
}
export async function executeEmailNode(context: NodeExecutionContext): Promise<{ success: boolean; output?: EmailExecutionResult; error?: string }> {
const { config, signal } = context

private static async sendWithGmail(config: EmailNodeConfig): Promise<EmailExecutionResult> {
// Gmail uses SMTP with specific settings
return await sendWithNodemailer({
...config,
emailService: {
...config.emailService,
host: 'smtp.gmail.com',
port: 587,
secure: false
}
}, 'Gmail')
// Check for abort signal
if (signal?.aborted) {
return { success: false, error: 'Execution was cancelled' }
}

private static async sendWithOutlook(config: EmailNodeConfig): Promise<EmailExecutionResult> {
// Outlook uses SMTP with specific settings
return await sendWithNodemailer({
...config,
emailService: {
...config.emailService,
host: 'smtp-mail.outlook.com',
port: 587,
secure: false
}
}, 'Outlook')
// 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

Comment on lines +12 to 18
Copy link
Copy Markdown
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

export async function executeEmailNode(context: NodeExecutionContext): Promise<NodeExecutionResult> {
try {
const config = context.config as unknown as EmailNodeConfig

// Validate basic configuration
if (!Array.isArray(config.to) || config.to.length === 0) {
return {
success: false,
error: 'At least one recipient is required'
}
}

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

if (!config.body || config.body.trim().length === 0) {
return {
success: false,
error: 'Email body is required'
}
}

// Enhanced email service validation with security checks
if (!config.emailService) {
return {
success: false,
error: 'Email service configuration is required'
}
}
// Validate required fields
if (!emailConfig.to || emailConfig.to.length === 0) {
return { success: false, error: 'At least one recipient is required' }
}

// Validate service type
if (!config.emailService.type || !['smtp', 'gmail', 'outlook', 'sendgrid'].includes(config.emailService.type)) {
return {
success: false,
error: 'Valid email service type is required (smtp, gmail, outlook, sendgrid)'
}
}
if (!emailConfig.subject || emailConfig.subject.trim() === '') {
return { success: false, error: 'Subject is required' }
}

// Validate email address format
if (!config.emailService.auth?.user) {
return {
success: false,
error: 'Email address is required'
}
}
if (!emailConfig.body || emailConfig.body.trim() === '') {
return { success: false, error: 'Email body is required' }
}

const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(config.emailService.auth.user)) {
return {
success: false,
error: 'Valid email address format is required'
}
}
try {
let result: EmailExecutionResult

// For SendGrid, validate API key format
if (config.emailService.type === 'sendgrid') {
if (!config.emailService.apiKey) {
return {
success: false,
error: 'SendGrid API key is required'
}
}
if (!config.emailService.apiKey.startsWith('SG.')) {
return {
success: false,
error: 'SendGrid API key should start with "SG."'
}
// Only load email providers on the server side
if (typeof window === 'undefined') {
// Dynamic import to avoid bundling on client side
const { sendWithNodemailer, sendWithSendGrid } = await import('./email-providers')

if (emailConfig.emailService.type === 'sendgrid') {
result = await sendWithSendGrid(emailConfig)
} else {
// Default to nodemailer for SMTP/Gmail/Outlook
result = await sendWithNodemailer(emailConfig, emailConfig.emailService.type)
}
} else {
// For other services, validate password
if (!config.emailService.auth?.pass) {
return {
success: false,
error: 'Password or app-specific password is required'
}
}
if (config.emailService.auth.pass.length < 6) {
return {
success: false,
error: 'Password should be at least 6 characters long'
}
}
}

// Validate SMTP-specific settings
if (config.emailService.type === 'smtp') {
if (!config.emailService.host || config.emailService.host.trim().length === 0) {
return {
success: false,
error: 'SMTP host is required for SMTP service'
}
}
if (config.emailService.port && (config.emailService.port < 1 || config.emailService.port > 65535)) {
return {
success: false,
error: 'SMTP port must be between 1 and 65535'
}
// Client-side fallback - return simulated result
result = {
sent: true,
to: emailConfig.to,
subject: emailConfig.subject,
messageId: `client-${Date.now()}`,
timestamp: new Date(),
provider: `${emailConfig.emailService.type} (Client-side)`
}
}

// Check for abort signal
if (context.signal?.aborted) {
return {
success: false,
error: 'Execution was cancelled'
}
}

// Send real email using the configured service
const result = await EmailService.sendEmail(config)

return {
success: true,
output: result
}
return { success: true, output: result }
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'
}
}
}
22 changes: 22 additions & 0 deletions nodes/EmailNode/EmailNode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@ import { NodeExecutionContext, createTestContext } from '../types'
import { EMAIL_NODE_DEFINITION } from './EmailNode.schema'
import { EmailNodeConfig, EmailExecutionResult } from './EmailNode.types'

// Mock the email provider functions
vi.mock('./email-providers', () => ({
sendWithNodemailer: vi.fn().mockImplementation((config: EmailNodeConfig) => Promise.resolve({
sent: true,
to: config.to,
subject: config.subject,
messageId: 'test-message-id',
timestamp: new Date(),
provider: 'Gmail'
})),
sendWithSendGrid: vi.fn().mockImplementation((config: EmailNodeConfig) => Promise.resolve({
sent: true,
to: config.to,
subject: config.subject,
messageId: 'test-message-id',
timestamp: new Date(),
provider: 'SendGrid'
}))
}))

// Mock the email provider functions to avoid actual email sending in tests

// Helper function to create test email config with required emailService
function createTestEmailConfig(overrides: Partial<EmailNodeConfig> = {}): EmailNodeConfig {
return {
Expand Down
Loading