Skip to content

Conversation

@Saedbhati
Copy link
Collaborator

Description

Describe your changes in detail (optional if the linked issue already contains a detailed description of the changes).
fixes #3203

Checklist

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

  • I have read the CONTRIBUTION guide (required)
  • 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)
  • I have checked if any dependencies need to be added or updated in pyproject.toml and uv lock
  • I have updated the tests accordingly (required for a bug fix or a new feature)
  • I have updated the documentation if needed:
  • 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!

@coderabbitai
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 feat_zoho_mail

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.

scope=scopes,
)
print("\nToken response:")
print(json.dumps(token_resp, indent=2))

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (secret)
as clear text.
This expression logs
sensitive data (secret)
as clear text.

Copilot Autofix

AI about 1 month ago

To fix this issue, we must avoid logging cleartext sensitive data such as OAuth tokens, the client secret, and refresh tokens or secrets that could be included in either a successful response or within error information.

The correct way is to:

  • Never print the entire token_resp out to the logs.
  • Instead, explicitly print only non-sensitive subfields as needed (e.g., print e.g. "Access token received" or the keys but not values, or simply suppress printing altogether unless debugging).
  • If details from the response must be shown, redact any sensitive fields like access_token, refresh_token, and any client_secret or request_params/request_json objects.
  • Specifically for the snippet, remove or replace the line print(json.dumps(token_resp, indent=2)) with a safe message, or print only a fixed set of non-sensitive keys (e.g. the keys present in the response).

Only make these changes in examples/toolkits/zoho_toolkit.py, as shown.


Suggested changeset 1
examples/toolkits/zoho_toolkit.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/toolkits/zoho_toolkit.py b/examples/toolkits/zoho_toolkit.py
--- a/examples/toolkits/zoho_toolkit.py
+++ b/examples/toolkits/zoho_toolkit.py
@@ -118,7 +118,14 @@
         scope=scopes,
     )
     print("\nToken response:")
-    print(json.dumps(token_resp, indent=2))
+    # Do not print entire response as it may contain sensitive information.
+    # Only indicate success or reason for failure (if available).
+    if "access_token" in token_resp:
+        print("Access token received.")
+    elif "error" in token_resp:
+        print(f"Token request failed: {token_resp.get('error')}")
+    else:
+        print("Token response received, but access_token not found.")
     access_token = token_resp.get("access_token") or ""
     if not access_token:
         print("Failed to obtain access token.")
EOF
@@ -118,7 +118,14 @@
scope=scopes,
)
print("\nToken response:")
print(json.dumps(token_resp, indent=2))
# Do not print entire response as it may contain sensitive information.
# Only indicate success or reason for failure (if available).
if "access_token" in token_resp:
print("Access token received.")
elif "error" in token_resp:
print(f"Token request failed: {token_resp.get('error')}")
else:
print("Token response received, but access_token not found.")
access_token = token_resp.get("access_token") or ""
if not access_token:
print("Failed to obtain access token.")
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines 200 to 205
on_subtask_completed (Optional[Callable[[Task], None]], optional):
Callback function to be called when a subtask is completed.
(default: :obj:`None`)
on_subtask_failed (Optional[Callable[[Task], None]], optional):
Callback function to be called when a subtask fails.
(default: :obj:`None`)
Copy link
Member

Choose a reason for hiding this comment

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

why we need to add these 2 argument in this PR, could you describe more for this change?

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 was working on this issue
, but when I saw that coolbeevip was already working on it, I decided to drop it. I’m not sure how I ended up committing, but I will remove it.

@a7m-1st
Copy link
Collaborator

a7m-1st commented Sep 29, 2025

@Saedbhati while testing, I provided:

$env:ZOHO_CLIENT_ID
$env:ZOHO_CLIENT_SECRET
$env:ZOHO_REDIRECT_URI
$env:ZOHO_TO_ADDRESS

