diff --git a/agent/component/__init__.py b/agent/component/__init__.py index d8e31fb86ce..9fa77288bac 100644 --- a/agent/component/__init__.py +++ b/agent/component/__init__.py @@ -31,6 +31,8 @@ from .crawler import Crawler, CrawlerParam from .invoke import Invoke, InvokeParam from .template import Template, TemplateParam +from .email import Email, EmailParam + def component_class(class_name): diff --git a/agent/component/email.py b/agent/component/email.py new file mode 100644 index 00000000000..ec9a15a51e3 --- /dev/null +++ b/agent/component/email.py @@ -0,0 +1,138 @@ +# +# Copyright 2024 The InfiniFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from abc import ABC +import json +import smtplib +import logging +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from email.header import Header +from email.utils import formataddr +from agent.component.base import ComponentBase, ComponentParamBase + +class EmailParam(ComponentParamBase): + """ + Define the Email component parameters. + """ + def __init__(self): + super().__init__() + # Fixed configuration parameters + self.smtp_server = "" # SMTP server address + self.smtp_port = 465 # SMTP port + self.email = "" # Sender email + self.password = "" # Email authorization code + self.sender_name = "" # Sender name + + def check(self): + # Check required parameters + self.check_empty(self.smtp_server, "SMTP Server") + self.check_empty(self.email, "Email") + self.check_empty(self.password, "Password") + self.check_empty(self.sender_name, "Sender Name") + +class Email(ComponentBase, ABC): + component_name = "Email" + + def _run(self, history, **kwargs): + # Get upstream component output and parse JSON + ans = self.get_input() + content = "".join(ans["content"]) if "content" in ans else "" + if not content: + return Email.be_output("No content to send") + + success = False + try: + # Parse JSON string passed from upstream + email_data = json.loads(content) + + # Validate required fields + if "to_email" not in email_data: + return Email.be_output("Missing required field: to_email") + + # Create email object + msg = MIMEMultipart('alternative') + + # Properly handle sender name encoding + msg['From'] = formataddr((str(Header(self._param.sender_name,'utf-8')), self._param.email)) + msg['To'] = email_data["to_email"] + if "cc_email" in email_data and email_data["cc_email"]: + msg['Cc'] = email_data["cc_email"] + msg['Subject'] = Header(email_data.get("subject", "No Subject"), 'utf-8').encode() + + # Use content from email_data or default content + email_content = email_data.get("content", "No content provided") + # msg.attach(MIMEText(email_content, 'plain', 'utf-8')) + msg.attach(MIMEText(email_content, 'html', 'utf-8')) + + # Connect to SMTP server and send + logging.info(f"Connecting to SMTP server {self._param.smtp_server}:{self._param.smtp_port}") + + context = smtplib.ssl.create_default_context() + with smtplib.SMTP_SSL(self._param.smtp_server, self._param.smtp_port, context=context) as server: + # Login + logging.info(f"Attempting to login with email: {self._param.email}") + server.login(self._param.email, self._param.password) + + # Get all recipient list + recipients = [email_data["to_email"]] + if "cc_email" in email_data and email_data["cc_email"]: + recipients.extend(email_data["cc_email"].split(',')) + + # Send email + logging.info(f"Sending email to recipients: {recipients}") + try: + server.send_message(msg, self._param.email, recipients) + success = True + except Exception as e: + logging.error(f"Error during send_message: {str(e)}") + # Try alternative method + server.sendmail(self._param.email, recipients, msg.as_string()) + success = True + + try: + server.quit() + except Exception as e: + # Ignore errors when closing connection + logging.warning(f"Non-fatal error during connection close: {str(e)}") + + if success: + return Email.be_output("Email sent successfully") + + except json.JSONDecodeError: + error_msg = "Invalid JSON format in input" + logging.error(error_msg) + return Email.be_output(error_msg) + + except smtplib.SMTPAuthenticationError: + error_msg = "SMTP Authentication failed. Please check your email and authorization code." + logging.error(error_msg) + return Email.be_output(f"Failed to send email: {error_msg}") + + except smtplib.SMTPConnectError: + error_msg = f"Failed to connect to SMTP server {self._param.smtp_server}:{self._param.smtp_port}" + logging.error(error_msg) + return Email.be_output(f"Failed to send email: {error_msg}") + + except smtplib.SMTPException as e: + error_msg = f"SMTP error occurred: {str(e)}" + logging.error(error_msg) + return Email.be_output(f"Failed to send email: {error_msg}") + + except Exception as e: + error_msg = f"Unexpected error: {str(e)}" + logging.error(error_msg) + return Email.be_output(f"Failed to send email: {error_msg}") \ No newline at end of file diff --git a/web/src/assets/svg/email.svg b/web/src/assets/svg/email.svg new file mode 100644 index 00000000000..470608122a1 --- /dev/null +++ b/web/src/assets/svg/email.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 22c86ee9ce9..9d2f8e60664 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -1050,6 +1050,31 @@ When you want to search the given knowledge base at first place, set a higher pa template: 'Template', templateDescription: 'This component is used for typesetting the outputs of various components.', + emailComponent: 'Email', + emailDescription: 'Send email to specified address', + smtpServer: 'SMTP Server', + smtpPort: 'SMTP Port', + senderEmail: 'Sender Email', + authCode: 'Authorization Code', + senderName: 'Sender Name', + toEmail: 'Recipient Email', + ccEmail: 'CC Email', + emailSubject: 'Subject', + emailContent: 'Content', + smtpServerRequired: 'Please input SMTP server address', + senderEmailRequired: 'Please input sender email', + authCodeRequired: 'Please input authorization code', + toEmailRequired: 'Please input recipient email', + emailContentRequired: 'Please input email content', + emailSentSuccess: 'Email sent successfully', + emailSentFailed: 'Failed to send email', + dynamicParameters: 'Dynamic Parameters', + jsonFormatTip: + 'Upstream component should provide JSON string in following format:', + toEmailTip: 'to_email: Recipient email (Required)', + ccEmailTip: 'cc_email: CC email (Optional)', + subjectTip: 'subject: Email subject (Optional)', + contentTip: 'content: Email content (Optional)', }, footer: { profile: 'All rights reserved @ React', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 8cb523dd5da..29a03b3302f 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -1029,6 +1029,30 @@ export default { testRun: '试运行', template: '模板转换', templateDescription: '该组件用于排版各种组件的输出。', + emailComponent: '邮件', + emailDescription: '发送邮件到指定邮箱', + smtpServer: 'SMTP服务器', + smtpPort: 'SMTP端口', + senderEmail: '发件人邮箱', + authCode: '授权码', + senderName: '发件人名称', + toEmail: '收件人邮箱', + ccEmail: '抄送邮箱', + emailSubject: '邮件主题', + emailContent: '邮件内容', + smtpServerRequired: '请输入SMTP服务器地址', + senderEmailRequired: '请输入发件人邮箱', + authCodeRequired: '请输入授权码', + toEmailRequired: '请输入收件人邮箱', + emailContentRequired: '请输入邮件内容', + emailSentSuccess: '邮件发送成功', + emailSentFailed: '邮件发送失败', + dynamicParameters: '动态参数说明', + jsonFormatTip: '上游组件需要传入以下格式的JSON字符串:', + toEmailTip: 'to_email: 收件人邮箱(必填)', + ccEmailTip: 'cc_email: 抄送邮箱(可选)', + subjectTip: 'subject: 邮件主题(可选)', + contentTip: 'content: 邮件内容(可选)', }, footer: { profile: 'All rights reserved @ React', diff --git a/web/src/pages/flow/canvas/index.tsx b/web/src/pages/flow/canvas/index.tsx index afa4c1e0699..d372f119862 100644 --- a/web/src/pages/flow/canvas/index.tsx +++ b/web/src/pages/flow/canvas/index.tsx @@ -25,6 +25,7 @@ import styles from './index.less'; import { RagNode } from './node'; import { BeginNode } from './node/begin-node'; import { CategorizeNode } from './node/categorize-node'; +import { EmailNode } from './node/email-node'; import { GenerateNode } from './node/generate-node'; import { InvokeNode } from './node/invoke-node'; import { KeywordNode } from './node/keyword-node'; @@ -52,6 +53,7 @@ const nodeTypes = { keywordNode: KeywordNode, invokeNode: InvokeNode, templateNode: TemplateNode, + emailNode: EmailNode, }; const edgeTypes = { diff --git a/web/src/pages/flow/canvas/node/email-node.tsx b/web/src/pages/flow/canvas/node/email-node.tsx new file mode 100644 index 00000000000..121719ceaf9 --- /dev/null +++ b/web/src/pages/flow/canvas/node/email-node.tsx @@ -0,0 +1,78 @@ +import { Flex } from 'antd'; +import classNames from 'classnames'; +import { useState } from 'react'; +import { Handle, NodeProps, Position } from 'reactflow'; +import { NodeData } from '../../interface'; +import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; +import styles from './index.less'; +import NodeHeader from './node-header'; + +export function EmailNode({ + id, + data, + isConnectable = true, + selected, +}: NodeProps) { + const [showDetails, setShowDetails] = useState(false); + + return ( + + + + + + + setShowDetails(!showDetails)} + > + + SMTP: + {data.form?.smtp_server} + + + Port: + {data.form?.smtp_port} + + + From: + {data.form?.email} + + {showDetails ? '▼' : '▶'} + + + {showDetails && ( + + Expected Input JSON: + + {`{ + "to_email": "...", + "cc_email": "...", + "subject": "...", + "content": "..." +}`} + + + )} + + + ); +} diff --git a/web/src/pages/flow/canvas/node/index.less b/web/src/pages/flow/canvas/node/index.less index a9907df2dcd..3b703e594c9 100644 --- a/web/src/pages/flow/canvas/node/index.less +++ b/web/src/pages/flow/canvas/node/index.less @@ -193,3 +193,80 @@ .conditionLine; } } + +.emailNodeContainer { + padding: 8px; + font-size: 12px; + + .emailConfig { + background: rgba(0, 0, 0, 0.02); + border-radius: 4px; + padding: 8px; + position: relative; + cursor: pointer; + + &:hover { + background: rgba(0, 0, 0, 0.04); + } + + .configItem { + display: flex; + align-items: center; + margin-bottom: 4px; + + &:last-child { + margin-bottom: 0; + } + + .configLabel { + color: #666; + width: 45px; + flex-shrink: 0; + } + + .configValue { + color: #333; + word-break: break-all; + } + } + + .expandIcon { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + color: #666; + font-size: 12px; + } + } + + .jsonExample { + background: #f5f5f5; + border-radius: 4px; + padding: 8px; + margin-top: 4px; + animation: slideDown 0.2s ease-out; + + .jsonTitle { + color: #666; + margin-bottom: 4px; + } + + .jsonContent { + margin: 0; + color: #333; + font-family: monospace; + } + } +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/web/src/pages/flow/constant.tsx b/web/src/pages/flow/constant.tsx index 5571bfc705e..14dece783a5 100644 --- a/web/src/pages/flow/constant.tsx +++ b/web/src/pages/flow/constant.tsx @@ -8,6 +8,7 @@ import { ReactComponent as ConcentratorIcon } from '@/assets/svg/concentrator.sv import { ReactComponent as CrawlerIcon } from '@/assets/svg/crawler.svg'; import { ReactComponent as DeepLIcon } from '@/assets/svg/deepl.svg'; import { ReactComponent as DuckIcon } from '@/assets/svg/duck.svg'; +import { ReactComponent as EmailIcon } from '@/assets/svg/email.svg'; import { ReactComponent as ExeSqlIcon } from '@/assets/svg/exesql.svg'; import { ReactComponent as GithubIcon } from '@/assets/svg/github.svg'; import { ReactComponent as GoogleScholarIcon } from '@/assets/svg/google-scholar.svg'; @@ -25,6 +26,8 @@ import { ReactComponent as WenCaiIcon } from '@/assets/svg/wencai.svg'; import { ReactComponent as WikipediaIcon } from '@/assets/svg/wikipedia.svg'; import { ReactComponent as YahooFinanceIcon } from '@/assets/svg/yahoo-finance.svg'; +// 邮件功能 + import { variableEnabledFieldMap } from '@/constants/chat'; import i18n from '@/locales/config'; @@ -87,6 +90,7 @@ export enum Operator { Crawler = 'Crawler', Invoke = 'Invoke', Template = 'Template', + Email = 'Email', } export const CommonOperatorList = Object.values(Operator).filter( @@ -127,6 +131,7 @@ export const operatorIconMap = { [Operator.Crawler]: CrawlerIcon, [Operator.Invoke]: InvokeIcon, [Operator.Template]: TemplateIcon, + [Operator.Email]: EmailIcon, }; export const operatorMap: Record< @@ -259,6 +264,7 @@ export const operatorMap: Record< [Operator.Template]: { backgroundColor: '#dee0e2', }, + [Operator.Email]: { backgroundColor: '#e6f7ff' }, }; export const componentMenuList = [ @@ -358,6 +364,9 @@ export const componentMenuList = [ { name: Operator.Invoke, }, + { + name: Operator.Email, + }, ]; const initialQueryBaseValues = { @@ -580,6 +589,18 @@ export const initialTemplateValues = { parameters: [], }; +export const initialEmailValues = { + smtp_server: '', + smtp_port: 587, + email: '', + password: '', + sender_name: '', + to_email: '', + cc_email: '', + subject: '', + content: '', +}; + export const CategorizeAnchorPointPositions = [ { top: 1, right: 34 }, { top: 8, right: 18 }, @@ -660,6 +681,7 @@ export const RestrictedUpstreamMap = { [Operator.Note]: [], [Operator.Invoke]: [Operator.Begin], [Operator.Template]: [Operator.Begin, Operator.Relevant], + [Operator.Email]: [Operator.Begin], }; export const NodeMap = { @@ -696,6 +718,7 @@ export const NodeMap = { [Operator.Crawler]: 'ragNode', [Operator.Invoke]: 'invokeNode', [Operator.Template]: 'templateNode', + [Operator.Email]: 'emailNode', }; export const LanguageOptions = [ diff --git a/web/src/pages/flow/flow-drawer/index.tsx b/web/src/pages/flow/flow-drawer/index.tsx index 5a2c7e40e05..542f4dada0a 100644 --- a/web/src/pages/flow/flow-drawer/index.tsx +++ b/web/src/pages/flow/flow-drawer/index.tsx @@ -81,6 +81,7 @@ const FormMap = { [Operator.Concentrator]: () => <>>, [Operator.Note]: () => <>>, [Operator.Template]: TemplateForm, + [Operator.Email]: EmailForm, }; const EmptyContent = () => ; diff --git a/web/src/pages/flow/form/email-form/index.tsx b/web/src/pages/flow/form/email-form/index.tsx new file mode 100644 index 00000000000..e01f2f85a3d --- /dev/null +++ b/web/src/pages/flow/form/email-form/index.tsx @@ -0,0 +1,53 @@ +import { useTranslate } from '@/hooks/common-hooks'; +import { Form, Input } from 'antd'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const EmailForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + + return ( + + + + {/* SMTP服务器配置 */} + + + + + + + + + + + + + + + + + {/* 动态参数说明 */} + + {t('dynamicParameters')} + {t('jsonFormatTip')} + + {`{ + "to_email": "recipient@example.com", + "cc_email": "cc@example.com", + "subject": "Email Subject", + "content": "Email Content" +}`} + + + + ); +}; + +export default EmailForm; diff --git a/web/src/pages/flow/hooks.tsx b/web/src/pages/flow/hooks.tsx index e33bc3b7004..66e4efc948f 100644 --- a/web/src/pages/flow/hooks.tsx +++ b/web/src/pages/flow/hooks.tsx @@ -44,6 +44,7 @@ import { initialCrawlerValues, initialDeepLValues, initialDuckValues, + initialEmailValues, initialExeSqlValues, initialGenerateValues, initialGithubValues, @@ -141,6 +142,7 @@ export const useInitializeOperatorParams = () => { [Operator.Crawler]: initialCrawlerValues, [Operator.Invoke]: initialInvokeValues, [Operator.Template]: initialTemplateValues, + [Operator.Email]: initialEmailValues, }; }, [llmId]);
+ {`{ + "to_email": "...", + "cc_email": "...", + "subject": "...", + "content": "..." +}`} +
+ {`{ + "to_email": "recipient@example.com", + "cc_email": "cc@example.com", + "subject": "Email Subject", + "content": "Email Content" +}`} +