Email sending tool (#3837)

### What problem does this PR solve?

_Briefly describe what this PR aims to solve. Include background context
that will help reviewers understand the purpose of the PR._
Added the function of sending emails through SMTP
Instructions for use-
Corresponding parameters need to be configured
Need to output upstream in a fixed format

![image](https://github.com/user-attachments/assets/93bc1af7-6d4f-4406-bd1d-bc042535dd82)


### Type of change


- [√] New Feature (non-breaking change which adds functionality)

---------

Co-authored-by: Kevin Hu <kevinhu.sh@gmail.com>
This commit is contained in:
小黑马 2024-12-04 11:21:17 +08:00 committed by GitHub
parent 285bc58364
commit efae7afd62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 426 additions and 0 deletions

View File

@ -31,6 +31,8 @@ from .akshare import AkShare, AkShareParam
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):

138
agent/component/email.py Normal file
View File

@ -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}")

View File

@ -0,0 +1 @@
<svg t="1733148906323" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2383" width="200" height="200"><path d="M883.36 784H49.76a32 32 0 0 1-32-32V176a32 32 0 0 1 32-32h832a32 32 0 0 1 32 32l1.76 576a32 32 0 0 1-32.16 32z" fill="#FAEFDE" p-id="2384"></path><path d="M913.76 204.32l-448 320L17.76 208v-16c0-17.6 14.4-48 32-48h832c17.6 0 32 26.72 32 44.32z" fill="#FFF7F0" p-id="2385"></path><path d="M897.76 784h-864c-8.8 0-16-3.52-16-12.32V768a64 64 0 0 1 64-64h769.6a64 64 0 0 1 64 64v3.68c0 8.8-8.8 12.32-17.6 12.32z" fill="#EFD8BE" p-id="2386"></path><path d="M816.8 752m-192 0a192 192 0 1 0 384 0 192 192 0 1 0-384 0Z" fill="#72CAAF" p-id="2387"></path><path d="M48 140.32h833.76a32 32 0 0 1 32 32 35.36 35.36 0 0 1-32 35.68h-832A36.8 36.8 0 0 1 16 172.32a32 32 0 0 1 32-32z" fill="#FFFFFF" p-id="2388"></path><path d="M144.8 674.72a16 16 0 0 0-16 16v32a16 16 0 1 0 32 0v-32a16 16 0 0 0-16-16zM64.8 674.72a16 16 0 0 0-16 16v32a16 16 0 0 0 32 0v-32a16 16 0 0 0-16-16zM224.8 674.72a16 16 0 0 0-16 16v32a16 16 0 0 0 32 0v-32a16 16 0 0 0-16-16zM304.8 674.72a16 16 0 0 0-16 16v32a16 16 0 0 0 32 0v-32a16 16 0 0 0-16-16zM384.8 674.72a16 16 0 0 0-16 16v32a16 16 0 0 0 32 0v-32a16 16 0 0 0-16-16zM464.8 674.72a16 16 0 0 0-16 16v32a16 16 0 0 0 32 0v-32a16 16 0 0 0-16-16zM544.8 674.72a16 16 0 0 0-16 16v32a16 16 0 0 0 32 0v-32a16 16 0 0 0-16-16z" fill="#8D6C9F" p-id="2389"></path><path d="M928.8 576.96V178.72a48 48 0 0 0-48-48h-832a48 48 0 0 0-48 48v576a48 48 0 0 0 48 48h566.24a208 208 0 1 0 313.76-225.76z m-880-414.24h832a16 16 0 0 1 16 16v19.52l-413.12 299.2a32 32 0 0 1-37.6 0L32.8 198.24v-19.52a16 16 0 0 1 16-16z m0 608a16 16 0 0 1-16-16v-86.88L314.08 496a16 16 0 0 0-16-27.36L32.8 630.24V237.76l394.56 285.6a64 64 0 0 0 75.04 0l394.4-285.6V560a208 208 0 0 0-131.2-9.6l-133.28-81.28a16 16 0 0 0-21.92 5.28 16 16 0 0 0 5.28 21.6l112 67.84A208 208 0 0 0 608.8 752a183.68 183.68 0 0 0 0.96 18.72z m768 157.28a176 176 0 0 1-168.64-125.28 161.76 161.76 0 0 1-6.24-32 147.2 147.2 0 0 1-1.12-18.72 176 176 0 0 1 120.16-166.88 181.6 181.6 0 0 1 46.88-9.12h8.96a174.72 174.72 0 0 1 80 19.2 164.8 164.8 0 0 1 32 20.96 176 176 0 0 1-112 311.84z" fill="#8D6C9F" p-id="2390"></path><path d="M860.16 660.64a16 16 0 0 0-22.56 22.56l52.64 52.8h-57.44a112 112 0 0 0-112 112 16 16 0 0 0 32 0 80 80 0 0 1 80-80h57.44l-52.64 52.64a16 16 0 1 0 22.56 22.56l80-80a16 16 0 0 0 0-22.56z" fill="#F9EFDE" p-id="2391"></path></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -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',

