Skip to content

Commit

Permalink
Email sending tool (#3837)
Browse files Browse the repository at this point in the history
### 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>
  • Loading branch information
kunkeji and KevinHuSh authored Dec 4, 2024
1 parent 285bc58 commit efae7af
Show file tree
Hide file tree
Showing 12 changed files with 426 additions and 0 deletions.
2 changes: 2 additions & 0 deletions agent/component/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
138 changes: 138 additions & 0 deletions agent/component/email.py
Original file line number Diff line number Diff line change
@@ -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}")
1 change: 1 addition & 0 deletions web/src/assets/svg/email.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions web/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
24 changes: 24 additions & 0 deletions web/src/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions web/src/pages/flow/canvas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -52,6 +53,7 @@ const nodeTypes = {
keywordNode: KeywordNode,
invokeNode: InvokeNode,
templateNode: TemplateNode,
emailNode: EmailNode,
};

const edgeTypes = {
Expand Down
78 changes: 78 additions & 0 deletions web/src/pages/flow/canvas/node/email-node.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
77 changes: 77 additions & 0 deletions web/src/pages/flow/canvas/node/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading

0 comments on commit efae7af

Please sign in to comment.