mirror of
https://github.com/Nikhil-Doye/workflow-builder.git
synced 2026-07-22 02:01:56 +02:00
Add Gmail integration support across components
- Introduced Gmail integration in ConfigurationWizard with a new wizard step for Gmail operations. - Updated NodeLibrary to include Gmail as a node type with associated color coding. - Enhanced BaseNode to represent Gmail with an appropriate icon. - Configured nodeTypeConfigs for Gmail with detailed descriptions and common use cases. - Added Gmail-specific fields in fieldConfigs for operation configuration. - Implemented GmailService and gmailProcessor for handling various Gmail operations, including sending, reading, and managing emails. - Created GmailNode component for visualizing Gmail operation details, including status and configuration. - Developed GmailWizard component for configuring Gmail operations with dynamic form fields based on selected operation type. - Updated ProcessorRegistry to recognize Gmail-related patterns for improved operation handling.
This commit is contained in:
@@ -50,6 +50,7 @@ import { EmbeddingWizard } from "./wizards/EmbeddingWizard";
|
||||
import { SimilaritySearchWizard } from "./wizards/SimilaritySearchWizard";
|
||||
import { StructuredOutputWizard } from "./wizards/StructuredOutputWizard";
|
||||
import { DiscordWizard } from "./wizards/DiscordWizard";
|
||||
import { GmailWizard } from "./wizards/GmailWizard";
|
||||
|
||||
const WIZARD_STEPS: Record<NodeType, WizardStep[]> = {
|
||||
dataInput: [
|
||||
@@ -158,6 +159,17 @@ const WIZARD_STEPS: Record<NodeType, WizardStep[]> = {
|
||||
helpText: "Set up your Discord operation parameters",
|
||||
},
|
||||
],
|
||||
// Gmail integration
|
||||
gmail: [
|
||||
{
|
||||
id: "gmail-config",
|
||||
title: "Gmail Integration",
|
||||
description: "Configure your Gmail integration",
|
||||
component: GmailWizard,
|
||||
validation: (data: any) => !!data.operation && !!data.accessToken,
|
||||
helpText: "Set up your Gmail operation parameters",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const ConfigurationWizard: React.FC<ConfigurationWizardProps> = ({
|
||||
@@ -221,6 +233,7 @@ export const ConfigurationWizard: React.FC<ConfigurationWizardProps> = ({
|
||||
database: "Database Operations",
|
||||
slack: "Slack Integration",
|
||||
discord: "Discord Integration",
|
||||
gmail: "Gmail Integration",
|
||||
};
|
||||
return labels[type] || "New Node";
|
||||
};
|
||||
|
||||
@@ -43,6 +43,7 @@ export const NodeLibrary: React.FC = () => {
|
||||
"database",
|
||||
"slack",
|
||||
"discord",
|
||||
"gmail",
|
||||
];
|
||||
|
||||
const nodeTypeColors: Record<NodeType, string> = {
|
||||
@@ -56,6 +57,7 @@ export const NodeLibrary: React.FC = () => {
|
||||
database: "cyan",
|
||||
slack: "purple",
|
||||
discord: "indigo",
|
||||
gmail: "red",
|
||||
};
|
||||
|
||||
const nodeTypesByCategory = getNodeTypesByCategory();
|
||||
|
||||
@@ -36,6 +36,8 @@ import {
|
||||
SlackNode,
|
||||
// Discord node
|
||||
DiscordNode,
|
||||
// Gmail node
|
||||
GmailNode,
|
||||
} from "./nodes";
|
||||
import { NodeData } from "../types";
|
||||
import { Grid, Trash2, Sparkles, X, Lightbulb, Play } from "lucide-react";
|
||||
@@ -54,6 +56,8 @@ const nodeTypes: NodeTypes = {
|
||||
slack: SlackNode,
|
||||
// Discord node
|
||||
discord: DiscordNode,
|
||||
// Gmail node
|
||||
gmail: GmailNode,
|
||||
};
|
||||
|
||||
interface WorkflowEditorProps {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Database,
|
||||
MessageSquare,
|
||||
Bot,
|
||||
Mail,
|
||||
} from "lucide-react";
|
||||
import { clsx } from "clsx";
|
||||
|
||||
@@ -29,6 +30,7 @@ const nodeIcons: Record<NodeType, React.ComponentType<any>> = {
|
||||
database: Database,
|
||||
slack: MessageSquare,
|
||||
discord: Bot,
|
||||
gmail: Mail,
|
||||
};
|
||||
|
||||
const nodeColors: Record<
|
||||
@@ -95,6 +97,12 @@ const nodeColors: Record<
|
||||
icon: "text-indigo-600",
|
||||
accent: "bg-indigo-500",
|
||||
},
|
||||
gmail: {
|
||||
bg: "bg-red-50",
|
||||
border: "border-red-200",
|
||||
icon: "text-red-600",
|
||||
accent: "bg-red-500",
|
||||
},
|
||||
};
|
||||
|
||||
const statusConfig = {
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import React from "react";
|
||||
import { NodeData } from "../../types";
|
||||
import { GmailOperation } from "../../types/gmail";
|
||||
import { NodeProps } from "reactflow";
|
||||
import {
|
||||
Mail,
|
||||
Send,
|
||||
Eye,
|
||||
Reply,
|
||||
Forward,
|
||||
FileText,
|
||||
Tag,
|
||||
Search,
|
||||
Paperclip,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Clock,
|
||||
} from "lucide-react";
|
||||
|
||||
interface GmailNodeProps extends NodeProps {
|
||||
data: NodeData;
|
||||
}
|
||||
|
||||
const operationIcons: Record<GmailOperation, React.ComponentType<any>> = {
|
||||
[GmailOperation.SEND]: Send,
|
||||
[GmailOperation.READ]: Eye,
|
||||
[GmailOperation.REPLY]: Reply,
|
||||
[GmailOperation.FORWARD]: Forward,
|
||||
[GmailOperation.DRAFT]: FileText,
|
||||
[GmailOperation.LABEL]: Tag,
|
||||
[GmailOperation.SEARCH]: Search,
|
||||
[GmailOperation.ATTACHMENT]: Paperclip,
|
||||
};
|
||||
|
||||
const operationLabels: Record<GmailOperation, string> = {
|
||||
[GmailOperation.SEND]: "Send Email",
|
||||
[GmailOperation.READ]: "Read Email",
|
||||
[GmailOperation.REPLY]: "Reply to Email",
|
||||
[GmailOperation.FORWARD]: "Forward Email",
|
||||
[GmailOperation.DRAFT]: "Manage Draft",
|
||||
[GmailOperation.LABEL]: "Manage Label",
|
||||
[GmailOperation.SEARCH]: "Search Emails",
|
||||
[GmailOperation.ATTACHMENT]: "Handle Attachment",
|
||||
};
|
||||
|
||||
export const GmailNode: React.FC<GmailNodeProps> = ({ data, selected }) => {
|
||||
const operation = data.config?.operation as GmailOperation;
|
||||
const IconComponent = operation ? operationIcons[operation] : Mail;
|
||||
const operationLabel = operation
|
||||
? operationLabels[operation]
|
||||
: "Gmail Operation";
|
||||
|
||||
const getStatusColor = () => {
|
||||
if (data.status === "success") return "text-green-600";
|
||||
if (data.status === "error") return "text-red-600";
|
||||
if (data.status === "running") return "text-blue-600";
|
||||
return "text-gray-600";
|
||||
};
|
||||
|
||||
const getPreviewData = () => {
|
||||
if (!data.config) return null;
|
||||
|
||||
switch (operation) {
|
||||
case GmailOperation.SEND:
|
||||
return {
|
||||
to: data.config.to || "No recipient",
|
||||
subject: data.config.subject || "No subject",
|
||||
};
|
||||
case GmailOperation.READ:
|
||||
return {
|
||||
messageId: data.config.messageId || "No message ID",
|
||||
query: data.config.query || "No query",
|
||||
};
|
||||
case GmailOperation.REPLY:
|
||||
return {
|
||||
replyTo: data.config.replyToMessageId || "No reply target",
|
||||
subject: data.config.subject || "No subject",
|
||||
};
|
||||
case GmailOperation.FORWARD:
|
||||
return {
|
||||
messageId: data.config.messageId || "No message ID",
|
||||
to: data.config.to || "No recipient",
|
||||
};
|
||||
case GmailOperation.DRAFT:
|
||||
return {
|
||||
draftId: data.config.draftId || "New draft",
|
||||
subject: data.config.subject || "No subject",
|
||||
};
|
||||
case GmailOperation.LABEL:
|
||||
return {
|
||||
labelName: data.config.labelName || "No label name",
|
||||
labelId: data.config.labelId || "No label ID",
|
||||
};
|
||||
case GmailOperation.SEARCH:
|
||||
return {
|
||||
query: data.config.searchQuery || "No search query",
|
||||
maxResults: data.config.maxResults || "Default",
|
||||
};
|
||||
case GmailOperation.ATTACHMENT:
|
||||
return {
|
||||
messageId: data.config.messageId || "No message ID",
|
||||
attachmentId: data.config.attachmentId || "No attachment ID",
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const preview = getPreviewData();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-white border-2 rounded-lg shadow-sm transition-all duration-200 ${
|
||||
selected
|
||||
? "border-red-500 shadow-lg"
|
||||
: "border-red-200 hover:border-red-300"
|
||||
}`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-100">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-red-100 flex items-center justify-center">
|
||||
<IconComponent className="w-4 h-4 text-red-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900">
|
||||
{data.label || "Gmail Integration"}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500">{operationLabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Indicator */}
|
||||
<div className="flex items-center space-x-2">
|
||||
{data.status === "running" && (
|
||||
<div className="w-6 h-6 rounded-full flex items-center justify-center bg-blue-100">
|
||||
<Clock className="w-3 h-3 text-blue-500 animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
{data.status === "success" && (
|
||||
<div className="w-6 h-6 rounded-full flex items-center justify-center bg-green-100">
|
||||
<CheckCircle className="w-3 h-3 text-green-500" />
|
||||
</div>
|
||||
)}
|
||||
{data.status === "error" && (
|
||||
<div className="w-6 h-6 rounded-full flex items-center justify-center bg-red-100">
|
||||
<XCircle className="w-3 h-3 text-red-500" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="px-4 pb-4">
|
||||
<div className="space-y-2">
|
||||
{/* Operation Details */}
|
||||
<div className="text-xs text-gray-600">
|
||||
{preview && (
|
||||
<div className="space-y-1">
|
||||
{Object.entries(preview).map(([key, value]) => (
|
||||
<div key={key} className="flex justify-between">
|
||||
<span className="text-gray-500 capitalize">{key}:</span>
|
||||
<span className="text-gray-700 truncate ml-2">
|
||||
{String(value)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status Display */}
|
||||
{data.status && (
|
||||
<div className={`text-xs font-medium ${getStatusColor()}`}>
|
||||
{data.status === "success" && "✓ Completed"}
|
||||
{data.status === "error" && "✗ Failed"}
|
||||
{data.status === "running" && "⏳ Running"}
|
||||
{data.status === "idle" && "⏸ Ready"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -12,3 +12,5 @@ export { UnifiedDatabaseNode } from "./UnifiedDatabaseNode";
|
||||
export { SlackNode } from "./SlackNode";
|
||||
// Discord node
|
||||
export { DiscordNode } from "./DiscordNode";
|
||||
// Gmail node
|
||||
export { GmailNode } from "./GmailNode";
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
import React, { useState } from "react";
|
||||
import { WizardStepProps } from "../ConfigurationWizard";
|
||||
import { Button } from "../ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
|
||||
import { Badge } from "../ui/badge";
|
||||
import {
|
||||
Mail,
|
||||
Send,
|
||||
Eye,
|
||||
Reply,
|
||||
Forward,
|
||||
FileText,
|
||||
Tag,
|
||||
Search,
|
||||
Paperclip,
|
||||
CheckCircle,
|
||||
Settings,
|
||||
} from "lucide-react";
|
||||
import { GmailOperation } from "../../types/gmail";
|
||||
|
||||
const GMAIL_OPERATIONS = [
|
||||
{
|
||||
id: GmailOperation.SEND,
|
||||
name: "Send Email",
|
||||
description: "Send emails with attachments and rich content",
|
||||
icon: Send,
|
||||
color: "bg-blue-100 text-blue-800 border-blue-200",
|
||||
features: ["Rich HTML", "Attachments", "CC/BCC", "Threading"],
|
||||
},
|
||||
{
|
||||
id: GmailOperation.READ,
|
||||
name: "Read Email",
|
||||
description: "Read emails and message threads",
|
||||
icon: Eye,
|
||||
color: "bg-green-100 text-green-800 border-green-200",
|
||||
features: ["Message content", "Headers", "Attachments", "Thread view"],
|
||||
},
|
||||
{
|
||||
id: GmailOperation.REPLY,
|
||||
name: "Reply to Email",
|
||||
description: "Reply to specific email messages",
|
||||
icon: Reply,
|
||||
color: "bg-purple-100 text-purple-800 border-purple-200",
|
||||
features: ["Thread replies", "Rich content", "Attachments", "Auto-reply"],
|
||||
},
|
||||
{
|
||||
id: GmailOperation.FORWARD,
|
||||
name: "Forward Email",
|
||||
description: "Forward emails to other recipients",
|
||||
icon: Forward,
|
||||
color: "bg-orange-100 text-orange-800 border-orange-200",
|
||||
features: [
|
||||
"Multiple recipients",
|
||||
"Add comments",
|
||||
"Preserve formatting",
|
||||
"Thread context",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: GmailOperation.DRAFT,
|
||||
name: "Manage Draft",
|
||||
description: "Create, edit, and manage email drafts",
|
||||
icon: FileText,
|
||||
color: "bg-yellow-100 text-yellow-800 border-yellow-200",
|
||||
features: ["Save drafts", "Edit drafts", "Send drafts", "Draft templates"],
|
||||
},
|
||||
{
|
||||
id: GmailOperation.LABEL,
|
||||
name: "Manage Label",
|
||||
description: "Create and manage Gmail labels",
|
||||
icon: Tag,
|
||||
color: "bg-pink-100 text-pink-800 border-pink-200",
|
||||
features: [
|
||||
"Create labels",
|
||||
"Color coding",
|
||||
"Nested labels",
|
||||
"Auto-labeling",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: GmailOperation.SEARCH,
|
||||
name: "Search Emails",
|
||||
description: "Search emails with advanced filters",
|
||||
icon: Search,
|
||||
color: "bg-indigo-100 text-indigo-800 border-indigo-200",
|
||||
features: ["Advanced search", "Filters", "Date ranges", "Custom queries"],
|
||||
},
|
||||
{
|
||||
id: GmailOperation.ATTACHMENT,
|
||||
name: "Handle Attachment",
|
||||
description: "Download and manage email attachments",
|
||||
icon: Paperclip,
|
||||
color: "bg-teal-100 text-teal-800 border-teal-200",
|
||||
features: [
|
||||
"Download files",
|
||||
"File info",
|
||||
"Multiple formats",
|
||||
"Batch processing",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const GmailWizard: React.FC<WizardStepProps> = ({
|
||||
data,
|
||||
onChange,
|
||||
onNext,
|
||||
onPrevious,
|
||||
}) => {
|
||||
const [selectedOperation, setSelectedOperation] = useState<GmailOperation>(
|
||||
data.operation || GmailOperation.SEND
|
||||
);
|
||||
|
||||
const handleOperationSelect = (operation: GmailOperation) => {
|
||||
setSelectedOperation(operation);
|
||||
onChange({
|
||||
...data,
|
||||
operation,
|
||||
});
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (selectedOperation) {
|
||||
onNext();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center w-16 h-16 mx-auto mb-4 bg-red-100 rounded-full">
|
||||
<Mail className="w-8 h-8 text-red-600" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">Gmail Integration</h2>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Choose what type of Gmail operation you want to perform
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{GMAIL_OPERATIONS.map((operation) => {
|
||||
const IconComponent = operation.icon;
|
||||
const isSelected = selectedOperation === operation.id;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={operation.id}
|
||||
className={`cursor-pointer transition-all duration-200 ${
|
||||
isSelected
|
||||
? "ring-2 ring-red-500 bg-red-50"
|
||||
: "hover:shadow-md hover:border-red-200"
|
||||
}`}
|
||||
onClick={() => handleOperationSelect(operation.id)}
|
||||
>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`p-2 rounded-lg ${operation.color}`}>
|
||||
<IconComponent className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-lg font-semibold">
|
||||
{operation.name}
|
||||
</CardTitle>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
{operation.description}
|
||||
</p>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<CheckCircle className="w-5 h-5 text-red-600" />
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<div className="space-y-2">
|
||||
{operation.features.map((feature, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="secondary"
|
||||
className="text-xs mr-1 mb-1"
|
||||
>
|
||||
{feature}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between pt-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onPrevious}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
<span>Back</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
disabled={!selectedOperation}
|
||||
className="flex items-center space-x-2 bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
<span>Next: Configure {selectedOperation}</span>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -388,6 +388,42 @@ export const fieldConfigs: Record<string, FieldConfig> = {
|
||||
maxLength: 50,
|
||||
},
|
||||
},
|
||||
|
||||
// Gmail-specific fields
|
||||
"gmail.accessToken": {
|
||||
technicalName: "accessToken",
|
||||
userFriendlyName: "Gmail Access Token",
|
||||
description: "Your Gmail API access token for authentication",
|
||||
helpText:
|
||||
"OAuth2 access token for Gmail API. This allows the workflow to access your Gmail account.",
|
||||
examples: [
|
||||
"ya29.a0AfH6SMC... (OAuth2 token)",
|
||||
"Obtain from Google Cloud Console",
|
||||
],
|
||||
validation: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
"gmail.operation": {
|
||||
technicalName: "operation",
|
||||
userFriendlyName: "Gmail operation",
|
||||
description: "Choose what type of Gmail operation to perform",
|
||||
helpText:
|
||||
"Select the type of Gmail operation you want to perform. Each operation has different configuration options.",
|
||||
examples: [
|
||||
"Send - Send emails with attachments",
|
||||
"Read - Read emails and threads",
|
||||
"Reply - Reply to specific messages",
|
||||
"Forward - Forward emails",
|
||||
"Draft - Create and manage drafts",
|
||||
"Label - Manage labels and folders",
|
||||
"Search - Search emails with filters",
|
||||
"Attachment - Handle file attachments",
|
||||
],
|
||||
validation: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const getFieldConfig = (
|
||||
@@ -426,6 +462,7 @@ export const getFieldsForNodeType = (nodeType: string): string[] => {
|
||||
database: ["connectionId", "operation", "label"],
|
||||
slack: ["botToken", "operation", "label"],
|
||||
discord: ["botToken", "operation", "label"],
|
||||
gmail: ["accessToken", "operation", "label"],
|
||||
};
|
||||
|
||||
return fieldMappings[nodeType] || [];
|
||||
|
||||
@@ -171,6 +171,28 @@ export const nodeTypeConfigs: Record<NodeType, NodeTypeConfig> = {
|
||||
"Welcome new members",
|
||||
],
|
||||
},
|
||||
gmail: {
|
||||
technicalName: "gmail",
|
||||
userFriendlyName: "Gmail Integration",
|
||||
description:
|
||||
"Send, read, manage emails, labels, drafts, and search Gmail messages",
|
||||
category: "Communication",
|
||||
icon: "📧",
|
||||
helpText:
|
||||
"Integrate with Gmail to send emails, read messages, manage labels, create drafts, and search your inbox. Perfect for email automation and communication workflows.",
|
||||
commonUseCases: [
|
||||
"Send automated emails",
|
||||
"Read and process incoming emails",
|
||||
"Reply to emails automatically",
|
||||
"Forward important messages",
|
||||
"Create and manage email drafts",
|
||||
"Organize emails with labels",
|
||||
"Search emails with filters",
|
||||
"Handle email attachments",
|
||||
"Email marketing campaigns",
|
||||
"Customer support automation",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const getNodeTypeConfig = (nodeType: NodeType): NodeTypeConfig => {
|
||||
|
||||
@@ -836,6 +836,8 @@ export class ExecutionEngine {
|
||||
return (await import("./processors/slackProcessor")).default;
|
||||
case "discord":
|
||||
return (await import("./processors/discordProcessor")).default;
|
||||
case "gmail":
|
||||
return (await import("./processors/gmailProcessor")).default;
|
||||
case "structuredOutput":
|
||||
return (await import("./processors/structuredOutputProcessor")).default;
|
||||
case "embeddingGenerator":
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
import { GmailOperation, GmailConfig } from "../../types/gmail";
|
||||
|
||||
export class GmailService {
|
||||
private baseUrl = "https://gmail.googleapis.com/gmail/v1";
|
||||
|
||||
async executeOperation(
|
||||
operation: GmailOperation,
|
||||
config: GmailConfig
|
||||
): Promise<any> {
|
||||
try {
|
||||
switch (operation) {
|
||||
case GmailOperation.SEND:
|
||||
return await this.sendEmail(config);
|
||||
case GmailOperation.READ:
|
||||
return await this.readEmail(config);
|
||||
case GmailOperation.REPLY:
|
||||
return await this.replyToEmail(config);
|
||||
case GmailOperation.FORWARD:
|
||||
return await this.forwardEmail(config);
|
||||
case GmailOperation.DRAFT:
|
||||
return await this.manageDraft(config);
|
||||
case GmailOperation.LABEL:
|
||||
return await this.manageLabel(config);
|
||||
case GmailOperation.SEARCH:
|
||||
return await this.searchEmails(config);
|
||||
case GmailOperation.ATTACHMENT:
|
||||
return await this.handleAttachment(config);
|
||||
default:
|
||||
throw new Error(`Unsupported Gmail operation: ${operation}`);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Gmail operation failed: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendEmail(config: GmailConfig): Promise<any> {
|
||||
const message = this.createMessage(config);
|
||||
return await this.makeRequest(
|
||||
"/users/me/messages/send",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({ raw: message }),
|
||||
},
|
||||
config.accessToken
|
||||
);
|
||||
}
|
||||
|
||||
private async readEmail(config: GmailConfig): Promise<any> {
|
||||
if (config.messageId) {
|
||||
return await this.makeRequest(
|
||||
`/users/me/messages/${config.messageId}`,
|
||||
{
|
||||
method: "GET",
|
||||
},
|
||||
config.accessToken
|
||||
);
|
||||
}
|
||||
|
||||
// List messages with query
|
||||
const params = new URLSearchParams();
|
||||
if (config.query) params.append("q", config.query);
|
||||
if (config.maxResults)
|
||||
params.append("maxResults", config.maxResults.toString());
|
||||
|
||||
return await this.makeRequest(
|
||||
`/users/me/messages?${params.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
},
|
||||
config.accessToken
|
||||
);
|
||||
}
|
||||
|
||||
private async replyToEmail(config: GmailConfig): Promise<any> {
|
||||
const message = this.createMessage(config);
|
||||
return await this.makeRequest(
|
||||
`/users/me/messages/${config.replyToMessageId}/reply`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({ raw: message }),
|
||||
},
|
||||
config.accessToken
|
||||
);
|
||||
}
|
||||
|
||||
private async forwardEmail(config: GmailConfig): Promise<any> {
|
||||
const message = this.createMessage(config);
|
||||
return await this.makeRequest(
|
||||
`/users/me/messages/${config.messageId}/forward`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({ raw: message }),
|
||||
},
|
||||
config.accessToken
|
||||
);
|
||||
}
|
||||
|
||||
private async manageDraft(config: GmailConfig): Promise<any> {
|
||||
if (config.draftId) {
|
||||
// Get existing draft
|
||||
return await this.makeRequest(
|
||||
`/users/me/drafts/${config.draftId}`,
|
||||
{
|
||||
method: "GET",
|
||||
},
|
||||
config.accessToken
|
||||
);
|
||||
}
|
||||
|
||||
// Create new draft
|
||||
const message = this.createMessage(config);
|
||||
return await this.makeRequest(
|
||||
"/users/me/drafts",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
message: { raw: message },
|
||||
}),
|
||||
},
|
||||
config.accessToken
|
||||
);
|
||||
}
|
||||
|
||||
private async manageLabel(config: GmailConfig): Promise<any> {
|
||||
if (config.labelId) {
|
||||
// Get existing label
|
||||
return await this.makeRequest(
|
||||
`/users/me/labels/${config.labelId}`,
|
||||
{
|
||||
method: "GET",
|
||||
},
|
||||
config.accessToken
|
||||
);
|
||||
}
|
||||
|
||||
// Create new label
|
||||
const labelData = {
|
||||
name: config.labelName,
|
||||
labelListVisibility: "labelShow",
|
||||
messageListVisibility: "show",
|
||||
};
|
||||
|
||||
if (config.labelColor) {
|
||||
labelData.color = { textColor: config.labelColor };
|
||||
}
|
||||
|
||||
return await this.makeRequest(
|
||||
"/users/me/labels",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(labelData),
|
||||
},
|
||||
config.accessToken
|
||||
);
|
||||
}
|
||||
|
||||
private async searchEmails(config: GmailConfig): Promise<any> {
|
||||
const params = new URLSearchParams();
|
||||
if (config.searchQuery) params.append("q", config.searchQuery);
|
||||
if (config.maxResults)
|
||||
params.append("maxResults", config.maxResults.toString());
|
||||
if (config.includeSpamTrash) params.append("includeSpamTrash", "true");
|
||||
|
||||
return await this.makeRequest(
|
||||
`/users/me/messages?${params.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
},
|
||||
config.accessToken
|
||||
);
|
||||
}
|
||||
|
||||
private async handleAttachment(config: GmailConfig): Promise<any> {
|
||||
return await this.makeRequest(
|
||||
`/users/me/messages/${config.messageId}/attachments/${config.attachmentId}`,
|
||||
{
|
||||
method: "GET",
|
||||
},
|
||||
config.accessToken
|
||||
);
|
||||
}
|
||||
|
||||
private createMessage(config: GmailConfig): string {
|
||||
const headers = [];
|
||||
|
||||
if (config.to) headers.push(`To: ${config.to}`);
|
||||
if (config.cc) headers.push(`Cc: ${config.cc}`);
|
||||
if (config.bcc) headers.push(`Bcc: ${config.bcc}`);
|
||||
if (config.subject) headers.push(`Subject: ${config.subject}`);
|
||||
|
||||
headers.push("Content-Type: text/html; charset=utf-8");
|
||||
headers.push(""); // Empty line before body
|
||||
|
||||
const body = config.htmlBody || config.body || "";
|
||||
|
||||
const fullMessage = [...headers, body].join("\n");
|
||||
return Buffer.from(fullMessage).toString("base64url");
|
||||
}
|
||||
|
||||
private async makeRequest(
|
||||
endpoint: string,
|
||||
options: RequestInit,
|
||||
accessToken: string
|
||||
): Promise<any> {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
`Gmail API error: ${response.status} ${response.statusText} - ${
|
||||
errorData.error?.message || "Unknown error"
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
validateConfig(config: GmailConfig): {
|
||||
isValid: boolean;
|
||||
errors: string[];
|
||||
} {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!config.accessToken) {
|
||||
errors.push("Access token is required");
|
||||
}
|
||||
|
||||
if (!config.operation) {
|
||||
errors.push("Operation is required");
|
||||
}
|
||||
|
||||
switch (config.operation) {
|
||||
case GmailOperation.SEND:
|
||||
if (!config.to)
|
||||
errors.push("Recipient email is required for send operation");
|
||||
if (!config.subject)
|
||||
errors.push("Subject is required for send operation");
|
||||
if (!config.body && !config.htmlBody)
|
||||
errors.push("Message body is required for send operation");
|
||||
break;
|
||||
case GmailOperation.READ:
|
||||
if (!config.messageId && !config.query) {
|
||||
errors.push(
|
||||
"Either message ID or search query is required for read operation"
|
||||
);
|
||||
}
|
||||
break;
|
||||
case GmailOperation.REPLY:
|
||||
if (!config.replyToMessageId)
|
||||
errors.push("Reply to message ID is required for reply operation");
|
||||
if (!config.body && !config.htmlBody)
|
||||
errors.push("Reply message body is required");
|
||||
break;
|
||||
case GmailOperation.FORWARD:
|
||||
if (!config.messageId)
|
||||
errors.push("Message ID is required for forward operation");
|
||||
if (!config.to)
|
||||
errors.push("Recipient email is required for forward operation");
|
||||
break;
|
||||
case GmailOperation.DRAFT:
|
||||
if (!config.subject)
|
||||
errors.push("Subject is required for draft operation");
|
||||
break;
|
||||
case GmailOperation.LABEL:
|
||||
if (!config.labelName && !config.labelId) {
|
||||
errors.push(
|
||||
"Either label name or label ID is required for label operation"
|
||||
);
|
||||
}
|
||||
break;
|
||||
case GmailOperation.SEARCH:
|
||||
if (!config.searchQuery)
|
||||
errors.push("Search query is required for search operation");
|
||||
break;
|
||||
case GmailOperation.ATTACHMENT:
|
||||
if (!config.messageId)
|
||||
errors.push("Message ID is required for attachment operation");
|
||||
if (!config.attachmentId)
|
||||
errors.push("Attachment ID is required for attachment operation");
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export class ProcessorRegistry {
|
||||
"database",
|
||||
"slack",
|
||||
"discord",
|
||||
"gmail",
|
||||
"structuredOutput",
|
||||
"embeddingGenerator",
|
||||
"similaritySearch",
|
||||
@@ -155,6 +156,10 @@ export class ProcessorRegistry {
|
||||
pattern: /discord|gaming|community|server/i,
|
||||
types: ["discord"],
|
||||
},
|
||||
{
|
||||
pattern: /gmail|email|mail/i,
|
||||
types: ["gmail"],
|
||||
},
|
||||
{
|
||||
pattern: /embed|vector/i,
|
||||
types: ["embeddingGenerator", "similaritySearch"],
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { NodeData } from "../../types";
|
||||
import { GmailService } from "../gmail/GmailService";
|
||||
import { GmailOperation } from "../../types/gmail";
|
||||
|
||||
async function gmailProcessor(nodeData: NodeData): Promise<{
|
||||
success: boolean;
|
||||
data?: any;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const { config } = nodeData;
|
||||
|
||||
if (!config) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Gmail configuration is missing",
|
||||
};
|
||||
}
|
||||
|
||||
const gmailService = new GmailService();
|
||||
|
||||
// Validate configuration
|
||||
const validation = gmailService.validateConfig(config as any);
|
||||
if (!validation.isValid) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Configuration validation failed: ${validation.errors.join(
|
||||
", "
|
||||
)}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Execute Gmail operation
|
||||
const result = await gmailService.executeOperation(
|
||||
config.operation as GmailOperation,
|
||||
config as any
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Unknown Gmail operation error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default gmailProcessor;
|
||||
@@ -0,0 +1,50 @@
|
||||
export enum GmailOperation {
|
||||
SEND = "send",
|
||||
READ = "read",
|
||||
REPLY = "reply",
|
||||
FORWARD = "forward",
|
||||
DRAFT = "draft",
|
||||
LABEL = "label",
|
||||
SEARCH = "search",
|
||||
ATTACHMENT = "attachment",
|
||||
}
|
||||
|
||||
export interface GmailConfig {
|
||||
accessToken: string;
|
||||
operation: GmailOperation;
|
||||
label: string;
|
||||
|
||||
// Send operations
|
||||
to?: string;
|
||||
cc?: string;
|
||||
bcc?: string;
|
||||
subject?: string;
|
||||
body?: string;
|
||||
htmlBody?: string;
|
||||
|
||||
// Read operations
|
||||
messageId?: string;
|
||||
query?: string;
|
||||
maxResults?: number;
|
||||
|
||||
// Reply/Forward operations
|
||||
threadId?: string;
|
||||
replyToMessageId?: string;
|
||||
|
||||
// Draft operations
|
||||
draftId?: string;
|
||||
|
||||
// Label operations
|
||||
labelName?: string;
|
||||
labelId?: string;
|
||||
labelColor?: string;
|
||||
|
||||
// Search operations
|
||||
searchQuery?: string;
|
||||
includeSpamTrash?: boolean;
|
||||
|
||||
// Attachment operations
|
||||
attachmentId?: string;
|
||||
filename?: string;
|
||||
mimeType?: string;
|
||||
}
|
||||
+3
-1
@@ -21,7 +21,9 @@ export type NodeType =
|
||||
| "database"
|
||||
// Slack integration
|
||||
| "slack"
|
||||
| "discord";
|
||||
| "discord"
|
||||
// Gmail integration
|
||||
| "gmail";
|
||||
|
||||
export type NodeStatus =
|
||||
| "idle"
|
||||
|
||||
@@ -140,6 +140,18 @@ const NODE_COMPATIBILITY: Record<
|
||||
description:
|
||||
"Discord nodes typically send messages and updates to community channels",
|
||||
},
|
||||
gmail: {
|
||||
canConnectTo: ["dataOutput"],
|
||||
shouldConnectTo: ["dataOutput"],
|
||||
commonPatterns: [
|
||||
"Process → Send email",
|
||||
"Generate report → Email results",
|
||||
"Data analysis → Email summary",
|
||||
"Notifications → Email alerts",
|
||||
],
|
||||
description:
|
||||
"Gmail nodes typically send emails and notifications to recipients",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user