From 27fc2e29a17ec209649b306e4d7d2f92ee095ca4 Mon Sep 17 00:00:00 2001 From: Bryce Date: Tue, 5 Aug 2025 12:37:36 -0700 Subject: [PATCH] starts work on recent emails. --- .roo/rules/01-general.md | 2 + app/imap_service.py | 84 +++++++++++++++++- app/models.py | 1 + app/templates/base.html | 2 + app/templates/partials/folder_card.html | 2 +- docs/design/imap-connectivity.md | 88 +++++++++++++++++++ ...dd_recent_emails_field_to_folders_table.py | 32 +++++++ 7 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 migrations/versions/9a88c7e94083_add_recent_emails_field_to_folders_table.py diff --git a/.roo/rules/01-general.md b/.roo/rules/01-general.md index 9b8d24d..a2a3fdb 100644 --- a/.roo/rules/01-general.md +++ b/.roo/rules/01-general.md @@ -13,6 +13,8 @@ Here are special rules you must follow: 11. Design docs go into docs/design/*.md. These docs are always kept up to date. 12. Before completing work, ensure that no design docs are left out of sync 13. Plans go into docs/plans/*.md. These may not be kept in sync, as they are just for brainstorming. +14. Database migrations are automatically created via `flask db migrate -m 'message'`. NEVER create migrations by hand. + # Conventions 1. modals are rendered server-side, by targeting #modal-holder, and using an hx-trigger response attribute for open-modal. diff --git a/app/imap_service.py b/app/imap_service.py index f11452c..72b7947 100644 --- a/app/imap_service.py +++ b/app/imap_service.py @@ -3,6 +3,7 @@ import ssl import logging from typing import List, Dict, Optional, Tuple from email.header import decode_header +from email.utils import parsedate_to_datetime from app.models import db, Folder, User from app import create_app @@ -173,6 +174,83 @@ class IMAPService: self.connection = None return 0 + def get_recent_emails(self, folder_name: str, limit: int = 3) -> List[Dict[str, any]]: + """Get the most recent email subjects and dates from a specific folder.""" + try: + # Connect to IMAP server + self._connect() + + # Login + self.connection.login( + self.config.get('username', ''), + self.config.get('password', '') + ) + + # Select the folder + resp_code, content = self.connection.select(folder_name) + if resp_code != 'OK': + return [] + + # Get email IDs (most recent first) + resp_code, content = self.connection.search(None, 'ALL') + if resp_code != 'OK': + return [] + + # Get the most recent emails (limit to 3) + email_ids = content[0].split() + recent_email_ids = email_ids[:limit] if len(email_ids) >= limit else email_ids + + recent_emails = [] + for email_id in reversed(recent_email_ids): # Process from newest to oldest + # Fetch the email headers + resp_code, content = self.connection.fetch(email_id, '(RFC822.HEADER)') + if resp_code != 'OK': + continue + + # Parse the email headers + raw_email = content[0][1] + import email + msg = email.message_from_bytes(raw_email) + + # Extract subject and date + subject = msg.get('Subject', 'No Subject') + date_str = msg.get('Date', '') + + # Decode the subject if needed + try: + decoded_parts = decode_header(subject) + subject = ''.join([str(part, encoding or 'utf-8') if isinstance(part, bytes) else part for part, encoding in decoded_parts]) + except Exception: + pass # If decoding fails, use the original subject + + # Parse date if available + try: + email_date = parsedate_to_datetime(date_str) if date_str else None + except Exception: + email_date = None + + recent_emails.append({ + 'subject': subject, + 'date': email_date.isoformat() if email_date else None + }) + + # Close folder and logout + self.connection.close() + self.connection.logout() + self.connection = None + + return recent_emails + + except Exception as e: + logging.error(f"Error getting recent emails for folder {folder_name}: {str(e)}") + if self.connection: + try: + self.connection.logout() + except: + pass + self.connection = None + return [] + def sync_folders(self) -> Tuple[bool, str]: """Sync IMAP folders with local database.""" try: @@ -215,11 +293,15 @@ class IMAPService: db.session.add(new_folder) synced_count += 1 else: - # Update existing folder with email counts + # Update existing folder with email counts and recent emails # Get the total count of emails in this folder total_count = self.get_folder_email_count(folder_name) existing_folder.total_count = total_count existing_folder.pending_count = 0 # Initially set to 0 + + # Get the most recent emails for this folder + recent_emails = self.get_recent_emails(folder_name, 3) + existing_folder.recent_emails = recent_emails db.session.commit() return True, f"Successfully synced {synced_count} folders" diff --git a/app/models.py b/app/models.py index febf6af..bd8f6b1 100644 --- a/app/models.py +++ b/app/models.py @@ -42,5 +42,6 @@ class Folder(Base): organize_enabled = db.Column(db.Boolean, default=True) total_count = db.Column(db.Integer, default=0) pending_count = db.Column(db.Integer, default=0) + recent_emails = db.Column(db.JSON, default=list) # Store recent email subjects with dates user = db.relationship('User', backref=db.backref('folders', lazy=True)) \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index 740fc87..d4b4034 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -10,6 +10,8 @@ + +