starts work on recent emails.

This commit is contained in:
Bryce
2025-08-05 12:37:36 -07:00
parent 2335c4ceca
commit 27fc2e29a1
7 changed files with 209 additions and 2 deletions

View File

@@ -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. 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 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. 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 # Conventions
1. modals are rendered server-side, by targeting #modal-holder, and using an hx-trigger response attribute for open-modal. 1. modals are rendered server-side, by targeting #modal-holder, and using an hx-trigger response attribute for open-modal.

View File

@@ -3,6 +3,7 @@ import ssl
import logging import logging
from typing import List, Dict, Optional, Tuple from typing import List, Dict, Optional, Tuple
from email.header import decode_header from email.header import decode_header
from email.utils import parsedate_to_datetime
from app.models import db, Folder, User from app.models import db, Folder, User
from app import create_app from app import create_app
@@ -173,6 +174,83 @@ class IMAPService:
self.connection = None self.connection = None
return 0 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]: def sync_folders(self) -> Tuple[bool, str]:
"""Sync IMAP folders with local database.""" """Sync IMAP folders with local database."""
try: try:
@@ -215,12 +293,16 @@ class IMAPService:
db.session.add(new_folder) db.session.add(new_folder)
synced_count += 1 synced_count += 1
else: 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 # Get the total count of emails in this folder
total_count = self.get_folder_email_count(folder_name) total_count = self.get_folder_email_count(folder_name)
existing_folder.total_count = total_count existing_folder.total_count = total_count
existing_folder.pending_count = 0 # Initially set to 0 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() db.session.commit()
return True, f"Successfully synced {synced_count} folders" return True, f"Successfully synced {synced_count} folders"

View File

@@ -42,5 +42,6 @@ class Folder(Base):
organize_enabled = db.Column(db.Boolean, default=True) organize_enabled = db.Column(db.Boolean, default=True)
total_count = db.Column(db.Integer, default=0) total_count = db.Column(db.Integer, default=0)
pending_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)) user = db.relationship('User', backref=db.backref('folders', lazy=True))

View File

@@ -10,6 +10,8 @@
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css" /> <link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@ryangjchandler/alpine-tooltip@1.x.x/dist/cdn.min.js" defer></script>
<link rel="stylesheet" href="https://unpkg.com/tippy.js@6/dist/tippy.css" />
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<style> <style>
@keyframes shake { @keyframes shake {

View File

@@ -31,7 +31,7 @@
<div class="flex justify-between items-center mb-2"> <div class="flex justify-between items-center mb-2">
<div class="flex space-x-1"> <div class="flex space-x-1">
<span class="badge badge-outline">{{ folder.total_count }} emails</span> <span class="badge badge-outline">{{ folder.total_count }} emails</span>
<span class="badge badge-secondary">{{ folder.pending_count }} pending</span> <span class="badge badge-secondary" x-tooltip.raw="{% if folder.recent_emails %}<table class='text-xs'><tr><th class='text-left pr-2'>Subject</th><th class='text-left'>Date</th></tr>{% for email in folder.recent_emails %}<tr><td class='text-left pr-2 truncate max-w-[150px]'>{{ email.subject }}</td><td class='text-left'>{{ email.date[:10] if email.date else 'N/A' }}</td></tr>{% endfor %}</table>{% else %}No recent emails{% endif %}">{{ folder.pending_count }} pending</span>
</div> </div>
{% if folder.priority == 1 %} {% if folder.priority == 1 %}
<span class="badge badge-error">High Priority</span> <span class="badge badge-error">High Priority</span>

View File

@@ -80,6 +80,17 @@ class User(Base, UserMixin):
# ... existing fields ... # ... existing fields ...
``` ```
### Folder Model Updates
The `Folder` model now includes a new field for storing recent email information:
```python
class Folder(Base):
# ... existing fields ...
recent_emails = db.Column(db.JSON, default=list) # Store recent email subjects with dates
# ... existing fields ...
```
### IMAP Configuration Structure ### IMAP Configuration Structure
```json ```json
@@ -267,6 +278,83 @@ class IMAPService:
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
return False, f"Sync error: {str(e)}" return False, f"Sync error: {str(e)}"
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 []
``` ```
## Routes Implementation ## Routes Implementation

View File

@@ -0,0 +1,32 @@
"""Add recent_emails field to folders table
Revision ID: 9a88c7e94083
Revises: a3ad1b9a0e5f
Create Date: 2025-08-05 11:20:32.637203
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9a88c7e94083'
down_revision = 'a3ad1b9a0e5f'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('folders', schema=None) as batch_op:
batch_op.add_column(sa.Column('recent_emails', sa.JSON(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('folders', schema=None) as batch_op:
batch_op.drop_column('recent_emails')
# ### end Alembic commands ###