-
Notifications
You must be signed in to change notification settings - Fork 1.6k
feat: preliminary imap_mail integration closes issue #3202 #3213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
first draft still needs some tweaks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks @waleedalzarooni , left some initial comments
camel/toolkits/imap_mail_toolkit.py
Outdated
# Validate required parameters | ||
if not self.imap_server: | ||
raise ValueError( | ||
"IMAP server must be provided or set via IMAP_SERVER" | ||
"environment variable" | ||
) | ||
if not self.smtp_server: | ||
raise ValueError( | ||
"SMTP server must be provided or set via SMTP_SERVER" | ||
"environment variable" | ||
) | ||
if not self.username: | ||
raise ValueError( | ||
"Username must be provided or set via EMAIL_USERNAME" | ||
"environment variable" | ||
) | ||
if not self.password: | ||
raise ValueError( | ||
"Password must be provided or set via EMAIL_PASSWORD" | ||
"environment variable" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can use the @api_keys_required
decorator make the code more tidy
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems not updated?
camel/toolkits/imap_mail_toolkit.py
Outdated
if not self.imap_server or not self.username or not self.password: | ||
raise ValueError( | ||
"IMAP server, username, and password must be provided" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this check seems redundant, we already checked in __init__
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
got it, removed
def _get_smtp_connection(self) -> smtplib.SMTP: | ||
r"""Establish SMTP connection. | ||
|
||
Returns: | ||
smtplib.SMTP: Connected SMTP client | ||
""" | ||
if not self.smtp_server or not self.username or not self.password: | ||
raise ValueError( | ||
"SMTP server, username, and password must be provided" | ||
) | ||
|
||
try: | ||
smtp = smtplib.SMTP(self.smtp_server, self.smtp_port) | ||
smtp.starttls() | ||
smtp.login(self.username, self.password) | ||
logger.info( | ||
"Successfully connected to SMTP server %s", self.smtp_server | ||
) | ||
return smtp | ||
except Exception as e: | ||
logger.error("Failed to connect to SMTP server: %s", e) | ||
raise |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would be better to do the connect in __init__
? otherwise we would need to connect eachtime the function is called
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I only make the connection in __init__
then there will be an issue when sequential function calls occur. After each function call the connection must be closed, unless the connection is opened at each function call this is not possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Wendong-Fan Would you prefer that I integrated the connections into the __init__
method then implemented a separate close_connections
for the agent to call to close all connections at the end of an interaction (more risky as it may be missed by the agent)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think we can implement automatic connection management with timeouts for this
|
||
def fetch_emails( | ||
self, | ||
folder: str = "INBOX", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for the folder, how many types do we support? would be better to use Literal
to list all supported type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My concern with using Literal here is that since we support many different email providers with slightly different naming conventions for folders, e.g. Gmail uses Sent and Outlook uses Sent Items.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Literal would be better compared with string, it would be more hard for llm to fill in free string with slight difference correctly
camel/toolkits/imap_mail_toolkit.py
Outdated
except imaplib.IMAP4.error: | ||
pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not good practice to handle errors silently
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
got it, issue is fixed
imap_server="imap.gmail.com", | ||
smtp_server="smtp.gmail.com", | ||
username="[email protected]", | ||
password="your_app_password", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great Job @waleedalzarooni. Sensitive email credentials are hardcoded in the example, which poses security risks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
got it, will be removed
- Moving emails to folders | ||
- Deleting emails | ||
|
||
Args: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we can rename this to Attributes:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'll just remove it, not super necessary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably better, bcz in the future sometimes its forgotten to update both of them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's keep the docstring here, otherwise it's not user-friendly for the developer to fill in the argument
try: | ||
imap = self._get_imap_connection() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of logging in inside each function, we could handle the login once in the constructor (init). This way, the connection is established when the class is initialized, and functions can directly use it. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mentioned to Wendong there is a slight concern with this approach, if I wanted to open connections in the innit method I would need to create a new close method to close all connections, I will discuss in the meeting today whether I should do this as it would add an extra method the agent would need to call, which it could forget
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now that is all from me @waleedalzarooni. The example is working all right. The changes I noticed are up for discussion.
Thanks !
Args: | ||
imap_server: IMAP server hostname | ||
imap_port: IMAP server port (default: 993) | ||
smtp_server: SMTP server hostname | ||
smtp_port: SMTP server port (default: 587) | ||
username: Email username | ||
password: Email password | ||
timeout: Timeout for operations | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think can also use the same as format as attributes
above just for consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i feel use Args would be better, args would be more useful information compared with attributes
logger.info( | ||
"Email sent successfully to %s", ", ".join(to_recipients) | ||
) | ||
return "Email sent successfully. Message ID: Unknown" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you update the return statement as the model is recieving:
... {'role': 'tool', 'content': 'Email sent successfully. Message ID: Unknown', 'tool_call_id': 'call_YwrH4jMN258NYWyPfXIMcX0i'}]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks @waleedalzarooni , left some comments below, also added one enhance PR to update part of the content discussed below here: #3236
- Moving emails to folders | ||
- Deleting emails | ||
|
||
Args: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's keep the docstring here, otherwise it's not user-friendly for the developer to fill in the argument
Args: | ||
imap_server: IMAP server hostname | ||
imap_port: IMAP server port (default: 993) | ||
smtp_server: SMTP server hostname | ||
smtp_port: SMTP server port (default: 587) | ||
username: Email username | ||
password: Email password | ||
timeout: Timeout for operations | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i feel use Args would be better, args would be more useful information compared with attributes
camel/toolkits/imap_mail_toolkit.py
Outdated
# Validate required parameters | ||
if not self.imap_server: | ||
raise ValueError( | ||
"IMAP server must be provided or set via IMAP_SERVER" | ||
"environment variable" | ||
) | ||
if not self.smtp_server: | ||
raise ValueError( | ||
"SMTP server must be provided or set via SMTP_SERVER" | ||
"environment variable" | ||
) | ||
if not self.username: | ||
raise ValueError( | ||
"Username must be provided or set via EMAIL_USERNAME" | ||
"environment variable" | ||
) | ||
if not self.password: | ||
raise ValueError( | ||
"Password must be provided or set via EMAIL_PASSWORD" | ||
"environment variable" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems not updated?
camel/toolkits/imap_mail_toolkit.py
Outdated
) | ||
continue | ||
|
||
email_dict = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems in the fetch_emails
method when email_body
is neither bytes nor str, the code executes continue. However, outside of this branch, the email_message
variable is used, could lead to NameError
?
camel/toolkits/imap_mail_toolkit.py
Outdated
content_type = part.get_content_type() | ||
content_disposition = str(part.get("Content-Disposition")) | ||
|
||
# Skip attachments |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
duplicated comment
def _get_smtp_connection(self) -> smtplib.SMTP: | ||
r"""Establish SMTP connection. | ||
|
||
Returns: | ||
smtplib.SMTP: Connected SMTP client | ||
""" | ||
if not self.smtp_server or not self.username or not self.password: | ||
raise ValueError( | ||
"SMTP server, username, and password must be provided" | ||
) | ||
|
||
try: | ||
smtp = smtplib.SMTP(self.smtp_server, self.smtp_port) | ||
smtp.starttls() | ||
smtp.login(self.username, self.password) | ||
logger.info( | ||
"Successfully connected to SMTP server %s", self.smtp_server | ||
) | ||
return smtp | ||
except Exception as e: | ||
logger.error("Failed to connect to SMTP server: %s", e) | ||
raise |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think we can implement automatic connection management with timeouts for this
|
||
def fetch_emails( | ||
self, | ||
folder: str = "INBOX", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Literal would be better compared with string, it would be more hard for llm to fill in free string with slight difference correctly
r"""Fetch emails from a folder with optional filtering. | ||
|
||
Args: | ||
folder (str): Email folder to search in (default: "INBOX") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for the default value, please check use format like (default: :obj:
True)
same for other parts
logger.info( | ||
"Email sent successfully to %s", ", ".join(to_recipients) | ||
) | ||
return "Email sent successfully. Message ID: Unknown" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why here use Message ID: Unknown
Description
IMAP integration into toolkit
Checklist
Go over all the following points, and put an
x
in all the boxes that apply.Fixes #issue-number
in the PR description (required)pyproject.toml
anduv lock
If you are unsure about any of these, don't hesitate to ask. We are here to help!