Which I got from the api console (Workspace account):
image

But I am facing this issue, the outh generation seems fine though. Is this the correct way to get started with the api?

image

@a7m-1st
Copy link
Collaborator

a7m-1st commented Sep 29, 2025

Program flow for me:
image

@Saedbhati
Copy link
Collaborator Author

@a7m-1st
image
you have to set redirect url to http://localhost:8000/callback

@a7m-1st
Copy link
Collaborator

a7m-1st commented Sep 29, 2025

Oh all right, didn't realize that. I will try again later. Thanks @Saedbhati

Copy link
Collaborator

@waleedalzarooni waleedalzarooni left a comment

Choose a reason for hiding this comment

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

Hey Saed good work!

I left some comments, most are quick fixes, let me know if you need any clarification!

"Content-Type": "application/json",
}
try:
response = requests.request(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Other toolkits include a timeout parameter here just in case, may be useful if you have time

url = f"{self.base_url}/api/accounts/{self.account_id}/messages/search"
query = f"label:{tag_id}"
params: Dict[str, Any] = {
"query": query,
Copy link
Collaborator

Choose a reason for hiding this comment

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

In line 758 you use searchKey, just wan't to double check this difference is intentional

@staticmethod
def build_authorize_url(
*,
client_type: str,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Consider adding some input validation for client_type client_id and scope

}
if scope:
params["scope"] = scope
return self._request_json("POST", url, params=params)
Copy link
Collaborator

Choose a reason for hiding this comment

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

OAUTH 2.0 specification (RFC 6749) violation here may cause exposed client secrets susceptible to attack.
params is sent in URL query string as opposed to request body

}
if scope:
params["scope"] = scope
return self._request_json("POST", url, params=params)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I believe this is the same violation as above

description: Optional[str] = None,
due_in_epoch_ms: Optional[int] = None,
) -> Dict[str, Any]:
r"""Action: Create Task (OAuth Scope: ZohoMail.tasks )
Copy link
Collaborator

Choose a reason for hiding this comment

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

docstring

description: Optional[str] = None,
due_in_epoch_ms: Optional[int] = None,
) -> Dict[str, Any]:
r"""Action: Create Task (OAuth Scope: ZohoMail.tasks )
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
r"""Action: Create Task (OAuth Scope: ZohoMail.tasks )
r"""Action: Create Task (OAuth Scope: ZohoMail.tasks)

folder_id (str): Folder ID.
since_epoch_seconds (Optional[int]): Since epoch seconds.
limit (int): Limit.
sort_order (str): Sort order.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Possibly a more detailed description to help the agent understand what the Args mean, also missing default tags, see CONTRIBUTING.md

def trigger_new_email_matching_search_poll(
self,
query: str,
folder_id: Optional[str] = None,
Copy link
Collaborator

Choose a reason for hiding this comment

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

never used

params["includeto"] = True
if received_time_ms is not None:
params["receivedTime"] = int(received_time_ms)
# Note: folderId is not supported in search API; omit to avoid
Copy link
Collaborator

Choose a reason for hiding this comment

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

the next method uses folderId in the search API request so not sure whether this note is correct or not

@Saedbhati Saedbhati self-assigned this Oct 9, 2025
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.

Thanks for the PR @Saedbhati , all in all my comments is that it requires an existing server side zoho api which is configured correctly. Also requires a local oauth2 server for redirection for some reason and have to manage the auth & api requests manually i.e. no sdk's or anything. Have to use Zoho's mail and set the region server manually too.

Too much hussle for a niche use case, but as a reviewer, it works for me (perhaps just the fixes in mentioned)

Comment on lines +498 to +499
subject: str,
content: str,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps can rename this to body for consistency with imap_toolkit

"encoding": encoding,
}

self._add_recipients(payload, to=to, cc=cc, bcc=bcc)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Cool, I like this idea of modular add recipients.