View File

@ -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',

View File

@ -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 = {

View File

@ -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<NodeData>) {
const [showDetails, setShowDetails] = useState(false);
return (
<section
className={classNames(styles.ragNode, {
[styles.selectedNode]: selected,
})}
>
<Handle
id="c"
type="source"
position={Position.Left}
isConnectable={isConnectable}
className={styles.handle}
style={LeftHandleStyle}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
className={styles.handle}
style={RightHandleStyle}
id="b"
></Handle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
<Flex vertical gap={8} className={styles.emailNodeContainer}>
<div
className={styles.emailConfig}
onClick={() => setShowDetails(!showDetails)}
>
<div className={styles.configItem}>
<span className={styles.configLabel}>SMTP:</span>
<span className={styles.configValue}>{data.form?.smtp_server}</span>
</div>
<div className={styles.configItem}>
<span className={styles.configLabel}>Port:</span>
<span className={styles.configValue}>{data.form?.smtp_port}</span>
</div>
<div className={styles.configItem}>
<span className={styles.configLabel}>From:</span>
<span className={styles.configValue}>{data.form?.email}</span>
</div>
<div className={styles.expandIcon}>{showDetails ? '▼' : '▶'}</div>
</div>
{showDetails && (
<div className={styles.jsonExample}>
<div className={styles.jsonTitle}>Expected Input JSON:</div>
<pre className={styles.jsonContent}>
{`{
"to_email": "...",
"cc_email": "...",
"subject": "...",
"content": "..."
}`}
</pre>
</div>
)}
</Flex>
</section>
);
}

View File

@ -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);
}
}

View File

@ -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 = [

View File

@ -81,6 +81,7 @@ const FormMap = {
[Operator.Concentrator]: () => <></>,
[Operator.Note]: () => <></>,
[Operator.Template]: TemplateForm,
[Operator.Email]: EmailForm,
};
const EmptyContent = () => <div></div>;

View File

@ -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 (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
{/* SMTP服务器配置 */}
<Form.Item label={t('smtpServer')} name={'smtp_server'}>
<Input placeholder="smtp.example.com" />
</Form.Item>
<Form.Item label={t('smtpPort')} name={'smtp_port'}>
<Input type="number" placeholder="587" />
</Form.Item>
<Form.Item label={t('senderEmail')} name={'email'}>
<Input placeholder="sender@example.com" />
</Form.Item>
<Form.Item label={t('authCode')} name={'password'}>
<Input.Password placeholder="your_password" />
</Form.Item>
<Form.Item label={t('senderName')} name={'sender_name'}>
<Input placeholder="Sender Name" />
</Form.Item>
{/* 动态参数说明 */}
<div style={{ marginBottom: 24 }}>
<h4>{t('dynamicParameters')}</h4>
<div>{t('jsonFormatTip')}</div>
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4 }}>
{`{
"to_email": "recipient@example.com",
"cc_email": "cc@example.com",
"subject": "Email Subject",
"content": "Email Content"
}`}
</pre>
</div>
</Form>
);
};
export default EmailForm;

View File

@ -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]);