starts work on recent emails.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
@@ -10,6 +10,8 @@
|
||||
<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">
|
||||
<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>
|
||||
<style>
|
||||
@keyframes shake {
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="flex space-x-1">
|
||||
<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>
|
||||
{% if folder.priority == 1 %}
|
||||
<span class="badge badge-error">High Priority</span>
|
||||
|
||||
@@ -80,6 +80,17 @@ class User(Base, UserMixin):
|
||||
# ... 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
|
||||
|
||||
```json
|
||||
@@ -267,6 +278,83 @@ class IMAPService:
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
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
|
||||
|
||||
@@ -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 ###
|
||||
Reference in New Issue
Block a user