From 411f47e0393e7821501f8acf1cbf71746b6bef18 Mon Sep 17 00:00:00 2001 From: bolade Date: Wed, 13 Aug 2025 09:00:18 +0100 Subject: [PATCH] feat: Increase max email fetch results and enhance email body extraction --- src/app.py | 2 +- src/zoho_client.py | 71 +++++++++++++++++++++++++++++++++++++--------- static/styles.css | 38 ++++++++++++++++++++++--- 3 files changed, 92 insertions(+), 19 deletions(-) diff --git a/src/app.py b/src/app.py index 3cb8cfe..24f10a1 100644 --- a/src/app.py +++ b/src/app.py @@ -364,7 +364,7 @@ def _sync_emails_once(cfg: dict) -> int: days_back = max(1, delta_days) except Exception: pass - max_results = 5 + max_results = 100 client = ZohoClient( email=cfg.get("zoho_email") or account_email, app_password=cfg.get("zoho_app_password"), diff --git a/src/zoho_client.py b/src/zoho_client.py index e5f9bd5..c7ea940 100644 --- a/src/zoho_client.py +++ b/src/zoho_client.py @@ -132,21 +132,28 @@ class ZohoClient: email_message = email.message_from_bytes(raw_email) date_header = email_message.get("Date", "") email_date = parse_email_date_safely(date_header) - - + # Ensure both dates are timezone-aware for comparison if email_date and latest_date: print(f"📅 Email date: {email_date} Latest: {latest_date}") # If latest_date is timezone-naive, make it timezone-aware (assume UTC) if latest_date.tzinfo is None: latest_date = latest_date.replace(tzinfo=timezone.utc) - + if (email_date > latest_date) or first_time: - # Extract headers - print(f"📅 Email date: {email_date} Latest: {latest_date}") - subject = self._decode_header(email_message.get("Subject", "")) - from_header = self._decode_header(email_message.get("From", "")) - to_header = self._decode_header(email_message.get("To", "")) + # Extract headers + print( + f"📅 Email date: {email_date} Latest: {latest_date}" + ) + subject = self._decode_header( + email_message.get("Subject", "") + ) + from_header = self._decode_header( + email_message.get("From", "") + ) + to_header = self._decode_header( + email_message.get("To", "") + ) message_id = email_message.get("Message-ID", "") in_reply_to = email_message.get("In-Reply-To", "") @@ -156,7 +163,6 @@ class ZohoClient: # Get email body snippet body = self._get_email_body(email_message) - email_data = { "id": num.decode(), @@ -170,7 +176,7 @@ class ZohoClient: "folder": folder, "snippet": body, } - + emails.append(email_data) logging.info(f"Long body: {body}") except Exception as e: @@ -227,18 +233,22 @@ class ZohoClient: return str(header_value) def _get_email_body(self, email_message) -> str: - """Extract email body text""" + """Extract email body text - get only the main content, not quoted replies""" body = "" if email_message.is_multipart(): + # Get only the first text/plain part (main content) for part in email_message.walk(): - if part.get_content_type() == "text/plain": + if part.get_content_type() == "text/plain" and not part.get_filename(): try: - body += part.get_payload(decode=True).decode( + content = part.get_payload(decode=True).decode( "utf-8", errors="ignore" ) + # Take only the first text part we find + body = content + break # Stop after first text/plain part except Exception: - pass + continue else: try: body = email_message.get_payload(decode=True).decode( @@ -247,8 +257,41 @@ class ZohoClient: except Exception: pass + # Optional: Clean up the body by removing quoted content + body = self._clean_email_body(body) return body + def _clean_email_body(self, body: str) -> str: + """Clean email body by removing quoted content and signatures""" + if not body: + return "" + + lines = body.split("\n") + cleaned_lines = [] + + for line in lines: + line = line.strip() + + # Stop at common quote indicators + if ( + line.startswith("---- On ") + or line.startswith("On ") + and "wrote:" in line + or line.startswith("From:") + or line.startswith("> ") + or line.startswith("-----Original Message-----") + or line.startswith("---------- Forwarded message ---------") + ): + break + + cleaned_lines.append(line) + + # Remove trailing empty lines + while cleaned_lines and not cleaned_lines[-1]: + cleaned_lines.pop() + + return "\n".join(cleaned_lines) + def get_thread_messages(self, thread_id: str) -> List[Dict[str, Any]]: """Get all messages in a thread (simplified for IMAP)""" # For IMAP, we'll return a single message since thread grouping is more complex diff --git a/static/styles.css b/static/styles.css index d8e49da..b12a465 100644 --- a/static/styles.css +++ b/static/styles.css @@ -61,7 +61,17 @@ th { background: #0f152a; color: var(--muted); text-align: left; position: stick tbody tr:hover { background: #0e1426; } pre, code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } -pre { background: #0b1121; padding: 0.75rem; border-radius: 8px; white-space: pre-wrap; border: 1px solid var(--border); } +pre { + background: #0b1121; + padding: 0.75rem; + border-radius: 8px; + white-space: pre-wrap; + word-wrap: break-word; + overflow-wrap: break-word; + border: 1px solid var(--border); + max-width: 100%; + overflow-x: auto; +} /* Chat-style messages */ .messages { display: flex; flex-direction: column; gap: 0.75rem; } @@ -72,12 +82,25 @@ pre { background: #0b1121; padding: 0.75rem; border-radius: 8px; white-space: pr max-width: 800px; width: fit-content; background: #0f152a; border: 1px solid var(--border); border-radius: 12px; padding: 0.75rem 0.9rem; box-shadow: 0 4px 20px rgba(0,0,0,0.2); + word-wrap: break-word; + overflow-wrap: break-word; + overflow-x: auto; } .incoming .msg-bubble { background: #0f152a; } .outgoing .msg-bubble { background: var(--brand-weak); border-color: #345fb0; } -.msg-meta { font-size: 0.78rem; color: var(--muted); margin-bottom: 0.35rem; } +.msg-meta { + font-size: 0.78rem; + color: var(--muted); + margin-bottom: 0.35rem; + word-wrap: break-word; + overflow-wrap: break-word; +} .msg-subject { font-size: 0.9rem; margin-bottom: 0.25rem; color: var(--text); } -.msg-body { font-size: 0.92rem; } +.msg-body { + font-size: 0.92rem; + word-wrap: break-word; + overflow-wrap: break-word; +} .row { display: flex; gap: 1rem; flex-wrap: wrap; } .col { flex: 1 1 360px; } @@ -87,7 +110,14 @@ a { color: var(--brand); } a:hover { text-decoration: underline; } /* Small helpers */ -.pill { padding: 0.15rem 0.5rem; border-radius: 999px; border: 1px solid var(--border); } +.pill { + padding: 0.15rem 0.5rem; + border-radius: 999px; + border: 1px solid var(--border); + word-wrap: break-word; + overflow-wrap: break-word; + display: inline-block; +} .right { text-align: right; } .mt-1 { margin-top: 0.5rem; } .mt-2 { margin-top: 1rem; } .mb-1 { margin-bottom: 0.5rem; } .mb-2 { margin-bottom: 1rem; }