Comment on lines +776 to +780
) -> Dict[str, Any]:
r"""Trigger: New Tagged Email (Polling)
Retrieves emails that have the specified tag/label.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since this we can retrieve emails based on tags, is there a way to send emails with a tag as in this example from Resend toolkit:

def send_email(
      self,
      to: List[str],
      subject: str,
      from_email: str,
      html: Optional[str] = None,
      text: Optional[str] = None,
      cc: Optional[List[str]] = None,
      bcc: Optional[List[str]] = None,
      reply_to: Optional[str] = None,
      tags: Optional[List[Dict[str, str]]] = None,
      headers: Optional[Dict[str, str]] = None,
  ) -> str:

Comment on lines +500 to +504
from_email: Optional[str] = None,
cc: Optional[Union[List[str], List[Dict[str, Any]]]] = None,
bcc: Optional[Union[List[str], List[Dict[str, Any]]]] = None,
is_html: bool = True,
encoding: str = "UTF-8",
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 would be nice to have reply_to param with the documentation:
https://www.zoho.com/mail/help/api/post-reply-to-an-email.html

I guess in your case it will be a boolean value & just add those to your api request

"askReceipt" : "yes",
"action":"reply"

Comment on lines +111 to +113

temp = ZohoMailToolkit(access_token="", account_id="", datacenter="in")
token_resp = temp.exchange_code_for_token(
Copy link
Collaborator

Choose a reason for hiding this comment

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

datacenter is hardcoded here ++

Comment on lines +127 to +134
identifier = os.getenv("ZOHO_ACCOUNT_ID", "").strip()
if not identifier:
identifier = input(
"Enter ZOHO_ACCOUNT_ID (numeric or email, leave blank to auto): "
).strip()
if identifier.isdigit():
account_id, default_from_email = identifier, ""
else:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Currently there is no way to authenticate if account id is real, so it still proceeds to send emails.

Image

Comment on lines +194 to +198
# Action: Create Task
task_resp = toolkit.create_task(
title="Follow up", description="Call customer", due_in_epoch_ms=None
)
print("\nCreate Task response:")
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 need to update the scope , as api returns 404: URL_RULE_NOT_CONFIGURED.
Need to add ZohoMail.tasks to the scope above

Comment on lines +674 to +677
Creates a task in Zoho Mail Tasks.
"""
url = f"{self.base_url}/api/accounts/{self.account_id}/tasks"
payload: Dict[str, Any] = {"title": title}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This endpoint doesn't seem to match the doc, unless you are using a different one such as Zoho Mail360?
https://www.zoho.com/mail/help/api/task-api.html

Comment on lines +135 to +145
continue
fid = f.get("folderId") or f.get("id") or f.get("folder_id")
names = []
for key in (
"folderName",
"displayName",
"name",
"systemFolder",
"folderType",
"type",
):
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think we need to check for id, type, or display name. Here is what each item in the loop looks like:

{'path': '/Outbox', 
'VW': True, 
'previousFolderId': '1026610003021', 
'HIDE': False, 
'isArchived': 0, 
'folderName': 'Outbox', 
'imapAccess': True, 
'folderType': 'Outbox', 
'URI': 'https:mail.zoho.in/api/accounts/1026610002002/folders/1026610002028', 
'folderId': '102660002028'}

Comment on lines +209 to +213
print("\nTrigger: New Email Matching Search (Polling)")
search_resp = toolkit.trigger_new_email_matching_search_poll(
query=f'from:"{to_email}"', folder_id="inbox", limit=10
)
print(json.dumps(search_resp, indent=2))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can I ask what the polling is supposed to do? Just fetch latest results?

@Wendong-Fan Wendong-Fan added the Waiting for Update PR has been reviewed, need to be updated based on review comment label Oct 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Waiting for Update PR has been reviewed, need to be updated based on review comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Integrate Zoho Mail into CAMEL with Actions and Triggers

5 participants