From 82c853bbb5e2b478ff1ed3962178de53bf627973 Mon Sep 17 00:00:00 2001 From: trufurs Date: Fri, 15 May 2026 14:32:51 +0000 Subject: [PATCH 1/6] fix: Improve sync_labels_api to handle args and doc_name more robustly --- .../doctype/gmail_account/gmail_account.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/frappe_gmail_thread/frappe_gmail_thread/doctype/gmail_account/gmail_account.py b/frappe_gmail_thread/frappe_gmail_thread/doctype/gmail_account/gmail_account.py index 85f43ba..c70ee37 100644 --- a/frappe_gmail_thread/frappe_gmail_thread/doctype/gmail_account/gmail_account.py +++ b/frappe_gmail_thread/frappe_gmail_thread/doctype/gmail_account/gmail_account.py @@ -132,10 +132,15 @@ def before_save(self): @frappe.whitelist() # nosemgrep -def sync_labels_api(args): - args = json.loads(args) - doc = frappe.get_doc("Gmail Account", args.get("doc_name")) - if args.get("reset_historyid", False): +def sync_labels_api(args: str | dict | None = None, doc_name: str | None = None): + if args: + if isinstance(args, str): + args = json.loads(args) + doc_name = doc_name or args.get("doc_name") + if not args and not doc_name: + frappe.throw(_("doc_name is required")) + doc = frappe.get_doc("Gmail Account", doc_name) + if args and args.get("reset_historyid", False): doc.last_historyid = 0 doc.save() doc.reload() From ae04b7461eba678f3cfa1162e72b8e865b76327a Mon Sep 17 00:00:00 2001 From: trufurs Date: Fri, 15 May 2026 14:33:32 +0000 Subject: [PATCH 2/6] fix: Update GmailInboundMail constructor to handle email_account and set attachment limit --- frappe_gmail_thread/utils/helpers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frappe_gmail_thread/utils/helpers.py b/frappe_gmail_thread/utils/helpers.py index e1a556d..d36ea29 100644 --- a/frappe_gmail_thread/utils/helpers.py +++ b/frappe_gmail_thread/utils/helpers.py @@ -10,7 +10,9 @@ class GmailInboundMail(Email): - def __init__(self, content): + def __init__(self, content, email_account=None): + self.email_account = email_account + self.email_account.attachment_limit = None super().__init__(content) # remove quoted replies from email text content self.text_content = self.pop_down_quoted_replies(self.text_content, "text") @@ -126,8 +128,7 @@ def create_new_email(email, gmail_account): email_content = base64.urlsafe_b64decode(email["raw"].encode("ASCII")).decode( "utf-8", errors="replace" ) - email_object = GmailInboundMail(content=email_content) - # check if email is sent or received + email_object = GmailInboundMail(content=email_content, email_account=gmail_account) is_sent = False # check if there is a user (not website user) with the same email as the sender in frappe, if yes, then it is a sent email is_sent = ( From 69cd3f0c58aa4bde6239567e151bc6960595fa8b Mon Sep 17 00:00:00 2001 From: dpk404 Date: Mon, 18 May 2026 08:31:34 +0000 Subject: [PATCH 3/6] get_attachments_data: skip deleted files; batch File lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs in the old per-attachment loop: 1. frappe.db.get_value("File", file_doc_name, "file_url") returns None when the File row has been deleted. The function then wrote attachment["file_url"] = None and returned it. The timeline_message_box template renders a.file_url.split("/") to derive the filename, which crashed with "Cannot read properties of null" — the whole form timeline failed to render. 2. The frappe.db.get_value call was inside the per-attachment loop, so a thread with N attachments did N round-trips against tabFile. Now a single frappe.db.get_all collects every referenced file_url, and attachments whose File row is gone are dropped from the returned list so the template never sees a null file_url. --- frappe_gmail_thread/api/activity.py | 35 ++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/frappe_gmail_thread/api/activity.py b/frappe_gmail_thread/api/activity.py index 7d2a4bc..3aa5a97 100644 --- a/frappe_gmail_thread/api/activity.py +++ b/frappe_gmail_thread/api/activity.py @@ -6,13 +6,42 @@ def get_attachments_data(email): attachments_data = json.loads(email.attachments_data) - # instead of using file_url from attachments_data, we have to use frappe.get_value to get the latest file_url + if not attachments_data: + return [] + + # Collect every referenced File name and resolve them in a single + # query — the earlier per-attachment frappe.db.get_value was N+1 + # against tabFile. + file_doc_names = { + a["file_doc_name"] + for a in attachments_data + if a.get("file_doc_name") + } + file_url_by_name: dict[str, str] = {} + if file_doc_names: + file_url_by_name = { + r["name"]: r["file_url"] + for r in frappe.db.get_all( + "File", + filters={"name": ("in", list(file_doc_names))}, + fields=["name", "file_url"], + ) + } + + valid: list[dict] = [] for attachment in attachments_data: file_doc_name = attachment.get("file_doc_name") if file_doc_name: - file_url = frappe.db.get_value("File", file_doc_name, "file_url") + file_url = file_url_by_name.get(file_doc_name) + if not file_url: + # File row was deleted (manual cleanup, expired temp, + # etc.). Skip — rendering it would crash the timeline + # template at `file_url.split("/")`. + continue attachment["file_url"] = file_url - return attachments_data + if attachment.get("file_url"): + valid.append(attachment) + return valid @frappe.whitelist() From 199232d47591fd280294830b46766a13010b4296 Mon Sep 17 00:00:00 2001 From: trufurs Date: Mon, 1 Jun 2026 11:10:25 +0000 Subject: [PATCH 4/6] fix: Enhance attachment data handling and improve GmailInboundMail initialization --- frappe_gmail_thread/api/activity.py | 36 ++++++++++++------- .../doctype/gmail_account/gmail_account.py | 5 +-- frappe_gmail_thread/utils/helpers.py | 10 +++--- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/frappe_gmail_thread/api/activity.py b/frappe_gmail_thread/api/activity.py index 3aa5a97..c282160 100644 --- a/frappe_gmail_thread/api/activity.py +++ b/frappe_gmail_thread/api/activity.py @@ -1,21 +1,34 @@ -import json - import frappe import frappe.utils +from frappe import _ def get_attachments_data(email): - attachments_data = json.loads(email.attachments_data) - if not attachments_data: + raw_attachments = getattr(email, "attachments_data", None) + if not raw_attachments: + return [] + + if isinstance(raw_attachments, str): + try: + attachments_data = frappe.parse_json(raw_attachments) + except (TypeError, ValueError, frappe.ValidationError): + frappe.log_error( + title=_("Invalid Gmail attachments_data JSON"), + message=frappe.get_traceback(), + ) + return [] + elif isinstance(raw_attachments, list): + attachments_data = raw_attachments + else: + return [] + + if not attachments_data or not isinstance(attachments_data, list): return [] - # Collect every referenced File name and resolve them in a single - # query — the earlier per-attachment frappe.db.get_value was N+1 - # against tabFile. file_doc_names = { - a["file_doc_name"] + a.get("file_doc_name") for a in attachments_data - if a.get("file_doc_name") + if isinstance(a, dict) and a.get("file_doc_name") } file_url_by_name: dict[str, str] = {} if file_doc_names: @@ -30,13 +43,12 @@ def get_attachments_data(email): valid: list[dict] = [] for attachment in attachments_data: + if not isinstance(attachment, dict): + continue file_doc_name = attachment.get("file_doc_name") if file_doc_name: file_url = file_url_by_name.get(file_doc_name) if not file_url: - # File row was deleted (manual cleanup, expired temp, - # etc.). Skip — rendering it would crash the timeline - # template at `file_url.split("/")`. continue attachment["file_url"] = file_url if attachment.get("file_url"): diff --git a/frappe_gmail_thread/frappe_gmail_thread/doctype/gmail_account/gmail_account.py b/frappe_gmail_thread/frappe_gmail_thread/doctype/gmail_account/gmail_account.py index c70ee37..d79059a 100644 --- a/frappe_gmail_thread/frappe_gmail_thread/doctype/gmail_account/gmail_account.py +++ b/frappe_gmail_thread/frappe_gmail_thread/doctype/gmail_account/gmail_account.py @@ -1,9 +1,6 @@ # Copyright (c) 2024, rtCamp and contributors # For license information, please see license.txt -# import frappe -import json - import frappe from frappe import _ from frappe.model.document import Document @@ -135,7 +132,7 @@ def before_save(self): def sync_labels_api(args: str | dict | None = None, doc_name: str | None = None): if args: if isinstance(args, str): - args = json.loads(args) + args = frappe.parse_json(args) doc_name = doc_name or args.get("doc_name") if not args and not doc_name: frappe.throw(_("doc_name is required")) diff --git a/frappe_gmail_thread/utils/helpers.py b/frappe_gmail_thread/utils/helpers.py index d36ea29..ec5db73 100644 --- a/frappe_gmail_thread/utils/helpers.py +++ b/frappe_gmail_thread/utils/helpers.py @@ -11,10 +11,12 @@ class GmailInboundMail(Email): def __init__(self, content, email_account=None): - self.email_account = email_account - self.email_account.attachment_limit = None - super().__init__(content) - # remove quoted replies from email text content + # temp compatibility with frappe.email.receive.Email + self.email_account = email_account or frappe._dict() + if not hasattr(self.email_account, "attachment_limit"): + self.email_account.attachment_limit = None + + super().__init__(content, email_account=self.email_account) self.text_content = self.pop_down_quoted_replies(self.text_content, "text") self.html_content = self.pop_down_quoted_replies(self.html_content, "html") self.set_content_and_type() From 1c721b458a171c5ef6da0332c62ef2d2f034a3ac Mon Sep 17 00:00:00 2001 From: trufurs Date: Mon, 1 Jun 2026 12:22:41 +0000 Subject: [PATCH 5/6] fix: Update GmailInboundMail initialization to remove email_account parameter --- frappe_gmail_thread/utils/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe_gmail_thread/utils/helpers.py b/frappe_gmail_thread/utils/helpers.py index ec5db73..701f8c0 100644 --- a/frappe_gmail_thread/utils/helpers.py +++ b/frappe_gmail_thread/utils/helpers.py @@ -16,7 +16,7 @@ def __init__(self, content, email_account=None): if not hasattr(self.email_account, "attachment_limit"): self.email_account.attachment_limit = None - super().__init__(content, email_account=self.email_account) + super().__init__(content) self.text_content = self.pop_down_quoted_replies(self.text_content, "text") self.html_content = self.pop_down_quoted_replies(self.html_content, "html") self.set_content_and_type() From f5844bcf466c8c19e61a8c67afeb10382f2ccafc Mon Sep 17 00:00:00 2001 From: Himanshu Jain <118419692+trufurs@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:31:00 +0530 Subject: [PATCH 6/6] Bump version from 1.0.0 to 1.0.1 --- frappe_gmail_thread/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe_gmail_thread/__init__.py b/frappe_gmail_thread/__init__.py index 5becc17..5c4105c 100644 --- a/frappe_gmail_thread/__init__.py +++ b/frappe_gmail_thread/__init__.py @@ -1 +1 @@ -__version__ = "1.0.0" +__version__ = "1.0.1"