Skip to content

Conversation

waleedalzarooni
Copy link
Collaborator

Description

IMAP integration into toolkit

Checklist

Go over all the following points, and put an x in all the boxes that apply.

  • [X ] I have read the CONTRIBUTION guide (required)
  • [X ] I have linked this PR to an issue using the Development section on the right sidebar or by adding Fixes #issue-number in the PR description (required)
  • [ X] I have checked if any dependencies need to be added or updated in pyproject.toml and uv lock
  • [ X] I have updated the tests accordingly (required for a bug fix or a new feature)
  • [ X] I have updated the documentation if needed:
  • [ X] I have added examples if this is a new feature

If you are unsure about any of these, don't hesitate to ask. We are here to help!

Copy link
Contributor

coderabbitai bot commented Sep 26, 2025

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch imap-mail-toolkit

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@waleedalzarooni waleedalzarooni linked an issue Sep 26, 2025 that may be closed by this pull request
2 tasks
@waleedalzarooni
Copy link
Collaborator Author

first draft still needs some tweaks!

Copy link
Member

@Wendong-Fan Wendong-Fan left a 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

Comment on lines 89 to 109
# 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"
)
Copy link
Member

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems not updated?

Comment on lines 117 to 120
if not self.imap_server or not self.username or not self.password:
raise ValueError(
"IMAP server, username, and password must be provided"
)
Copy link
Member

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__

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, removed

Comment on lines 133 to 154
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
Copy link
Member

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

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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)

Copy link
Member

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",
Copy link
Member

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?

Copy link
Collaborator Author

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.

Copy link
Member

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

Comment on lines 276 to 277
except imaplib.IMAP4.error:
pass
Copy link
Member

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

Copy link
Collaborator Author

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",
Copy link
Collaborator

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.

Copy link
Collaborator Author

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:
Copy link
Collaborator

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:

Copy link
Collaborator Author

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

Copy link
Collaborator

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.

Copy link
Member

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

Comment on lines +179 to +180
try:
imap = self._get_imap_connection()
Copy link
Collaborator

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?

Copy link
Collaborator Author

@waleedalzarooni waleedalzarooni Sep 29, 2025

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

Copy link
Collaborator

@a7m-1st a7m-1st left a 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 !

Comment on lines 70 to 78
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
"""
Copy link
Collaborator

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.

Copy link
Member

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

Comment on lines +403 to +407
logger.info(
"Email sent successfully to %s", ", ".join(to_recipients)
)
return "Email sent successfully. Message ID: Unknown"

Copy link
Collaborator

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'}]

Copy link
Member

@Wendong-Fan Wendong-Fan left a 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:
Copy link
Member

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

Comment on lines 70 to 78
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
"""
Copy link
Member

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

Comment on lines 89 to 109
# 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"
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems not updated?

)
continue

email_dict = {
Copy link
Member

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?

content_type = part.get_content_type()
content_disposition = str(part.get("Content-Disposition"))

# Skip attachments
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplicated comment

Comment on lines 133 to 154
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
Copy link
Member

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",
Copy link
Member

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")
Copy link
Member

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"
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Feature Request] Integrate Generic Email Support via IMAP
5 participants