lots of configuration progress.
This commit is contained in:
@@ -13,7 +13,7 @@ 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.
|
||||
14. ****IMPORTANT**** Database migrations are automatically created via `flask db migrate -m 'message'`. **NEVER** create migrations by hand. You should never have to read the contents of migrations/
|
||||
15. In the technical documentation, code should be used sparingly. Also, when needed, focus on the APIs, and only use snippets.
|
||||
|
||||
|
||||
|
||||
@@ -251,6 +251,102 @@ class IMAPService:
|
||||
self.connection = None
|
||||
return []
|
||||
|
||||
def get_folder_email_uids(self, folder_name: str) -> List[str]:
|
||||
"""Get the list of email UIDs in 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 UIDs
|
||||
resp_code, content = self.connection.search(None, 'ALL')
|
||||
if resp_code != 'OK':
|
||||
return []
|
||||
|
||||
# Extract UIDs
|
||||
email_uids = content[0].split()
|
||||
uid_list = [uid.decode('utf-8') for uid in email_uids]
|
||||
|
||||
# Close folder and logout
|
||||
self.connection.close()
|
||||
self.connection.logout()
|
||||
self.connection = None
|
||||
|
||||
return uid_list
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting email UIDs for folder {folder_name}: {str(e)}")
|
||||
if self.connection:
|
||||
try:
|
||||
self.connection.logout()
|
||||
except:
|
||||
pass
|
||||
self.connection = None
|
||||
return []
|
||||
|
||||
def get_email_headers(self, folder_name: str, email_uid: str) -> Dict[str, str]:
|
||||
"""Get email headers for a specific email UID."""
|
||||
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 {}
|
||||
|
||||
# Fetch email headers
|
||||
resp_code, content = self.connection.fetch(email_uid, '(RFC822.HEADER)')
|
||||
if resp_code != 'OK':
|
||||
return {}
|
||||
|
||||
# Parse the email headers
|
||||
raw_email = content[0][1]
|
||||
import email
|
||||
msg = email.message_from_bytes(raw_email)
|
||||
|
||||
# Extract headers
|
||||
headers = {
|
||||
'subject': msg.get('Subject', 'No Subject'),
|
||||
'date': msg.get('Date', ''),
|
||||
'from': msg.get('From', ''),
|
||||
'to': msg.get('To', ''),
|
||||
'message_id': msg.get('Message-ID', '')
|
||||
}
|
||||
|
||||
# Close folder and logout
|
||||
self.connection.close()
|
||||
self.connection.logout()
|
||||
self.connection = None
|
||||
|
||||
return headers
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting email headers for UID {email_uid} in 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:
|
||||
|
||||
@@ -40,8 +40,29 @@ class Folder(Base):
|
||||
rule_text = db.Column(db.Text)
|
||||
priority = db.Column(db.Integer)
|
||||
organize_enabled = db.Column(db.Boolean, default=True)
|
||||
folder_type = db.Column(db.String(20), default='destination', nullable=False)
|
||||
total_count = db.Column(db.Integer, default=0)
|
||||
pending_count = db.Column(db.Integer, default=0)
|
||||
emails_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))
|
||||
|
||||
class ProcessedEmail(Base):
|
||||
__tablename__ = 'processed_emails'
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
folder_id = db.Column(db.Integer, db.ForeignKey('folders.id'), nullable=False)
|
||||
email_uid = db.Column(db.String(255), nullable=False)
|
||||
folder_name = db.Column(db.String(255), nullable=False)
|
||||
is_processed = db.Column(db.Boolean, default=False)
|
||||
first_seen_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
processed_at = db.Column(db.DateTime, nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
user = db.relationship('User', backref=db.backref('processed_emails', lazy=True))
|
||||
folder = db.relationship('Folder', backref=db.backref('processed_emails', lazy=True))
|
||||
|
||||
def __repr__(self):
|
||||
return f'<ProcessedEmail {self.email_uid} for folder {self.folder_name}>'
|
||||
235
app/processed_emails_service.py
Normal file
235
app/processed_emails_service.py
Normal file
@@ -0,0 +1,235 @@
|
||||
from typing import List, Dict, Optional
|
||||
from datetime import datetime
|
||||
from app.models import db, ProcessedEmail, User, Folder
|
||||
from sqlalchemy import and_, or_
|
||||
|
||||
|
||||
class ProcessedEmailsService:
|
||||
def __init__(self, user: User):
|
||||
self.user = user
|
||||
|
||||
def get_pending_emails(self, folder_name: str) -> List[str]:
|
||||
"""Get list of email UIDs that are pending processing in a folder."""
|
||||
try:
|
||||
pending_emails = ProcessedEmail.query.filter_by(
|
||||
user_id=self.user.id,
|
||||
folder_name=folder_name,
|
||||
is_processed=False
|
||||
).all()
|
||||
|
||||
return [email.email_uid for email in pending_emails]
|
||||
except Exception as e:
|
||||
print(f"Error getting pending emails: {str(e)}")
|
||||
return []
|
||||
|
||||
def mark_email_processed(self, folder_name: str, email_uid: str) -> bool:
|
||||
"""Mark an email as processed."""
|
||||
try:
|
||||
processed_email = ProcessedEmail.query.filter_by(
|
||||
user_id=self.user.id,
|
||||
folder_name=folder_name,
|
||||
email_uid=email_uid
|
||||
).first()
|
||||
|
||||
if processed_email:
|
||||
processed_email.is_processed = True
|
||||
processed_email.processed_at = datetime.utcnow()
|
||||
processed_email.updated_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
return True
|
||||
else:
|
||||
# Get the folder object to set the folder_id
|
||||
folder = Folder.query.filter_by(
|
||||
user_id=self.user.id,
|
||||
name=folder_name
|
||||
).first()
|
||||
|
||||
if folder:
|
||||
# Create a new record if it doesn't exist
|
||||
new_record = ProcessedEmail(
|
||||
user_id=self.user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name=folder_name,
|
||||
email_uid=email_uid,
|
||||
is_processed=True,
|
||||
processed_at=datetime.utcnow()
|
||||
)
|
||||
db.session.add(new_record)
|
||||
db.session.commit()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error marking email as processed: {str(e)}")
|
||||
db.session.rollback()
|
||||
return False
|
||||
|
||||
def mark_emails_processed(self, folder_name: str, email_uids: List[str]) -> int:
|
||||
"""Mark multiple emails as processed in bulk."""
|
||||
try:
|
||||
updated_count = 0
|
||||
|
||||
# Update existing records
|
||||
ProcessedEmail.query.filter_by(
|
||||
user_id=self.user.id,
|
||||
folder_name=folder_name,
|
||||
is_processed=False
|
||||
).filter(ProcessedEmail.email_uid.in_(email_uids)).update(
|
||||
{
|
||||
'is_processed': True,
|
||||
'processed_at': datetime.utcnow(),
|
||||
'updated_at': datetime.utcnow()
|
||||
},
|
||||
synchronize_session=False
|
||||
)
|
||||
|
||||
# Check for any email UIDs that don't have records yet
|
||||
existing_uids = {email.email_uid for email in ProcessedEmail.query.filter_by(
|
||||
user_id=self.user.id,
|
||||
folder_name=folder_name
|
||||
).all()}
|
||||
|
||||
missing_uids = set(email_uids) - existing_uids
|
||||
if missing_uids:
|
||||
# Get the folder object to set the folder_id
|
||||
folder = Folder.query.filter_by(
|
||||
user_id=self.user.id,
|
||||
name=folder_name
|
||||
).first()
|
||||
|
||||
if folder:
|
||||
# Create new records for missing UIDs
|
||||
new_records = [
|
||||
ProcessedEmail(
|
||||
user_id=self.user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name=folder_name,
|
||||
email_uid=uid,
|
||||
is_processed=True,
|
||||
processed_at=datetime.utcnow()
|
||||
)
|
||||
for uid in missing_uids
|
||||
]
|
||||
db.session.bulk_save_objects(new_records)
|
||||
|
||||
updated_count = len(email_uids)
|
||||
db.session.commit()
|
||||
return updated_count
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error marking emails as processed: {str(e)}")
|
||||
db.session.rollback()
|
||||
return 0
|
||||
|
||||
def sync_folder_emails(self, folder_name: str, email_uids: List[str]) -> int:
|
||||
"""Sync email UIDs for a folder, adding new ones as pending."""
|
||||
try:
|
||||
# Get existing UIDs for this folder
|
||||
existing_uids = {email.email_uid for email in ProcessedEmail.query.filter_by(
|
||||
user_id=self.user.id,
|
||||
folder_name=folder_name
|
||||
).all()}
|
||||
|
||||
# Find new UIDs that don't exist yet
|
||||
new_uids = set(email_uids) - existing_uids
|
||||
|
||||
# Create new records for new UIDs
|
||||
if new_uids:
|
||||
# Get the folder object to set the folder_id
|
||||
folder = Folder.query.filter_by(
|
||||
user_id=self.user.id,
|
||||
name=folder_name
|
||||
).first()
|
||||
|
||||
if folder:
|
||||
new_records = [
|
||||
ProcessedEmail(
|
||||
user_id=self.user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name=folder_name,
|
||||
email_uid=uid,
|
||||
is_processed=False
|
||||
)
|
||||
for uid in new_uids
|
||||
]
|
||||
db.session.bulk_save_objects(new_records)
|
||||
|
||||
# Get the folder to update counts
|
||||
folder = Folder.query.filter_by(
|
||||
user_id=self.user.id,
|
||||
name=folder_name
|
||||
).first()
|
||||
|
||||
if folder:
|
||||
# Update pending count
|
||||
pending_count = ProcessedEmail.query.filter_by(
|
||||
user_id=self.user.id,
|
||||
folder_name=folder_name,
|
||||
is_processed=False
|
||||
).count()
|
||||
folder.pending_count = pending_count
|
||||
folder.total_count = len(email_uids)
|
||||
db.session.commit()
|
||||
|
||||
return len(new_uids)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error syncing folder emails: {str(e)}")
|
||||
db.session.rollback()
|
||||
return 0
|
||||
|
||||
def get_pending_count(self, folder_name: str) -> int:
|
||||
"""Get count of pending emails for a folder."""
|
||||
try:
|
||||
count = ProcessedEmail.query.filter_by(
|
||||
user_id=self.user.id,
|
||||
folder_name=folder_name,
|
||||
is_processed=False
|
||||
).count()
|
||||
return count
|
||||
except Exception as e:
|
||||
print(f"Error getting pending count: {str(e)}")
|
||||
return 0
|
||||
|
||||
def cleanup_old_records(self, folder_name: str, current_uids: List[str]) -> int:
|
||||
"""Remove records for emails that no longer exist in the folder."""
|
||||
try:
|
||||
# Find records that no longer exist in the current UIDs
|
||||
records_to_delete = ProcessedEmail.query.filter(
|
||||
and_(
|
||||
ProcessedEmail.user_id == self.user.id,
|
||||
ProcessedEmail.folder_name == folder_name,
|
||||
~ProcessedEmail.email_uid.in_(current_uids)
|
||||
)
|
||||
).all()
|
||||
|
||||
# Delete the records
|
||||
for record in records_to_delete:
|
||||
db.session.delete(record)
|
||||
|
||||
deleted_count = len(records_to_delete)
|
||||
db.session.commit()
|
||||
|
||||
# Update folder counts after cleanup
|
||||
folder = Folder.query.filter_by(
|
||||
user_id=self.user.id,
|
||||
name=folder_name
|
||||
).first()
|
||||
|
||||
if folder:
|
||||
pending_count = ProcessedEmail.query.filter_by(
|
||||
user_id=self.user.id,
|
||||
folder_name=folder_name,
|
||||
is_processed=False
|
||||
).count()
|
||||
folder.pending_count = pending_count
|
||||
folder.total_count = len(current_uids)
|
||||
db.session.commit()
|
||||
|
||||
return deleted_count
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error cleaning up old records: {str(e)}")
|
||||
db.session.rollback()
|
||||
return 0
|
||||
397
app/routes.py
397
app/routes.py
@@ -1,8 +1,9 @@
|
||||
from flask import Blueprint, render_template, request, jsonify, make_response, flash, redirect, url_for
|
||||
from flask_login import login_required, current_user
|
||||
from app import db
|
||||
from app.models import Folder, User
|
||||
from app.models import Folder, User, ProcessedEmail
|
||||
from app.imap_service import IMAPService
|
||||
from app.processed_emails_service import ProcessedEmailsService
|
||||
import uuid
|
||||
import logging
|
||||
|
||||
@@ -13,7 +14,15 @@ main = Blueprint('main', __name__)
|
||||
def index():
|
||||
# Get folders for the current authenticated user
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template('index.html', folders=folders)
|
||||
|
||||
# Separate folders by type
|
||||
tidy_folders = [folder for folder in folders if folder.folder_type == 'tidy']
|
||||
destination_folders = [folder for folder in folders if folder.folder_type == 'destination']
|
||||
|
||||
return render_template('index.html',
|
||||
folders=folders,
|
||||
tidy_folders=tidy_folders,
|
||||
destination_folders=destination_folders)
|
||||
|
||||
@main.route('/api/folders/new', methods=['GET'])
|
||||
@login_required
|
||||
@@ -56,11 +65,15 @@ def add_folder():
|
||||
return response
|
||||
|
||||
# Create new folder for the current user
|
||||
# Default to 'destination' type for manually created folders
|
||||
folder_type = 'tidy' if name.strip().lower() == 'inbox' else 'destination'
|
||||
|
||||
folder = Folder(
|
||||
user_id=current_user.id,
|
||||
name=name.strip(),
|
||||
rule_text=rule_text.strip(),
|
||||
priority=int(priority) if priority else 0
|
||||
priority=int(priority) if priority else 0,
|
||||
folder_type=folder_type
|
||||
)
|
||||
|
||||
db.session.add(folder)
|
||||
@@ -69,8 +82,15 @@ def add_folder():
|
||||
# Get updated list of folders for the current user
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
|
||||
# Return the updated folders list HTML
|
||||
response = make_response(render_template('partials/folders_list.html', folders=folders))
|
||||
# Get updated lists of folders by type
|
||||
tidy_folders = [folder for folder in folders if folder.folder_type == 'tidy']
|
||||
destination_folders = [folder for folder in folders if folder.folder_type == 'destination']
|
||||
|
||||
# Return both sections
|
||||
tidy_section = render_template('partials/folders_to_tidy_section.html', tidy_folders=tidy_folders)
|
||||
destination_section = render_template('partials/destination_folders_section.html', destination_folders=destination_folders)
|
||||
|
||||
response = make_response(tidy_section + destination_section)
|
||||
response.headers['HX-Trigger'] = 'close-modal'
|
||||
response.status_code = 201
|
||||
return response
|
||||
@@ -81,6 +101,15 @@ def add_folder():
|
||||
db.session.rollback()
|
||||
# Return error in modal
|
||||
errors = {'general': 'An unexpected error occurred. Please try again.'}
|
||||
# Get updated lists of folders by type for error fallback
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
tidy_folders = [folder for folder in folders if folder.folder_type == 'tidy']
|
||||
destination_folders = [folder for folder in folders if folder.folder_type == 'destination']
|
||||
|
||||
# Return both sections as fallback
|
||||
tidy_section = render_template('partials/folders_to_tidy_section.html', tidy_folders=tidy_folders)
|
||||
destination_section = render_template('partials/destination_folders_section.html', destination_folders=destination_folders)
|
||||
|
||||
response = make_response(render_template('partials/folder_modal.html', errors=errors, name=name, rule_text=rule_text, priority=priority))
|
||||
response.headers['HX-Retarget'] = '#folder-modal'
|
||||
response.headers['HX-Reswap'] = 'outerHTML'
|
||||
@@ -98,6 +127,9 @@ def delete_folder(folder_id):
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
|
||||
# Delete all associated processed emails first
|
||||
ProcessedEmail.query.filter_by(folder_id=folder.id).delete()
|
||||
|
||||
# Delete the folder
|
||||
db.session.delete(folder)
|
||||
db.session.commit()
|
||||
@@ -105,8 +137,15 @@ def delete_folder(folder_id):
|
||||
# Get updated list of folders for the current user
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
|
||||
# Return the updated folders list HTML
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
# Get updated lists of folders by type
|
||||
tidy_folders = [folder for folder in folders if folder.folder_type == 'tidy']
|
||||
destination_folders = [folder for folder in folders if folder.folder_type == 'destination']
|
||||
|
||||
# Return both sections
|
||||
tidy_section = render_template('partials/folders_to_tidy_section.html', tidy_folders=tidy_folders)
|
||||
destination_section = render_template('partials/destination_folders_section.html', destination_folders=destination_folders)
|
||||
|
||||
return tidy_section + destination_section
|
||||
|
||||
except Exception as e:
|
||||
# Print unhandled exceptions to the console as required
|
||||
@@ -114,7 +153,14 @@ def delete_folder(folder_id):
|
||||
db.session.rollback()
|
||||
# Return the folders list unchanged
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
tidy_folders = [folder for folder in folders if folder.folder_type == 'tidy']
|
||||
destination_folders = [folder for folder in folders if folder.folder_type == 'destination']
|
||||
|
||||
# Return both sections
|
||||
tidy_section = render_template('partials/folders_to_tidy_section.html', tidy_folders=tidy_folders)
|
||||
destination_section = render_template('partials/destination_folders_section.html', destination_folders=destination_folders)
|
||||
|
||||
return tidy_section + destination_section
|
||||
|
||||
@main.route('/api/folders/<folder_id>/toggle', methods=['PUT'])
|
||||
@login_required
|
||||
@@ -132,7 +178,11 @@ def toggle_folder_organize(folder_id):
|
||||
db.session.commit()
|
||||
|
||||
# Return just the updated folder card HTML for this specific folder
|
||||
return render_template('partials/folder_card.html', folder=folder)
|
||||
# Use the appropriate template based on folder type
|
||||
if folder.folder_type == 'tidy':
|
||||
return render_template('partials/folder_card_tidy.html', folder=folder)
|
||||
else:
|
||||
return render_template('partials/folder_card_destination.html', folder=folder)
|
||||
|
||||
except Exception as e:
|
||||
# Print unhandled exceptions to the console as required
|
||||
@@ -195,6 +245,15 @@ def update_folder(folder_id):
|
||||
|
||||
# If there are validation errors, return the modal with errors
|
||||
if errors:
|
||||
# Get updated lists of folders by type for error fallback
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
tidy_folders = [folder for folder in folders if folder.folder_type == 'tidy']
|
||||
destination_folders = [folder for folder in folders if folder.folder_type == 'destination']
|
||||
|
||||
# Return both sections as fallback
|
||||
tidy_section = render_template('partials/folders_to_tidy_section.html', tidy_folders=tidy_folders)
|
||||
destination_section = render_template('partials/destination_folders_section.html', destination_folders=destination_folders)
|
||||
|
||||
response = make_response(render_template('partials/folder_modal.html', folder=folder, errors=errors, name=name, rule_text=rule_text, priority=priority))
|
||||
response.headers['HX-Retarget'] = '#folder-modal'
|
||||
response.headers['HX-Reswap'] = 'outerHTML'
|
||||
@@ -205,12 +264,26 @@ def update_folder(folder_id):
|
||||
folder.rule_text = rule_text.strip()
|
||||
folder.priority = int(priority) if priority else 0
|
||||
|
||||
# Update folder type if the name changed to/from 'inbox'
|
||||
if name.strip().lower() == 'inbox' and folder.folder_type != 'tidy':
|
||||
folder.folder_type = 'tidy'
|
||||
elif name.strip().lower() != 'inbox' and folder.folder_type != 'destination':
|
||||
folder.folder_type = 'destination'
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Get updated list of folders for the current user
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
|
||||
response = make_response(render_template('partials/folders_list.html', folders=folders))
|
||||
# Get updated lists of folders by type
|
||||
tidy_folders = [folder for folder in folders if folder.folder_type == 'tidy']
|
||||
destination_folders = [folder for folder in folders if folder.folder_type == 'destination']
|
||||
|
||||
# Return both sections
|
||||
tidy_section = render_template('partials/folders_to_tidy_section.html', tidy_folders=tidy_folders)
|
||||
destination_section = render_template('partials/destination_folders_section.html', destination_folders=destination_folders)
|
||||
|
||||
response = make_response(tidy_section + destination_section)
|
||||
response.headers['HX-Trigger'] = 'close-modal'
|
||||
return response
|
||||
|
||||
@@ -317,25 +390,104 @@ def test_imap_connection():
|
||||
@main.route('/api/imap/sync', methods=['POST'])
|
||||
@login_required
|
||||
def sync_imap_folders():
|
||||
"""Sync folders from IMAP server."""
|
||||
"""Sync folders from IMAP server with processed email tracking."""
|
||||
try:
|
||||
if not current_user.imap_config:
|
||||
return jsonify({'error': 'No IMAP configuration found. Please configure IMAP first.'}), 400
|
||||
|
||||
# Test connection first
|
||||
imap_service = IMAPService(current_user)
|
||||
success, message = imap_service.sync_folders()
|
||||
|
||||
if success:
|
||||
# Get updated list of folders
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
else:
|
||||
return jsonify({'error': message}), 400
|
||||
# Get folders from IMAP server
|
||||
imap_folders = imap_service.get_folders()
|
||||
|
||||
if not imap_folders:
|
||||
return jsonify({'error': 'No folders found on IMAP server'}), 400
|
||||
|
||||
# Deduplicate folders by name to prevent creating multiple entries for the same folder
|
||||
unique_folders = []
|
||||
seen_names = set()
|
||||
for imap_folder in imap_folders:
|
||||
folder_name = imap_folder['name']
|
||||
|
||||
# Skip special folders that might not be needed
|
||||
if folder_name.lower() in ['sent', 'drafts', 'spam', 'trash']:
|
||||
continue
|
||||
|
||||
# Use case-insensitive comparison for deduplication
|
||||
folder_name_lower = folder_name.lower()
|
||||
if folder_name_lower not in seen_names:
|
||||
unique_folders.append(imap_folder)
|
||||
seen_names.add(folder_name_lower)
|
||||
|
||||
# Process each unique folder
|
||||
synced_count = 0
|
||||
print("HELLOOO")
|
||||
processed_emails_service = ProcessedEmailsService(current_user)
|
||||
|
||||
for imap_folder in unique_folders:
|
||||
folder_name = imap_folder['name'].strip()
|
||||
|
||||
# Handle nested folder names (convert slashes to underscores or keep as-is)
|
||||
# According to requirements, nested folders should be created with slashes in the name
|
||||
display_name = folder_name
|
||||
|
||||
# Check if folder already exists
|
||||
existing_folder = Folder.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
name=display_name
|
||||
).first()
|
||||
|
||||
if not existing_folder:
|
||||
# Create new folder
|
||||
# Determine folder type - inbox should be 'tidy', others 'destination'
|
||||
folder_type = 'tidy' if folder_name.lower().strip() == 'inbox' else 'destination'
|
||||
|
||||
new_folder = Folder(
|
||||
user_id=current_user.id,
|
||||
name=display_name,
|
||||
rule_text=f"Auto-synced from IMAP folder: {folder_name}",
|
||||
priority=0, # Default priority
|
||||
folder_type=folder_type
|
||||
)
|
||||
db.session.add(new_folder)
|
||||
synced_count += 1
|
||||
else:
|
||||
# Update existing folder with email counts and recent emails
|
||||
# Get all email UIDs in this folder
|
||||
email_uids = imap_service.get_folder_email_uids(folder_name)
|
||||
|
||||
# Sync with processed emails service
|
||||
new_emails_count = processed_emails_service.sync_folder_emails(display_name, email_uids)
|
||||
print("NEW", new_emails_count)
|
||||
|
||||
# Update counts
|
||||
pending_count = processed_emails_service.get_pending_count(display_name)
|
||||
existing_folder.pending_count = pending_count
|
||||
existing_folder.total_count = len(email_uids)
|
||||
|
||||
# Get the most recent emails for this folder
|
||||
recent_emails = imap_service.get_recent_emails(folder_name, 3)
|
||||
existing_folder.recent_emails = recent_emails
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Get updated list of folders
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
tidy_folders = [folder for folder in folders if folder.folder_type == 'tidy']
|
||||
destination_folders = [folder for folder in folders if folder.folder_type == 'destination']
|
||||
|
||||
# Return both sections
|
||||
tidy_section = render_template('partials/folders_to_tidy_section.html', tidy_folders=tidy_folders)
|
||||
destination_section = render_template('partials/destination_folders_section.html', destination_folders=destination_folders)
|
||||
|
||||
return tidy_section + destination_section
|
||||
|
||||
except Exception as e:
|
||||
logging.exception("Error syncing IMAP folders: %s", e)
|
||||
print(e)
|
||||
db.session.rollback()
|
||||
return jsonify({'error': 'An unexpected error occurred'}), 500
|
||||
|
||||
@main.route('/api/folders', methods=['GET'])
|
||||
@login_required
|
||||
@@ -343,9 +495,13 @@ def get_folders():
|
||||
"""Get folders with optional filtering."""
|
||||
# Get filter parameter from query string
|
||||
filter_type = request.args.get('filter', 'all')
|
||||
folder_type = request.args.get('type', None)
|
||||
|
||||
# Get folders for the current authenticated user
|
||||
if filter_type == 'high':
|
||||
if folder_type:
|
||||
# Filter by folder type
|
||||
folders = Folder.query.filter_by(user_id=current_user.id, folder_type=folder_type).all()
|
||||
elif filter_type == 'high':
|
||||
# Get high priority folders (priority = 1)
|
||||
folders = Folder.query.filter_by(user_id=current_user.id, priority=1).all()
|
||||
elif filter_type == 'normal':
|
||||
@@ -355,4 +511,205 @@ def get_folders():
|
||||
# Get all folders
|
||||
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
||||
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
# Check if we need to return a specific section
|
||||
if folder_type == 'tidy':
|
||||
return render_template('partials/folders_to_tidy_section.html', tidy_folders=folders)
|
||||
elif folder_type == 'destination':
|
||||
return render_template('partials/destination_folders_section.html', destination_folders=folders)
|
||||
else:
|
||||
return render_template('partials/folders_list.html', folders=folders)
|
||||
|
||||
|
||||
# Processed Emails API Endpoints
|
||||
|
||||
@main.route('/api/folders/<int:folder_id>/pending-emails', methods=['GET'])
|
||||
@login_required
|
||||
def get_pending_emails(folder_id):
|
||||
"""Get pending emails for a folder with email metadata."""
|
||||
try:
|
||||
# Find the folder and ensure it belongs to the current user
|
||||
folder = Folder.query.filter_by(id=folder_id, user_id=current_user.id).first()
|
||||
|
||||
if not folder:
|
||||
return jsonify({'error': 'Folder not found'}), 404
|
||||
|
||||
# Get IMAP service to fetch email data
|
||||
imap_service = IMAPService(current_user)
|
||||
|
||||
# Get all email UIDs in the folder
|
||||
all_uids = imap_service.get_folder_email_uids(folder.name)
|
||||
|
||||
# Get processed emails service
|
||||
processed_emails_service = ProcessedEmailsService(current_user)
|
||||
|
||||
# Get pending email UIDs
|
||||
pending_uids = processed_emails_service.get_pending_emails(folder.name)
|
||||
|
||||
# Get email headers for pending emails
|
||||
pending_emails = []
|
||||
for uid in pending_uids:
|
||||
headers = imap_service.get_email_headers(folder.name, uid)
|
||||
if headers:
|
||||
# Add UID to the response
|
||||
email_data = {
|
||||
'uid': uid,
|
||||
'subject': headers.get('subject', 'No Subject'),
|
||||
'date': headers.get('date', ''),
|
||||
'from': headers.get('from', ''),
|
||||
'to': headers.get('to', ''),
|
||||
'message_id': headers.get('message_id', '')
|
||||
}
|
||||
pending_emails.append(email_data)
|
||||
|
||||
# Return the pending emails in a dialog format
|
||||
response = make_response(render_template('partials/pending_emails_dialog.html',
|
||||
folder=folder,
|
||||
pending_emails=pending_emails))
|
||||
response.headers['HX-Trigger'] = 'open-modal'
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logging.exception("Error getting pending emails: %s", e)
|
||||
return jsonify({'error': 'An unexpected error occurred'}), 500
|
||||
|
||||
@main.route('/api/folders/<int:folder_id>/emails/<email_uid>/process', methods=['POST'])
|
||||
@login_required
|
||||
def mark_email_processed(folder_id, email_uid):
|
||||
"""Mark a specific email as processed."""
|
||||
try:
|
||||
# Find the folder and ensure it belongs to the current user
|
||||
folder = Folder.query.filter_by(id=folder_id, user_id=current_user.id).first()
|
||||
|
||||
if not folder:
|
||||
return jsonify({'error': 'Folder not found'}), 404
|
||||
|
||||
# Get processed emails service
|
||||
processed_emails_service = ProcessedEmailsService(current_user)
|
||||
|
||||
# Mark email as processed
|
||||
success = processed_emails_service.mark_email_processed(folder.name, email_uid)
|
||||
|
||||
if success:
|
||||
# Get updated counts
|
||||
pending_count = processed_emails_service.get_pending_count(folder.name)
|
||||
|
||||
# Update the folder's count based on folder type
|
||||
if folder.folder_type == 'tidy':
|
||||
folder.pending_count = pending_count
|
||||
elif folder.folder_type == 'destination':
|
||||
# For destination folders, update emails_count
|
||||
folder.emails_count = pending_count
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Return updated dialog body
|
||||
response = make_response(render_template('partials/pending_emails_updated.html',
|
||||
folder=folder,
|
||||
uid=email_uid))
|
||||
response.headers['HX-Trigger'] = 'close-modal'
|
||||
return response
|
||||
else:
|
||||
return jsonify({'error': 'Failed to mark email as processed'}), 500
|
||||
|
||||
except Exception as e:
|
||||
logging.exception("Error marking email as processed: %s", e)
|
||||
return jsonify({'error': 'An unexpected error occurred'}), 500
|
||||
|
||||
@main.route('/api/folders/<int:folder_id>/sync-emails', methods=['POST'])
|
||||
@login_required
|
||||
def sync_folder_emails(folder_id):
|
||||
"""Sync emails for a specific folder with processed email tracking."""
|
||||
try:
|
||||
# Find the folder and ensure it belongs to the current user
|
||||
folder = Folder.query.filter_by(id=folder_id, user_id=current_user.id).first()
|
||||
|
||||
if not folder:
|
||||
return jsonify({'error': 'Folder not found'}), 404
|
||||
|
||||
# Get IMAP service to fetch email UIDs
|
||||
imap_service = IMAPService(current_user)
|
||||
|
||||
# Get all email UIDs in the folder
|
||||
email_uids = imap_service.get_folder_email_uids(folder.name)
|
||||
|
||||
if not email_uids:
|
||||
return jsonify({'error': 'No emails found in folder'}), 404
|
||||
|
||||
# Get processed emails service
|
||||
processed_emails_service = ProcessedEmailsService(current_user)
|
||||
|
||||
# Sync emails with the processed emails service
|
||||
new_emails_count = processed_emails_service.sync_folder_emails(folder.name, email_uids)
|
||||
|
||||
# Get updated counts
|
||||
pending_count = processed_emails_service.get_pending_count(folder.name)
|
||||
|
||||
# Update the folder's counts based on folder type
|
||||
if folder.folder_type == 'tidy':
|
||||
folder.pending_count = pending_count
|
||||
folder.total_count = len(email_uids)
|
||||
elif folder.folder_type == 'destination':
|
||||
# For destination folders, update emails_count
|
||||
folder.emails_count = pending_count # Using pending_count as emails_count for destination folders
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Return success response with updated counts
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Synced {new_emails_count} new emails',
|
||||
'pending_count': pending_count,
|
||||
'total_count': len(email_uids)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.exception("Error syncing folder emails: %s", e)
|
||||
return jsonify({'error': 'An unexpected error occurred'}), 500
|
||||
|
||||
@main.route('/api/folders/<int:folder_id>/process-emails', methods=['POST'])
|
||||
@login_required
|
||||
def process_folder_emails(folder_id):
|
||||
"""Process multiple emails in a folder (mark as processed)."""
|
||||
try:
|
||||
# Find the folder and ensure it belongs to the current user
|
||||
folder = Folder.query.filter_by(id=folder_id, user_id=current_user.id).first()
|
||||
|
||||
if not folder:
|
||||
return jsonify({'error': 'Folder not found'}), 404
|
||||
|
||||
# Get email UIDs from form data
|
||||
email_uids = request.form.getlist('email_uids')
|
||||
|
||||
if not email_uids:
|
||||
return jsonify({'error': 'No email UIDs provided'}), 400
|
||||
|
||||
# Get processed emails service
|
||||
processed_emails_service = ProcessedEmailsService(current_user)
|
||||
|
||||
# Mark emails as processed
|
||||
processed_count = processed_emails_service.mark_emails_processed(folder.name, email_uids)
|
||||
|
||||
if processed_count > 0:
|
||||
# Get updated counts
|
||||
pending_count = processed_emails_service.get_pending_count(folder.name)
|
||||
|
||||
# Update the folder's count based on folder type
|
||||
if folder.folder_type == 'tidy':
|
||||
folder.pending_count = pending_count
|
||||
elif folder.folder_type == 'destination':
|
||||
# For destination folders, update emails_count
|
||||
folder.emails_count = pending_count
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Processed {processed_count} emails',
|
||||
'pending_count': pending_count
|
||||
})
|
||||
else:
|
||||
return jsonify({'error': 'No emails were processed'}), 400
|
||||
|
||||
except Exception as e:
|
||||
logging.exception("Error processing folder emails: %s", e)
|
||||
return jsonify({'error': 'An unexpected error occurred'}), 500
|
||||
|
||||
@@ -74,18 +74,24 @@
|
||||
</div>
|
||||
|
||||
<!-- Stats Section -->
|
||||
<div class="mb-8 grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="mb-8 grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="card bg-base-100 shadow-md border border-base-300 p-4">
|
||||
<div class="text-2xl font-bold text-primary">{{ folders|length }}</div>
|
||||
<div class="text-sm text-base-content/70">Total Folders</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow-md border border-base-300 p-4">
|
||||
<div class="text-2xl font-bold text-secondary">0</div>
|
||||
<div class="text-sm text-base-content/70">Emails Processed</div>
|
||||
<div class="text-2xl font-bold text-warning">{{ tidy_folders|length }}</div>
|
||||
<div class="text-sm text-base-content/70">Folders to Tidy</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow-md border border-base-300 p-4">
|
||||
<div class="text-2xl font-bold text-info">0</div>
|
||||
<div class="text-sm text-base-content/70">Active Rules</div>
|
||||
<div class="text-2xl font-bold text-secondary">{{ destination_folders|length }}</div>
|
||||
<div class="text-sm text-base-content/70">Destination Folders</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow-md border border-base-300 p-4">
|
||||
<div class="text-2xl font-bold text-info">
|
||||
{{ tidy_folders|sum(attribute='pending_count') }}
|
||||
</div>
|
||||
<div class="text-sm text-base-content/70">Pending Emails</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -103,7 +109,8 @@
|
||||
</div>
|
||||
|
||||
<section id="folders-list" class="mb-12">
|
||||
{% include 'partials/folders_list.html' %}
|
||||
{% include 'partials/folders_to_tidy_section.html' %}
|
||||
{% include 'partials/destination_folders_section.html' %}
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
35
app/templates/partials/destination_folders_section.html
Normal file
35
app/templates/partials/destination_folders_section.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<div class="destination-folders-section mb-8">
|
||||
<div class="section-header bg-base-200 p-4 rounded-t-lg border-b border-base-300">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-base-content">Destination Folders</h2>
|
||||
<p class="text-base-content/70 mt-1">Folders where emails are organized and stored</p>
|
||||
</div>
|
||||
<button class="btn btn-primary mt-4 md:mt-0" hx-get="/api/folders/new" hx-target="#modal-holder" hx-swap="innerHTML" hx-trigger="click">
|
||||
<i class="fas fa-plus mr-2"></i>Add Destination Folder
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="destination-folders-list" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 p-6">
|
||||
{% for folder in destination_folders %}
|
||||
{% include 'partials/folder_card_destination.html' %}
|
||||
{% else %}
|
||||
<div class="col-span-full text-center py-12 bg-base-100 rounded-box shadow-lg border border-dashed border-base-300">
|
||||
<div class="text-5xl mb-4 text-primary">
|
||||
<i class="fas fa-folder-plus"></i>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold mb-2">No destination folders yet</h3>
|
||||
<p class="mb-6 text-base-content/70">Create destination folders to organize your emails into categories.</p>
|
||||
<div data-loading-states>
|
||||
<button class="btn btn-primary btn-lg" hx-get="/api/folders/new" hx-target="#modal-holder" hx-swap="innerHTML"
|
||||
hx-trigger="click">
|
||||
<i class="fas fa-plus mr-2" data-loading-class="!hidden"></i>
|
||||
<span data-loading-class="!hidden">Create Folder</span>
|
||||
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,7 +31,20 @@
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="flex space-x-1">
|
||||
<span class="badge badge-outline cursor-pointer">{{ folder.total_count }} emails</span>
|
||||
{% if folder.pending_count > 0 %}
|
||||
<button
|
||||
class="badge badge-warning cursor-pointer"
|
||||
hx-get="/api/folders/{{ folder.id }}/pending-emails"
|
||||
hx-target="#modal-holder"
|
||||
hx-swap="innerHTML"
|
||||
hx-trigger="click"
|
||||
title="{{ folder.pending_count }} pending emails"
|
||||
>
|
||||
{{ folder.pending_count }} pending
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="badge badge-secondary cursor-pointer" x-tooltip.raw.html="{% 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>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if folder.priority == 1 %}
|
||||
<span class="badge badge-error">High Priority</span>
|
||||
|
||||
41
app/templates/partials/folder_card_destination.html
Normal file
41
app/templates/partials/folder_card_destination.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<div id="folder-{{ folder.id }}" class="card bg-base-100 shadow-xl border border-base-300 hover:shadow-lg">
|
||||
|
||||
<div class="card-body" data-loading-states>
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<h3 class="text-xl font-bold truncate flex-grow">{{ folder.name }}</h3>
|
||||
<div class="flex space-x-2">
|
||||
<button class="btn btn-sm btn-outline"
|
||||
hx-get="/api/folders/{{ folder.id }}/edit"
|
||||
hx-target="#modal-holder"
|
||||
hx-swap="innerHTML"
|
||||
hx-trigger="click"
|
||||
data-loading-disable
|
||||
>
|
||||
<i class="fas fa-edit" data-loading-class="!hidden"></i>
|
||||
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline btn-error fade-me-out"
|
||||
hx-delete="/api/folders/{{ folder.id }}"
|
||||
hx-target="#destination-folders-list"
|
||||
hx-swap="innerHTML swap:1s"
|
||||
hx-confirm="Are you sure you want to delete this folder?"
|
||||
data-loading-disable
|
||||
>
|
||||
<i class="fas fa-trash" data-loading-class="!hidden"></i>
|
||||
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email count badge for destination folders -->
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="flex space-x-1">
|
||||
<span class="badge badge-primary">{{ folder.emails_count }} emails</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-200 rounded-box p-4 mb-4">
|
||||
<p class="text-base-content/80">{{ folder.rule_text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
82
app/templates/partials/folder_card_tidy.html
Normal file
82
app/templates/partials/folder_card_tidy.html
Normal file
@@ -0,0 +1,82 @@
|
||||
<div id="folder-{{ folder.id }}" class="card bg-base-100 shadow-xl border border-base-300 hover:shadow-lg">
|
||||
|
||||
<div class="card-body" data-loading-states>
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<h3 class="text-xl font-bold truncate flex-grow">{{ folder.name }}</h3>
|
||||
<div class="flex space-x-2">
|
||||
<button class="btn btn-sm btn-outline"
|
||||
hx-get="/api/folders/{{ folder.id }}/edit"
|
||||
hx-target="#modal-holder"
|
||||
hx-swap="innerHTML"
|
||||
hx-trigger="click"
|
||||
data-loading-disable
|
||||
>
|
||||
<i class="fas fa-edit" data-loading-class="!hidden"></i>
|
||||
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline btn-error fade-me-out"
|
||||
hx-delete="/api/folders/{{ folder.id }}"
|
||||
hx-target="#folders-to-tidy-list"
|
||||
hx-swap="innerHTML swap:1s"
|
||||
hx-confirm="Are you sure you want to delete this folder?"
|
||||
data-loading-disable
|
||||
>
|
||||
<i class="fas fa-trash" data-loading-class="!hidden"></i>
|
||||
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email count badges placed below title but in a separate row -->
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="flex space-x-1">
|
||||
<span class="badge badge-outline">{{ folder.total_count }} total</span>
|
||||
{% if folder.pending_count > 0 %}
|
||||
<button
|
||||
class="badge badge-warning cursor-pointer"
|
||||
hx-get="/api/folders/{{ folder.id }}/pending-emails"
|
||||
hx-target="#modal-holder"
|
||||
hx-swap="innerHTML"
|
||||
hx-trigger="click"
|
||||
title="{{ folder.pending_count }} pending emails"
|
||||
>
|
||||
{{ folder.pending_count }} pending
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="badge badge-secondary cursor-pointer" x-tooltip.raw.html="{% 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>
|
||||
{% endif %}
|
||||
<span class="badge badge-success">{{ folder.total_count - folder.pending_count }} processed</span>
|
||||
</div>
|
||||
{% if folder.priority == 1 %}
|
||||
<span class="badge badge-error">High Priority</span>
|
||||
{% elif folder.priority == -1 %}
|
||||
<span class="badge badge-info">Low Priority</span>
|
||||
{% else %}
|
||||
<span class="badge badge-primary">Normal Priority</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="bg-base-200 rounded-box p-4 mb-4">
|
||||
<p class="text-base-content/80">{{ folder.rule_text }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mt-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xs">Organize:</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-sm toggle-success"
|
||||
{% if folder.organize_enabled %}checked="checked"{% endif %}
|
||||
hx-put="/api/folders/{{ folder.id }}/toggle"
|
||||
hx-target="#folder-{{ folder.id }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="click"
|
||||
data-loading-disable
|
||||
aria-label="Toggle organize enabled">
|
||||
</input>
|
||||
|
||||
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
35
app/templates/partials/folders_to_tidy_section.html
Normal file
35
app/templates/partials/folders_to_tidy_section.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<div class="folders-to-tidy-section mb-8">
|
||||
<div class="section-header bg-base-200 p-4 rounded-t-lg border-b border-base-300">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-base-content">Folders to Tidy</h2>
|
||||
<p class="text-base-content/70 mt-1">Folders containing emails that need to be processed</p>
|
||||
</div>
|
||||
<button class="btn btn-primary mt-4 md:mt-0" hx-get="/api/folders/new" hx-target="#modal-holder" hx-swap="innerHTML" hx-trigger="click">
|
||||
<i class="fas fa-plus mr-2"></i>Add Tidy Folder
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="folders-to-tidy-list" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 p-6">
|
||||
{% for folder in tidy_folders %}
|
||||
{% include 'partials/folder_card_tidy.html' %}
|
||||
{% else %}
|
||||
<div class="col-span-full text-center py-12 bg-base-100 rounded-box shadow-lg border border-dashed border-base-300">
|
||||
<div class="text-5xl mb-4 text-warning">
|
||||
<i class="fas fa-inbox"></i>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold mb-2">No folders to tidy yet</h3>
|
||||
<p class="mb-6 text-base-content/70">Add your first folder to start organizing your emails.</p>
|
||||
<div data-loading-states>
|
||||
<button class="btn btn-primary btn-lg" hx-get="/api/folders/new" hx-target="#modal-holder" hx-swap="innerHTML"
|
||||
hx-trigger="click">
|
||||
<i class="fas fa-plus mr-2" data-loading-class="!hidden"></i>
|
||||
<span data-loading-class="!hidden">Create Folder</span>
|
||||
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
123
app/templates/partials/pending_emails_dialog.html
Normal file
123
app/templates/partials/pending_emails_dialog.html
Normal file
@@ -0,0 +1,123 @@
|
||||
<div id="pending-emails-modal" @click.away="$refs.modal.close()" class="modal-box max-w-6xl" x-data="{
|
||||
errors: {{ 'true' if errors else 'false' }},
|
||||
selectedEmails: []
|
||||
}" x-init="$nextTick(() => { if (errors) { document.querySelector('#submit-btn').classList.add('shake'); } })">
|
||||
<h3 class="font-bold text-lg mb-4">Pending Emails in {{ folder.name }}</h3>
|
||||
|
||||
{% if errors and errors.general %}
|
||||
<div class="alert alert-error mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
<span>{{ errors.general }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Total Emails</div>
|
||||
<div class="stat-value">{{ folder.total_count }}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Pending</div>
|
||||
<div class="stat-value text-warning">{{ pending_emails|length }}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Processed</div>
|
||||
<div class="stat-value text-success">{{ folder.total_count - pending_emails|length }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if pending_emails %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<label class="cursor-pointer">
|
||||
<input type="checkbox" class="checkbox checkbox-xs" @change="selectAllEmails($event)">
|
||||
</label>
|
||||
</th>
|
||||
<th>Subject</th>
|
||||
<th>From</th>
|
||||
<th>Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for email in pending_emails %}
|
||||
<tr id="email-row-{{ email.uid }}">
|
||||
<td>
|
||||
<label class="cursor-pointer">
|
||||
<input type="checkbox" name="email_uids" value="{{ email.uid }}" class="checkbox checkbox-xs">
|
||||
</label>
|
||||
</td>
|
||||
<td class="font-medium">{{ email.subject }}</td>
|
||||
<td>{{ email.from }}</td>
|
||||
<td>{{ email.date }}</td>
|
||||
<td>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
class="btn btn-xs btn-primary"
|
||||
hx-post="/api/folders/{{ folder.id }}/emails/{{ email.uid }}/process"
|
||||
hx-target="#email-row-{{ email.uid }}"
|
||||
hx-swap="outerHTML"
|
||||
data-loading-disable
|
||||
>
|
||||
<i class="fas fa-check mr-1"></i>
|
||||
Process
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs btn-secondary"
|
||||
onclick="previewEmail('{{ email.uid }}', '{{ folder.name }}')"
|
||||
>
|
||||
<i class="fas fa-eye mr-1"></i>
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
hx-post="/api/folders/{{ folder.id }}/process-emails"
|
||||
hx-include="[name='email_uids']"
|
||||
hx-target="#modal-holder"
|
||||
hx-swap="beforeend"
|
||||
data-loading-disable
|
||||
>
|
||||
<span data-loading-class="!hidden"><i class="fas fa-check-double mr-2"></i>Process Selected</span>
|
||||
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
|
||||
</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
<span>No pending emails found in this folder.</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="modal-action">
|
||||
<button class="btn btn-outline" @click="$dispatch('close-modal')">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function selectAllEmails(event) {
|
||||
const checkboxes = document.querySelectorAll('input[name="email_uids"]');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = event.target.checked;
|
||||
});
|
||||
}
|
||||
|
||||
function previewEmail(uid, folderName) {
|
||||
// This would typically open a modal or new window with email preview
|
||||
// For now, we'll just show an alert
|
||||
alert(`Preview functionality would open email ${uid} from folder ${folderName}`);
|
||||
}
|
||||
</script>
|
||||
33
app/templates/partials/pending_emails_updated.html
Normal file
33
app/templates/partials/pending_emails_updated.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<div id="pending-emails-updated" class="alert alert-success mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
<span>Email {{ uid }} has been marked as processed successfully!</span>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Total Emails</div>
|
||||
<div class="stat-value">{{ folder.total_count }}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Pending</div>
|
||||
<div class="stat-value text-warning">{{ folder.pending_count }}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Processed</div>
|
||||
<div class="stat-value text-success">{{ folder.total_count - folder.pending_count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
hx-get="/api/folders/{{ folder.id }}/pending-emails"
|
||||
hx-target="#modal-holder"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<i class="fas fa-list mr-2"></i>
|
||||
View All Pending Emails
|
||||
</button>
|
||||
</div>
|
||||
@@ -26,8 +26,10 @@ erDiagram
|
||||
text rule_text "Natural Language Rule"
|
||||
int priority "Processing Order"
|
||||
boolean organize_enabled "Default: True"
|
||||
string folder_type "Default: 'destination'"
|
||||
int total_count "Default: 0"
|
||||
int pending_count "Default: 0"
|
||||
int emails_count "Default: 0"
|
||||
json recent_emails "JSON Array"
|
||||
datetime created_at "Default: UTC Now"
|
||||
datetime updated_at "Default: UTC Now, On Update"
|
||||
@@ -83,8 +85,10 @@ The `Folder` entity stores email organization rules and metadata for each user's
|
||||
| rule_text | Text | Nullable | Natural language description of the folder rule |
|
||||
| priority | Integer | Nullable | Processing order (0=normal, 1=high) |
|
||||
| organize_enabled | Boolean | Default: True | Whether the organization rule is active |
|
||||
| folder_type | String(20) | Default: 'destination' | Folder type: 'tidy' or 'destination' |
|
||||
| total_count | Integer | Default: 0 | Total number of emails in the folder |
|
||||
| pending_count | Integer | Default: 0 | Number of emails waiting to be processed |
|
||||
| emails_count | Integer | Default: 0 | Number of emails moved to this destination folder |
|
||||
| recent_emails | JSON | Default: [] | Array of recent email metadata |
|
||||
| created_at | DateTime | Default: datetime.utcnow | Timestamp of folder creation |
|
||||
| updated_at | DateTime | Default: datetime.utcnow, On Update | Timestamp of last update |
|
||||
@@ -100,6 +104,9 @@ The `Folder` entity stores email organization rules and metadata for each user's
|
||||
- Folder name must be unique per user
|
||||
- Rule text can be null (for manually created folders)
|
||||
- Priority values: 0 (normal), 1 (high priority)
|
||||
- Folder types:
|
||||
- 'tidy': Folders containing emails to be processed (e.g., Inbox)
|
||||
- 'destination': Folders that are targets for email organization (default)
|
||||
- Recent emails array stores JSON objects with subject and date information
|
||||
|
||||
## Data Constraints
|
||||
@@ -128,7 +135,8 @@ The `Folder` entity stores email organization rules and metadata for each user's
|
||||
- `User.created_at`, `User.updated_at`: Current UTC timestamp
|
||||
- `Folder.created_at`, `Folder.updated_at`: Current UTC timestamp
|
||||
- `Folder.organize_enabled`: True
|
||||
- `Folder.total_count`, `Folder.pending_count`: 0
|
||||
- `Folder.folder_type`: 'destination'
|
||||
- `Folder.total_count`, `Folder.pending_count`, `Folder.emails_count`: 0
|
||||
- `Folder.recent_emails`: Empty array
|
||||
|
||||
## JSON Data Structures
|
||||
@@ -219,6 +227,49 @@ The `recent_emails` field stores an array of JSON objects:
|
||||
- Batch updates for email counts
|
||||
- JSON operations for recent emails metadata
|
||||
|
||||
## Folder Types
|
||||
|
||||
The system supports two distinct types of folders, each with different purposes and behaviors:
|
||||
|
||||
### Tidy Folders
|
||||
|
||||
Folders with `folder_type = 'tidy'` are source folders that contain emails waiting to be processed and organized.
|
||||
|
||||
**Characteristics:**
|
||||
- Display pending and processed email counts
|
||||
- Can have organization rules enabled/disabled
|
||||
- Support viewing pending emails
|
||||
- Example: Inbox folder
|
||||
|
||||
**UI Representation:**
|
||||
- Shows "pending count" and "processed count" badges
|
||||
- Includes "View Pending" button if there are pending emails
|
||||
- May include priority indicators
|
||||
|
||||
### Destination Folders
|
||||
|
||||
Folders with `folder_type = 'destination'` are target folders where emails are moved from other folders during organization.
|
||||
|
||||
**Characteristics:**
|
||||
- Display count of emails moved to this folder
|
||||
- Typically don't have organization rules (or they're ignored)
|
||||
- Focus on showing how many emails have been organized into them
|
||||
- Example: "Projects", "Finance", "Personal" folders
|
||||
|
||||
**UI Representation:**
|
||||
- Shows "emails count" badge
|
||||
- Simpler interface without pending/processed indicators
|
||||
- Focus on folder management and viewing contents
|
||||
|
||||
### Folder Type Determination
|
||||
|
||||
Folder types are determined as follows:
|
||||
- During IMAP synchronization:
|
||||
- Folders named "inbox" (case-insensitive) are automatically set as 'tidy'
|
||||
- All other folders are set as 'destination'
|
||||
- Manually created folders default to 'destination'
|
||||
- Folder type can be changed through administrative functions
|
||||
|
||||
## Future Data Model Considerations
|
||||
|
||||
### Potential Enhancements
|
||||
|
||||
228
docs/design/folder-types-ui.md
Normal file
228
docs/design/folder-types-ui.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# Folder Types UI Design
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the UI design changes needed to support the new folder types feature: "Folders to Tidy" and "Destination Folders". The UI will be reorganized to clearly separate these two types of folders into distinct sections.
|
||||
|
||||
## UI Structure
|
||||
|
||||
### Main Page Layout
|
||||
|
||||
The main page will be divided into two distinct sections:
|
||||
|
||||
1. **Folders to Tidy Section**
|
||||
- Contains folders with `folder_type = 'tidy'`
|
||||
- Typically includes the inbox and other source folders
|
||||
- Shows pending and processed email counts
|
||||
- Has actions for viewing and processing emails
|
||||
|
||||
2. **Destination Folders Section**
|
||||
- Contains folders with `folder_type = 'destination'`
|
||||
- Contains target folders for email organization
|
||||
- Shows count of emails moved to each folder
|
||||
- Focuses on folder management and viewing contents
|
||||
|
||||
### Visual Design
|
||||
|
||||
#### Section Headers
|
||||
Each section will have a clear header with:
|
||||
- Section title
|
||||
- Brief description of the section's purpose
|
||||
- Action button (e.g., "Add Folder" for destination folders)
|
||||
|
||||
#### Section Styling
|
||||
- Different background colors or borders to visually distinguish sections
|
||||
- Consistent spacing and layout within each section
|
||||
- Responsive grid layout that adapts to screen size
|
||||
|
||||
## Folder Card Components
|
||||
|
||||
### Tidy Folder Card
|
||||
|
||||
For folders with `folder_type = 'tidy'`, the card will display:
|
||||
|
||||
#### Header
|
||||
- Folder name (prominent)
|
||||
- Edit and delete buttons
|
||||
|
||||
#### Count Badges
|
||||
- Total emails count
|
||||
- Pending emails count (with warning styling if > 0)
|
||||
- Processed emails count (calculated as total - pending)
|
||||
|
||||
#### Actions
|
||||
- "View Pending" button (if pending count > 0)
|
||||
- Organize toggle switch
|
||||
- Priority badge
|
||||
|
||||
#### Content
|
||||
- Rule text description
|
||||
- Recent email previews (if available)
|
||||
|
||||
### Destination Folder Card
|
||||
|
||||
For folders with `folder_type = 'destination'`, the card will display:
|
||||
|
||||
#### Header
|
||||
- Folder name (prominent)
|
||||
- Edit and delete buttons
|
||||
|
||||
#### Count Badge
|
||||
- Emails count (number of emails moved to this folder)
|
||||
|
||||
#### Content
|
||||
- Rule text description (if applicable)
|
||||
|
||||
#### Actions
|
||||
- Basic folder management options
|
||||
- No organize toggle (as it's not applicable)
|
||||
|
||||
## UI Components
|
||||
|
||||
### Section Templates
|
||||
|
||||
#### Folders to Tidy Section
|
||||
```html
|
||||
<div class="folders-to-tidy-section">
|
||||
<div class="section-header">
|
||||
<h2>Folders to Tidy</h2>
|
||||
<p>Folders containing emails that need to be processed</p>
|
||||
<button class="btn btn-primary" hx-get="/api/folders/new">
|
||||
<i class="fas fa-plus"></i> Add Tidy Folder
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for folder in tidy_folders %}
|
||||
{% include 'partials/folder_card_tidy.html' %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Destination Folders Section
|
||||
```html
|
||||
<div class="destination-folders-section">
|
||||
<div class="section-header">
|
||||
<h2>Destination Folders</h2>
|
||||
<p>Folders where emails are organized and stored</p>
|
||||
<button class="btn btn-primary" hx-get="/api/folders/new">
|
||||
<i class="fas fa-plus"></i> Add Destination Folder
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for folder in destination_folders %}
|
||||
{% include 'partials/folder_card_destination.html' %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Folder Card Templates
|
||||
|
||||
#### Tidy Folder Card
|
||||
```html
|
||||
<div class="card bg-base-100 shadow-xl border border-base-300">
|
||||
<div class="card-body">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<h3 class="text-xl font-bold truncate">{{ folder.name }}</h3>
|
||||
<div class="flex space-x-2">
|
||||
<button class="btn btn-sm btn-outline" hx-get="/api/folders/{{ folder.id }}/edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline btn-error" hx-delete="/api/folders/{{ folder.id }}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-2 mb-2">
|
||||
<span class="badge badge-outline">{{ folder.total_count }} total</span>
|
||||
{% if folder.pending_count > 0 %}
|
||||
<button class="badge badge-warning" hx-get="/api/folders/{{ folder.id }}/pending-emails">
|
||||
{{ folder.pending_count }} pending
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="badge badge-secondary">{{ folder.pending_count }} pending</span>
|
||||
{% endif %}
|
||||
<span class="badge badge-success">{{ folder.total_count - folder.pending_count }} processed</span>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-200 rounded-box p-3 mb-3">
|
||||
<p class="text-sm">{{ folder.rule_text }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xs">Organize:</span>
|
||||
<input type="checkbox" class="toggle toggle-sm"
|
||||
{% if folder.organize_enabled %}checked{% endif %}
|
||||
hx-put="/api/folders/{{ folder.id }}/toggle">
|
||||
</div>
|
||||
{% if folder.priority == 1 %}
|
||||
<span class="badge badge-error text-xs">High Priority</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Destination Folder Card
|
||||
```html
|
||||
<div class="card bg-base-100 shadow-xl border border-base-300">
|
||||
<div class="card-body">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<h3 class="text-xl font-bold truncate">{{ folder.name }}</h3>
|
||||
<div class="flex space-x-2">
|
||||
<button class="btn btn-sm btn-outline" hx-get="/api/folders/{{ folder.id }}/edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline btn-error" hx-delete="/api/folders/{{ folder.id }}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-2 mb-2">
|
||||
<span class="badge badge-primary">{{ folder.emails_count }} emails</span>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-200 rounded-box p-3 mb-3">
|
||||
<p class="text-sm">{{ folder.rule_text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Styling Guidelines
|
||||
|
||||
### Color Scheme
|
||||
- **Tidy Folders**: Use warning colors (yellow/orange) for pending emails
|
||||
- **Destination Folders**: Use primary colors (blue) for email counts
|
||||
- Consistent use of DaisyUI badge classes for different states
|
||||
|
||||
### Responsive Design
|
||||
- Grid layout adjusts from 1 column on mobile to 3 columns on desktop
|
||||
- Cards should have consistent width across all screen sizes
|
||||
- Section headers should remain readable on mobile
|
||||
|
||||
### Interactive Elements
|
||||
- Buttons should have hover states
|
||||
- Toggle switches should be clearly labeled
|
||||
- Pending email count should be clickable to open dialog
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Template Organization
|
||||
- Create separate partial templates for each folder type
|
||||
- Main index template will include both sections
|
||||
- Use HTMX for dynamic updates without full page reloads
|
||||
|
||||
### Data Loading
|
||||
- Fetch folders filtered by type from the backend
|
||||
- Pass filtered lists to appropriate section templates
|
||||
- Maintain existing functionality for folder operations
|
||||
|
||||
### Accessibility
|
||||
- Ensure all interactive elements have proper ARIA labels
|
||||
- Use semantic HTML5 elements for section structure
|
||||
- Maintain keyboard navigation support
|
||||
261
docs/design/processed-emails-spec.md
Normal file
261
docs/design/processed-emails-spec.md
Normal file
@@ -0,0 +1,261 @@
|
||||
# Processed Emails Feature Specification
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the specification for implementing a feature to persistently track which emails have been processed by the Email Organizer system. The goal is to maintain a record of email processing status to avoid reprocessing the same emails during synchronization and provide accurate pending email counts.
|
||||
|
||||
## Requirements
|
||||
|
||||
### 1. Email Tracking Requirements
|
||||
|
||||
- **Unique Email Identification**: Track emails using a unique identifier (UID) provided by the IMAP server, along with the folder name and user ID
|
||||
- **Processing Status**: Mark emails as either "pending" (unprocessed) or "processed"
|
||||
- **Minimal Data Storage**: Store only essential information - email UID, folder, user, and processing status - not email content, subjects, or bodies
|
||||
- **Persistence**: Maintain processing status across application restarts and synchronization cycles
|
||||
- **Efficient Lookup**: Quickly determine which emails in a folder are pending processing
|
||||
|
||||
### 2. Synchronization Requirements
|
||||
|
||||
- **Initial Sync**: During first synchronization of a folder, all emails should be marked as "pending"
|
||||
- **Incremental Sync**: On subsequent syncs, only emails that haven't been processed should be identified as pending
|
||||
- **Status Update**: When an email is processed, update its status from "pending" to "processed"
|
||||
- **Cleanup**: Remove records for emails that no longer exist on the IMAP server (optional for future enhancement)
|
||||
|
||||
### 3. Performance Requirements
|
||||
|
||||
- **Efficient Storage**: Use appropriate database indexing for fast lookups
|
||||
- **Minimal Memory Usage**: Store only essential data to keep memory footprint low
|
||||
- **Batch Processing**: Support batch operations for processing multiple emails efficiently
|
||||
|
||||
## Data Model Design
|
||||
|
||||
### ProcessedEmails Table
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USER {
|
||||
int id PK "Primary Key"
|
||||
string email "Unique, Not Null"
|
||||
string first_name "Not Null"
|
||||
string last_name "Not Null"
|
||||
string password_hash "Not Null"
|
||||
json imap_config "JSON Configuration"
|
||||
datetime created_at "Default: UTC Now"
|
||||
datetime updated_at "Default: UTC Now, On Update"
|
||||
}
|
||||
|
||||
FOLDER {
|
||||
int id PK "Primary Key"
|
||||
int user_id FK "Foreign Key to User"
|
||||
string name "Not Null"
|
||||
text rule_text "Natural Language Rule"
|
||||
int priority "Processing Order"
|
||||
boolean organize_enabled "Default: True"
|
||||
int total_count "Default: 0"
|
||||
int pending_count "Default: 0"
|
||||
json recent_emails "JSON Array"
|
||||
datetime created_at "Default: UTC Now"
|
||||
datetime updated_at "Default: UTC Now, On Update"
|
||||
}
|
||||
|
||||
PROCESSED_EMAIL {
|
||||
int id PK "Primary Key"
|
||||
int user_id FK "Foreign Key to User"
|
||||
int folder_id FK "Foreign Key to Folder"
|
||||
string email_uid "Not Null" "IMAP Email UID"
|
||||
string folder_name "Not Null" "IMAP Folder Name"
|
||||
boolean is_processed "Default: False" "Processing Status"
|
||||
datetime first_seen_at "Default: UTC Now" "First seen during sync"
|
||||
datetime processed_at "Nullable" "When email was processed"
|
||||
datetime created_at "Default: UTC Now"
|
||||
datetime updated_at "Default: UTC Now, On Update"
|
||||
}
|
||||
|
||||
USER ||--o{ FOLDER : "has"
|
||||
USER ||--o{ PROCESSED_EMAIL : "has"
|
||||
FOLDER ||--o{ PROCESSED_EMAIL : "has"
|
||||
```
|
||||
|
||||
### Column Specifications
|
||||
|
||||
| Table | Column | Data Type | Constraints | Description |
|
||||
|-------|--------|-----------|--------------|-------------|
|
||||
| PROCESSED_EMAIL | id | Integer | Primary Key, Autoincrement | Unique identifier for each processed email record |
|
||||
| PROCESSED_EMAIL | user_id | Integer | Foreign Key to User, Not Null | Reference to the user who owns this email |
|
||||
| PROCESSED_EMAIL | folder_id | Integer | Foreign Key to Folder, Not Null | Reference to the folder this email belongs to |
|
||||
| PROCESSED_EMAIL | email_uid | String(255) | Not Null | Unique ID of the email from IMAP server |
|
||||
| PROCESSED_EMAIL | folder_name | String(255) | Not Null | Name of the IMAP folder (for redundancy) |
|
||||
| PROCESSED_EMAIL | is_processed | Boolean | Default: False | Processing status (false=pending, true=processed) |
|
||||
| PROCESSED_EMAIL | first_seen_at | DateTime | Default: datetime.utcnow | First time this email was detected during sync |
|
||||
| PROCESSED_EMAIL | processed_at | DateTime | Nullable | When the email was marked as processed |
|
||||
| PROCESSED_EMAIL | created_at | DateTime | Default: datetime.utcnow | Record creation timestamp |
|
||||
| PROCESSED_EMAIL | updated_at | DateTime | Default: datetime.utcnow, On Update | Record update timestamp |
|
||||
|
||||
### Relationships
|
||||
|
||||
- **User to ProcessedEmail**: One-to-many relationship - each user can have multiple processed email records
|
||||
- **Folder to ProcessedEmail**: One-to-many relationship - each folder can have multiple processed email records
|
||||
- **Composite Key**: The combination of (user_id, folder_name, email_uid) should be unique to prevent duplicate records
|
||||
|
||||
### Database Indexes
|
||||
|
||||
- Primary key index on `id`
|
||||
- Foreign key indexes on `user_id` and `folder_id`
|
||||
- Composite unique index on `(user_id, folder_name, email_uid)`
|
||||
- Index on `folder_name` for faster folder-based queries
|
||||
- Index on `is_processed` for filtering pending emails
|
||||
- Index on `first_seen_at` for tracking recently added emails
|
||||
|
||||
## Service Design
|
||||
|
||||
### ProcessedEmailsService
|
||||
|
||||
A new service class will be responsible for managing processed email records:
|
||||
|
||||
```python
|
||||
class ProcessedEmailsService:
|
||||
def __init__(self, user: User):
|
||||
self.user = user
|
||||
|
||||
def get_pending_emails(self, folder_name: str) -> List[str]:
|
||||
"""Get list of email UIDs that are pending processing in a folder."""
|
||||
|
||||
def mark_email_processed(self, folder_name: str, email_uid: str) -> bool:
|
||||
"""Mark an email as processed."""
|
||||
|
||||
def mark_emails_processed(self, folder_name: str, email_uids: List[str]) -> int:
|
||||
"""Mark multiple emails as processed in bulk."""
|
||||
|
||||
def sync_folder_emails(self, folder_name: str, email_uids: List[str]) -> int:
|
||||
"""Sync email UIDs for a folder, adding new ones as pending."""
|
||||
|
||||
def get_pending_count(self, folder_name: str) -> int:
|
||||
"""Get count of pending emails for a folder."""
|
||||
|
||||
def cleanup_old_records(self, folder_name: str, current_uids: List[str]) -> int:
|
||||
"""Remove records for emails that no longer exist in the folder."""
|
||||
```
|
||||
|
||||
### IMAPService Integration
|
||||
|
||||
The existing IMAP service will be enhanced to use the ProcessedEmailsService:
|
||||
|
||||
```python
|
||||
class IMAPService:
|
||||
def __init__(self, user: User):
|
||||
self.user = user
|
||||
self.config = user.imap_config or {}
|
||||
self.connection = None
|
||||
self.processed_emails_service = ProcessedEmailsService(user)
|
||||
|
||||
def get_folder_email_count(self, folder_name: str) -> int:
|
||||
"""Get the count of emails in a specific folder, considering processed status."""
|
||||
|
||||
def get_pending_emails(self, folder_name: str) -> List[str]:
|
||||
"""Get email UIDs that are pending processing."""
|
||||
|
||||
def sync_folders(self) -> Tuple[bool, str]:
|
||||
"""Sync IMAP folders with local database, tracking email processing status."""
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### New HTMX Endpoints for Processed Email Management
|
||||
|
||||
1. **Get Pending Emails for a Folder**
|
||||
- Method: GET
|
||||
- Path: `/api/folders/<folder_id>/pending-emails`
|
||||
- Response: An Dialog List of email metadata for pending emails (subject, date, UID), a button to preview the email (fetch it from the imap server)
|
||||
|
||||
2. **Mark Email as Processed**
|
||||
- Method: POST
|
||||
- Path: `/api/folders/<folder_id>/emails/<email_uid>/process`
|
||||
- Action: Mark a specific email as processed
|
||||
- Response: Updated dialog body.
|
||||
|
||||
## Workflow Integration
|
||||
|
||||
### Email Processing Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User
|
||||
participant B as Browser
|
||||
participant M as Main Blueprint
|
||||
participant I as IMAP Service
|
||||
participant P as ProcessedEmails Service
|
||||
participant DB as Database
|
||||
|
||||
U->>B: Click "Sync Folders"
|
||||
B->>M: POST /api/imap/sync
|
||||
M->>I: Sync folders with processed email tracking
|
||||
I->>I: Connect to IMAP server
|
||||
I->>I: Get list of email UIDs for folder
|
||||
I->>P: sync_folder_emails(folder_name, email_uids)
|
||||
P->>DB: Create pending email records
|
||||
P->>I: Return list of pending email UIDs
|
||||
I->>M: Return sync results
|
||||
M->>B: Update UI with pending counts
|
||||
```
|
||||
|
||||
### Email Processing Status Update
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User
|
||||
participant B as Browser
|
||||
participant M as Main Blueprint
|
||||
participant P as ProcessedEmails Service
|
||||
participant DB as Database
|
||||
|
||||
U->>B: Trigger email processing
|
||||
B->>M: POST /api/folders/<folder_id>/process-emails
|
||||
M->>P: mark_emails_processed(folder_name, email_uids)
|
||||
P->>DB: Update email processing status
|
||||
P->>M: Return success count
|
||||
M->>B: Update UI with new counts
|
||||
```
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Phase 1: Data Model Implementation
|
||||
1. Create the `processed_emails` table with appropriate indexes
|
||||
2. Implement the `ProcessedEmailsService` class
|
||||
3. Add basic CRUD operations for email processing records
|
||||
|
||||
### Phase 2: IMAP Service Integration
|
||||
1. Update `IMAPService` to use `ProcessedEmailsService`
|
||||
2. Modify folder synchronization to track email UIDs
|
||||
3. Update email count methods to consider processing status
|
||||
|
||||
### Phase 3: API and UI Integration
|
||||
1. Add API endpoints for processed email management
|
||||
2. Update UI to display accurate pending counts
|
||||
3. Add bulk processing capabilities
|
||||
|
||||
### Phase 4: Optimization and Cleanup
|
||||
1. Implement batch processing for performance
|
||||
2. Add periodic cleanup of orphaned records
|
||||
3. Optimize database queries for large datasets
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Access Control**: Ensure users can only access their own email processing records
|
||||
2. **Data Validation**: Validate all email UIDs and folder names to prevent injection attacks
|
||||
3. **Rate Limiting**: Implement rate limiting for email processing endpoints to prevent abuse
|
||||
4. **Data Privacy**: Ensure no sensitive email content is stored in the database
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **Database Indexing**: Proper indexing on frequently queried fields
|
||||
2. **Batch Operations**: Use batch operations for processing multiple emails
|
||||
3. **Memory Management**: Process emails in batches to avoid memory issues with large mailboxes
|
||||
4. **Caching**: Consider caching frequently accessed email processing status
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Email Movement Tracking**: Track when emails are moved between folders
|
||||
2. **Processing History**: Maintain a history of email processing actions
|
||||
3. **Email Deduplication**: Handle duplicate emails across folders
|
||||
4. **Automated Cleanup**: Periodic cleanup of old or orphaned processing records
|
||||
5. **Analytics**: Provide insights into email processing patterns and efficiency
|
||||
143
docs/plans/folder-types-redesign.md
Normal file
143
docs/plans/folder-types-redesign.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Folder Types Redesign Plan
|
||||
|
||||
## Overview
|
||||
|
||||
This plan outlines the redesign of the email organizer to support two distinct types of folders:
|
||||
1. **Folders to Tidy**: These folders contain emails that need to be processed and organized
|
||||
2. **Destination Folders**: These folders are targets for organizing emails from other folders
|
||||
|
||||
## Current State
|
||||
|
||||
Currently, all folders are treated the same way with:
|
||||
- Pending and processed counts
|
||||
- Organization rules
|
||||
- Toggle for enabling/disabling organization
|
||||
|
||||
The system doesn't differentiate between source folders (where emails are processed from) and destination folders (where emails are moved to).
|
||||
|
||||
## Proposed Changes
|
||||
|
||||
### 1. Data Model Updates
|
||||
|
||||
#### Folder Model Changes
|
||||
|
||||
Add a new field to the `Folder` model:
|
||||
```python
|
||||
folder_type = db.Column(db.String(20), default='destination', nullable=False)
|
||||
```
|
||||
|
||||
Possible values:
|
||||
- `'tidy'`: Folders that contain emails to be processed (e.g., Inbox)
|
||||
- `'destination'`: Folders that are targets for email organization (default for new folders)
|
||||
|
||||
#### New Field Requirements:
|
||||
- Must be a string to allow for potential future folder types
|
||||
- Default value should be 'destination' for backward compatibility
|
||||
- Not nullable to ensure all folders have a defined type
|
||||
|
||||
### 2. Business Logic Changes
|
||||
|
||||
#### Folder Synchronization Logic
|
||||
- When synchronizing IMAP folders:
|
||||
- If folder name is 'inbox' (case-insensitive), set `folder_type = 'tidy'`
|
||||
- For all other folders, set `folder_type = 'destination'`
|
||||
- Existing folders will keep their current type unless explicitly changed
|
||||
|
||||
#### Folder Count Logic
|
||||
- **Folders to Tidy**:
|
||||
- Show `pending_count` (emails waiting to be processed)
|
||||
- Show `processed_count` (emails that have been processed)
|
||||
- May show `total_count` (total emails in the folder)
|
||||
|
||||
- **Destination Folders**:
|
||||
- Show `emails_count` (number of emails that have been put into this folder)
|
||||
- No need to show pending/processed counts
|
||||
|
||||
### 3. UI Changes
|
||||
|
||||
#### Main Page Layout
|
||||
Create two distinct sections:
|
||||
1. **Folders to Tidy Section**
|
||||
- Contains folders with `folder_type = 'tidy'`
|
||||
- Shows pending and processed counts
|
||||
- May include special styling to indicate these are source folders
|
||||
|
||||
2. **Destination Folders Section**
|
||||
- Contains folders with `folder_type = 'destination'`
|
||||
- Shows count of emails moved to this folder
|
||||
- May include special styling to indicate these are target folders
|
||||
|
||||
#### Folder Card Changes
|
||||
- **Tidy Folder Card**:
|
||||
- Display: Folder name, pending count, processed count
|
||||
- Actions: View pending emails, toggle organization
|
||||
|
||||
- **Destination Folder Card**:
|
||||
- Display: Folder name, emails count
|
||||
- Actions: View emails in folder (if implemented), basic folder management
|
||||
|
||||
### 4. API Changes
|
||||
|
||||
#### New Endpoints
|
||||
- `/api/folders/filter?type={tidy|destination}` - Get folders filtered by type
|
||||
- `/api/folders/<id>/type` - Update folder type (admin function)
|
||||
|
||||
#### Existing Endpoint Updates
|
||||
- `/api/folders` - Add filtering capability
|
||||
- `/api/imap/sync` - Implement new folder type logic
|
||||
- Folder-related templates should adapt based on folder type
|
||||
|
||||
### 5. Migration Strategy
|
||||
|
||||
#### Database Migration
|
||||
1. Create a new migration to add the `folder_type` column
|
||||
2. Set default value to 'destination' for existing folders
|
||||
3. Special handling for 'inbox' folders to set type to 'tidy'
|
||||
|
||||
#### Data Migration
|
||||
- Scan all existing folders for user with 'inbox' in the name
|
||||
- Set `folder_type = 'tidy'` for these folders
|
||||
- Set `folder_type = 'destination'` for all other folders
|
||||
|
||||
### 6. Implementation Steps
|
||||
|
||||
1. **Phase 1: Data Model**
|
||||
- Create migration for `folder_type` column
|
||||
- Update `Folder` model
|
||||
- Apply migration to database
|
||||
|
||||
2. **Phase 2: Backend Logic**
|
||||
- Update folder synchronization logic
|
||||
- Modify folder query methods
|
||||
- Update count calculation logic
|
||||
|
||||
3. **Phase 3: Frontend UI**
|
||||
- Create two new partial templates for folder sections
|
||||
- Update folder card templates based on type
|
||||
- Modify main page to show two sections
|
||||
|
||||
4. **Phase 4: Testing**
|
||||
- Test folder synchronization with new types
|
||||
- Verify UI shows correct sections
|
||||
- Test folder count calculations
|
||||
|
||||
### 7. Future Considerations
|
||||
|
||||
#### Potential Enhancements
|
||||
- Add ability to change folder type through UI
|
||||
- Implement email movement tracking
|
||||
- Add analytics for email organization efficiency
|
||||
- Support for additional folder types in the future
|
||||
|
||||
#### Backward Compatibility
|
||||
- Existing folders will be treated as destination folders
|
||||
- No breaking changes to existing functionality
|
||||
- Gradual rollout of new UI sections
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Folders are correctly categorized as either 'tidy' or 'destination'
|
||||
2. Inbox folders are automatically set as 'tidy' type
|
||||
3. UI displays two distinct sections for folder types
|
||||
4. Folder cards show appropriate counts based on type
|
||||
5. All existing functionality continues to work
|
||||
42
migrations/versions/29a9c4ac0fc6_processing_email.py
Normal file
42
migrations/versions/29a9c4ac0fc6_processing_email.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""processing email
|
||||
|
||||
Revision ID: 29a9c4ac0fc6
|
||||
Revises: 9a88c7e94083
|
||||
Create Date: 2025-08-06 11:40:52.702637
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '29a9c4ac0fc6'
|
||||
down_revision = '9a88c7e94083'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('processed_emails',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('folder_id', sa.Integer(), nullable=False),
|
||||
sa.Column('email_uid', sa.String(length=255), nullable=False),
|
||||
sa.Column('folder_name', sa.String(length=255), nullable=False),
|
||||
sa.Column('is_processed', sa.Boolean(), nullable=True),
|
||||
sa.Column('first_seen_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('processed_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['folder_id'], ['folders.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('processed_emails')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Add folder_type and emails_count fields to Folder table
|
||||
|
||||
Revision ID: 7b6db971e3a4
|
||||
Revises: 29a9c4ac0fc6
|
||||
Create Date: 2025-08-06 12:25:13.415140
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '7b6db971e3a4'
|
||||
down_revision = '29a9c4ac0fc6'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# Add new columns as nullable first
|
||||
op.add_column('folders', sa.Column('folder_type', sa.String(length=20), nullable=True))
|
||||
op.add_column('folders', sa.Column('emails_count', sa.Integer(), nullable=True))
|
||||
|
||||
# Set default values for existing records
|
||||
# Set all existing folders to 'destination' type
|
||||
op.execute("UPDATE folders SET folder_type = 'destination' WHERE folder_type IS NULL")
|
||||
|
||||
# Set 'inbox' folders to 'tidy' type
|
||||
op.execute("UPDATE folders SET folder_type = 'tidy' WHERE folder_type IS NULL AND name ILIKE '%inbox%'")
|
||||
|
||||
# Now make folder_type NOT NULL
|
||||
op.alter_column('folders', 'folder_type', nullable=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# Make column nullable again before dropping
|
||||
op.alter_column('folders', 'folder_type', nullable=True)
|
||||
|
||||
# Drop the columns
|
||||
op.drop_column('folders', 'emails_count')
|
||||
op.drop_column('folders', 'folder_type')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -40,7 +40,7 @@ def mock_user(app):
|
||||
first_name='Test',
|
||||
last_name='User',
|
||||
email='test@example.com',
|
||||
password_hash=b'hashed_password' # Will be properly hashed in real tests
|
||||
password_hash='hashed_password' # Will be properly hashed in real tests
|
||||
)
|
||||
|
||||
db.session.add(user)
|
||||
|
||||
78
tests/test_folder_deletion.py
Normal file
78
tests/test_folder_deletion.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import pytest
|
||||
from app.models import User, Folder, ProcessedEmail, db
|
||||
from app import create_app
|
||||
|
||||
|
||||
class TestFolderDeletion:
|
||||
def test_delete_folder_with_emails(self, app, mock_user, authenticated_client):
|
||||
"""Test that deleting a folder also deletes its associated emails."""
|
||||
with app.app_context():
|
||||
# Create a folder
|
||||
folder = Folder(
|
||||
user_id=mock_user.id,
|
||||
name='Test Folder',
|
||||
rule_text='Test rule'
|
||||
)
|
||||
db.session.add(folder)
|
||||
db.session.commit()
|
||||
|
||||
# Create some email records associated with this folder
|
||||
email_records = [
|
||||
ProcessedEmail(
|
||||
user_id=mock_user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='123',
|
||||
is_processed=False
|
||||
),
|
||||
ProcessedEmail(
|
||||
user_id=mock_user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='456',
|
||||
is_processed=False
|
||||
)
|
||||
]
|
||||
db.session.bulk_save_objects(email_records)
|
||||
db.session.commit()
|
||||
|
||||
# Verify emails were created
|
||||
assert ProcessedEmail.query.count() == 2
|
||||
|
||||
# Delete the folder
|
||||
response = authenticated_client.delete(f'/api/folders/{folder.id}')
|
||||
|
||||
# Verify the response is successful
|
||||
assert response.status_code == 200
|
||||
|
||||
# Verify the folder is deleted
|
||||
deleted_folder = Folder.query.filter_by(id=folder.id).first()
|
||||
assert deleted_folder is None
|
||||
|
||||
# Verify the associated emails are also deleted (cascade delete)
|
||||
assert ProcessedEmail.query.count() == 0
|
||||
|
||||
def test_delete_folder_with_no_emails(self, app, mock_user, authenticated_client):
|
||||
"""Test that deleting a folder with no associated emails works normally."""
|
||||
with app.app_context():
|
||||
# Create a folder
|
||||
folder = Folder(
|
||||
user_id=mock_user.id,
|
||||
name='Test Folder',
|
||||
rule_text='Test rule'
|
||||
)
|
||||
db.session.add(folder)
|
||||
db.session.commit()
|
||||
|
||||
# Verify no emails are associated with this folder
|
||||
assert ProcessedEmail.query.filter_by(folder_id=folder.id).count() == 0
|
||||
|
||||
# Delete the folder
|
||||
response = authenticated_client.delete(f'/api/folders/{folder.id}')
|
||||
|
||||
# Verify the response is successful
|
||||
assert response.status_code == 200
|
||||
|
||||
# Verify the folder is deleted
|
||||
deleted_folder = Folder.query.filter_by(id=folder.id).first()
|
||||
assert deleted_folder is None
|
||||
@@ -102,6 +102,10 @@ class TestIMAPService:
|
||||
with patch('app.imap_service.imaplib.IMAP4_SSL') as mock_imap_ssl:
|
||||
mock_connection = mock_imap_ssl.return_value
|
||||
mock_connection.list.return_value = ('OK', [])
|
||||
mock_connection.login.return_value = None
|
||||
mock_connection.select.return_value = ('OK', [b'1'])
|
||||
mock_connection.close.return_value = None
|
||||
mock_connection.logout.return_value = None
|
||||
|
||||
user = User(email='test@example.com', first_name='Test', last_name='User')
|
||||
user.imap_config = {'server': 'test.com', 'port': 993, 'username': 'user', 'password': 'pass'}
|
||||
@@ -119,6 +123,7 @@ class TestIMAPService:
|
||||
# Mock the IMAP connection to raise an exception
|
||||
with patch('app.imap_service.imaplib.IMAP4_SSL') as mock_imap_ssl:
|
||||
mock_connection = mock_imap_ssl.return_value
|
||||
mock_connection.login.return_value = None
|
||||
mock_connection.list.side_effect = Exception("IMAP error")
|
||||
|
||||
folders = imap_service.get_folders()
|
||||
@@ -193,22 +198,19 @@ class TestIMAPService:
|
||||
|
||||
def test_sync_folders_no_folders(self, app):
|
||||
with app.app_context():
|
||||
# Mock the IMAP connection to return an empty list of folders
|
||||
with patch('app.imap_service.imaplib.IMAP4_SSL') as mock_imap_ssl:
|
||||
mock_connection = mock_imap_ssl.return_value
|
||||
mock_connection.list.return_value = ('OK', [])
|
||||
user = User(email='test@example.com', first_name='Test', last_name='User')
|
||||
user.set_password('testpassword')
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
user.imap_config = {'server': 'localhost', 'port': 5143, 'username': 'user1@example.com', 'password': 'password1', 'use_ssl': False}
|
||||
imap_service = IMAPService(user)
|
||||
|
||||
# Mock the get_folders method to return an empty list
|
||||
with patch.object(imap_service, 'get_folders', return_value=[]):
|
||||
success, message = imap_service.sync_folders()
|
||||
|
||||
user = User(email='test@example.com', first_name='Test', last_name='User')
|
||||
user.set_password('testpassword')
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
user.imap_config = {'server': 'localhost', 'port': 5143, 'username': 'user1@example.com', 'password': 'password1', 'use_ssl': False}
|
||||
imap_service = IMAPService(user)
|
||||
|
||||
success, message = imap_service.sync_folders()
|
||||
|
||||
assert success is False
|
||||
assert message == "No folders found on IMAP server"
|
||||
assert success is False
|
||||
assert message == "No folders found on IMAP server"
|
||||
|
||||
def test_sync_folders_exception(self, app):
|
||||
with app.app_context():
|
||||
@@ -220,12 +222,9 @@ class TestIMAPService:
|
||||
user.imap_config = {'server': 'localhost', 'port': 5143, 'username': 'user1@example.com', 'password': 'password1', 'use_ssl': False }
|
||||
imap_service = IMAPService(user)
|
||||
|
||||
# Mock the IMAP connection to raise an exception
|
||||
with patch('app.imap_service.imaplib.IMAP4_SSL') as mock_imap_ssl:
|
||||
mock_connection = mock_imap_ssl.return_value
|
||||
mock_connection.list.side_effect = Exception("IMAP server error")
|
||||
|
||||
# Mock the get_folders method to raise an exception
|
||||
with patch.object(imap_service, 'get_folders', side_effect=Exception("IMAP server error")):
|
||||
success, message = imap_service.sync_folders()
|
||||
|
||||
assert success is False
|
||||
assert "No folders found on IMAP server" in message
|
||||
assert "Sync error: IMAP server error" in message
|
||||
267
tests/test_processed_emails_routes.py
Normal file
267
tests/test_processed_emails_routes.py
Normal file
@@ -0,0 +1,267 @@
|
||||
import pytest
|
||||
from flask import url_for
|
||||
from unittest.mock import patch
|
||||
from app.models import User, Folder, ProcessedEmail, db
|
||||
from app import create_app
|
||||
from app.imap_service import IMAPService
|
||||
|
||||
|
||||
class TestProcessedEmailsRoutes:
|
||||
def test_get_pending_emails_success(self, app, mock_user, authenticated_client):
|
||||
"""Test get_pending_emails endpoint successfully."""
|
||||
with app.app_context():
|
||||
# Create a folder
|
||||
folder = Folder(
|
||||
user_id=mock_user.id,
|
||||
name='Test Folder',
|
||||
rule_text='Test rule'
|
||||
)
|
||||
db.session.add(folder)
|
||||
db.session.commit()
|
||||
|
||||
# Create some pending email records
|
||||
pending_emails = [
|
||||
ProcessedEmail(
|
||||
user_id=mock_user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='123',
|
||||
is_processed=False
|
||||
),
|
||||
ProcessedEmail(
|
||||
user_id=mock_user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='456',
|
||||
is_processed=False
|
||||
)
|
||||
]
|
||||
db.session.bulk_save_objects(pending_emails)
|
||||
db.session.commit()
|
||||
|
||||
# Mock IMAP service to return email headers
|
||||
with patch('app.routes.IMAPService') as mock_imap_service:
|
||||
mock_imap_instance = mock_imap_service.return_value
|
||||
mock_imap_instance.get_email_headers.side_effect = [
|
||||
{
|
||||
'subject': 'Test Subject 1',
|
||||
'date': '2023-01-01T12:00:00',
|
||||
'from': 'sender1@example.com',
|
||||
'to': 'recipient@example.com',
|
||||
'message_id': '<msg1@example.com>'
|
||||
},
|
||||
{
|
||||
'subject': 'Test Subject 2',
|
||||
'date': '2023-01-02T12:00:00',
|
||||
'from': 'sender2@example.com',
|
||||
'to': 'recipient@example.com',
|
||||
'message_id': '<msg2@example.com>'
|
||||
}
|
||||
]
|
||||
|
||||
response = authenticated_client.get(f'/api/folders/{folder.id}/pending-emails')
|
||||
|
||||
assert response.status_code == 200
|
||||
# The response should be HTML with the pending emails dialog
|
||||
assert 'Test Subject 1' in response.get_data(as_text=True)
|
||||
assert 'Test Subject 2' in response.get_data(as_text=True)
|
||||
|
||||
def test_get_pending_emails_folder_not_found(self, app, mock_user, authenticated_client):
|
||||
"""Test get_pending_emails endpoint with non-existent folder."""
|
||||
with app.app_context():
|
||||
response = authenticated_client.get('/api/folders/999/pending-emails')
|
||||
|
||||
assert response.status_code == 404
|
||||
assert 'Folder not found' in response.get_json()['error']
|
||||
|
||||
def test_mark_email_processed_success(self, app, mock_user, authenticated_client):
|
||||
"""Test mark_email_processed endpoint successfully."""
|
||||
with app.app_context():
|
||||
# Create a folder
|
||||
folder = Folder(
|
||||
user_id=mock_user.id,
|
||||
name='Test Folder',
|
||||
rule_text='Test rule'
|
||||
)
|
||||
db.session.add(folder)
|
||||
db.session.commit()
|
||||
|
||||
# Create a pending email record
|
||||
pending_email = ProcessedEmail(
|
||||
user_id=mock_user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='123',
|
||||
is_processed=False
|
||||
)
|
||||
db.session.add(pending_email)
|
||||
db.session.commit()
|
||||
|
||||
response = authenticated_client.post(f'/api/folders/{folder.id}/emails/123/process')
|
||||
|
||||
assert response.status_code == 200
|
||||
# The response should be HTML with the updated dialog
|
||||
assert 'has been marked as processed successfully' in response.get_data(as_text=True)
|
||||
|
||||
# Verify the email is marked as processed
|
||||
updated_email = ProcessedEmail.query.filter_by(
|
||||
user_id=mock_user.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='123'
|
||||
).first()
|
||||
assert updated_email.is_processed is True
|
||||
|
||||
def test_mark_email_processed_folder_not_found(self, app, mock_user, authenticated_client):
|
||||
"""Test mark_email_processed endpoint with non-existent folder."""
|
||||
with app.app_context():
|
||||
# Login and make request
|
||||
login_response = response = authenticated_client.post('/api/folders/999/emails/123/process')
|
||||
|
||||
assert response.status_code == 404
|
||||
assert 'Folder not found' in response.get_json()['error']
|
||||
|
||||
def test_sync_folder_emails_success(self, app, mock_user, authenticated_client):
|
||||
"""Test sync_folder_emails endpoint successfully."""
|
||||
with app.app_context():
|
||||
# Create a folder
|
||||
folder = Folder(
|
||||
user_id=mock_user.id,
|
||||
name='Test Folder',
|
||||
rule_text='Test rule'
|
||||
)
|
||||
db.session.add(folder)
|
||||
db.session.commit()
|
||||
|
||||
# Mock IMAP service to return email UIDs
|
||||
with patch('app.routes.IMAPService') as mock_imap_service:
|
||||
mock_imap_instance = mock_imap_service.return_value
|
||||
mock_imap_instance.get_folder_email_uids.return_value = ['123', '456', '789']
|
||||
|
||||
# Login and make request
|
||||
response = authenticated_client.post(f'/api/folders/{folder.id}/sync-emails')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert data['success'] is True
|
||||
assert 'Synced 3 new emails' in data['message']
|
||||
assert data['pending_count'] == 3
|
||||
assert data['total_count'] == 3
|
||||
|
||||
# Verify records were created
|
||||
records = ProcessedEmail.query.filter_by(
|
||||
user_id=mock_user.id,
|
||||
folder_name='Test Folder'
|
||||
).all()
|
||||
assert len(records) == 3
|
||||
|
||||
def test_sync_folder_emails_no_emails(self, app, mock_user, authenticated_client):
|
||||
"""Test sync_folder_emails endpoint with no emails found."""
|
||||
with app.app_context():
|
||||
# Create a folder
|
||||
folder = Folder(
|
||||
user_id=mock_user.id,
|
||||
name='Test Folder',
|
||||
rule_text='Test rule'
|
||||
)
|
||||
db.session.add(folder)
|
||||
db.session.commit()
|
||||
|
||||
# Mock IMAP service to return no email UIDs
|
||||
with patch('app.routes.IMAPService') as mock_imap_service:
|
||||
mock_imap_instance = mock_imap_service.return_value
|
||||
mock_imap_instance.get_folder_email_uids.return_value = []
|
||||
|
||||
# Login and make request
|
||||
response = authenticated_client.post(f'/api/folders/{folder.id}/sync-emails')
|
||||
|
||||
assert response.status_code == 404
|
||||
assert 'No emails found in folder' in response.get_json()['error']
|
||||
|
||||
def test_sync_folder_emails_folder_not_found(self, app, mock_user, authenticated_client):
|
||||
"""Test sync_folder_emails endpoint with non-existent folder."""
|
||||
with app.app_context():
|
||||
# Login and make request
|
||||
login_response = response = authenticated_client.post('/api/folders/999/sync-emails')
|
||||
|
||||
assert response.status_code == 404
|
||||
assert 'Folder not found' in response.get_json()['error']
|
||||
|
||||
def test_process_folder_emails_success(self, app, mock_user, authenticated_client):
|
||||
"""Test process_folder_emails endpoint successfully."""
|
||||
with app.app_context():
|
||||
# Create a folder
|
||||
folder = Folder(
|
||||
user_id=mock_user.id,
|
||||
name='Test Folder',
|
||||
rule_text='Test rule'
|
||||
)
|
||||
db.session.add(folder)
|
||||
db.session.commit()
|
||||
|
||||
# Create some pending email records
|
||||
pending_emails = [
|
||||
ProcessedEmail(
|
||||
user_id=mock_user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='123',
|
||||
is_processed=False
|
||||
),
|
||||
ProcessedEmail(
|
||||
user_id=mock_user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='456',
|
||||
is_processed=False
|
||||
)
|
||||
]
|
||||
db.session.bulk_save_objects(pending_emails)
|
||||
db.session.commit()
|
||||
|
||||
# Login and make request
|
||||
login_response = response = authenticated_client.post(f'/api/folders/{folder.id}/process-emails', data={
|
||||
'email_uids': ['123', '456']
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert data['success'] is True
|
||||
assert 'Processed 2 emails' in data['message']
|
||||
assert data['pending_count'] == 0
|
||||
|
||||
# Verify emails are marked as processed
|
||||
processed_emails = ProcessedEmail.query.filter_by(
|
||||
user_id=mock_user.id,
|
||||
folder_name='Test Folder',
|
||||
is_processed=True
|
||||
).all()
|
||||
assert len(processed_emails) == 2
|
||||
|
||||
def test_process_folder_emails_no_uids(self, app, mock_user, authenticated_client):
|
||||
"""Test process_folder_emails endpoint with no email UIDs provided."""
|
||||
with app.app_context():
|
||||
# Create a folder
|
||||
folder = Folder(
|
||||
user_id=mock_user.id,
|
||||
name='Test Folder',
|
||||
rule_text='Test rule'
|
||||
)
|
||||
db.session.add(folder)
|
||||
db.session.commit()
|
||||
|
||||
# Login and make request
|
||||
login_response = response = authenticated_client.post(f'/api/folders/{folder.id}/process-emails', data={})
|
||||
|
||||
assert response.status_code == 400
|
||||
assert 'No email UIDs provided' in response.get_json()['error']
|
||||
|
||||
def test_process_folder_emails_folder_not_found(self, app, mock_user, authenticated_client):
|
||||
"""Test process_folder_emails endpoint with non-existent folder."""
|
||||
with app.app_context():
|
||||
# Login and make request
|
||||
login_response = response = authenticated_client.post('/api/folders/999/process-emails', data={
|
||||
'email_uids': ['123', '456']
|
||||
})
|
||||
|
||||
assert response.status_code == 404
|
||||
assert 'Folder not found' in response.get_json()['error']
|
||||
321
tests/test_processed_emails_service.py
Normal file
321
tests/test_processed_emails_service.py
Normal file
@@ -0,0 +1,321 @@
|
||||
import pytest
|
||||
from app.processed_emails_service import ProcessedEmailsService
|
||||
from app.models import User, Folder, ProcessedEmail, db
|
||||
from app import create_app
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class TestProcessedEmailsService:
|
||||
def test_init_with_user(self, app, mock_user):
|
||||
"""Test ProcessedEmailsService initialization with a user."""
|
||||
service = ProcessedEmailsService(mock_user)
|
||||
assert service.user == mock_user
|
||||
|
||||
def test_get_pending_emails_empty(self, app, mock_user):
|
||||
"""Test get_pending_emails when no emails are pending."""
|
||||
service = ProcessedEmailsService(mock_user)
|
||||
|
||||
# Mock a folder
|
||||
folder = Folder(
|
||||
user_id=mock_user.id,
|
||||
name='Test Folder',
|
||||
rule_text='Test rule'
|
||||
)
|
||||
db.session.add(folder)
|
||||
db.session.commit()
|
||||
|
||||
# Test with no pending emails
|
||||
pending_uids = service.get_pending_emails('Test Folder')
|
||||
assert len(pending_uids) == 0
|
||||
|
||||
def test_get_pending_emails_with_data(self, app, mock_user):
|
||||
"""Test get_pending_emails when there are pending emails."""
|
||||
with app.app_context():
|
||||
service = ProcessedEmailsService(mock_user)
|
||||
|
||||
# Mock a folder
|
||||
folder = Folder(
|
||||
user_id=mock_user.id,
|
||||
name='Test Folder',
|
||||
rule_text='Test rule'
|
||||
)
|
||||
db.session.add(folder)
|
||||
db.session.commit()
|
||||
|
||||
# Create some pending email records
|
||||
pending_emails = [
|
||||
ProcessedEmail(
|
||||
user_id=mock_user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='123',
|
||||
is_processed=False
|
||||
),
|
||||
ProcessedEmail(
|
||||
user_id=mock_user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='456',
|
||||
is_processed=False
|
||||
)
|
||||
]
|
||||
db.session.bulk_save_objects(pending_emails)
|
||||
db.session.commit()
|
||||
|
||||
# Test getting pending emails
|
||||
pending_uids = service.get_pending_emails('Test Folder')
|
||||
assert len(pending_uids) == 2
|
||||
assert '123' in pending_uids
|
||||
assert '456' in pending_uids
|
||||
|
||||
def test_mark_email_processed_existing(self, app, mock_user):
|
||||
"""Test mark_email_processed for an existing pending email."""
|
||||
with app.app_context():
|
||||
service = ProcessedEmailsService(mock_user)
|
||||
|
||||
# Mock a folder
|
||||
folder = Folder(
|
||||
user_id=mock_user.id,
|
||||
name='Test Folder',
|
||||
rule_text='Test rule'
|
||||
)
|
||||
db.session.add(folder)
|
||||
db.session.commit()
|
||||
|
||||
# Create a pending email record
|
||||
pending_email = ProcessedEmail(
|
||||
user_id=mock_user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='123',
|
||||
is_processed=False
|
||||
)
|
||||
db.session.add(pending_email)
|
||||
db.session.commit()
|
||||
|
||||
# Mark email as processed
|
||||
success = service.mark_email_processed('Test Folder', '123')
|
||||
assert success is True
|
||||
|
||||
# Verify the email is marked as processed
|
||||
updated_email = ProcessedEmail.query.filter_by(
|
||||
user_id=mock_user.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='123'
|
||||
).first()
|
||||
assert updated_email.is_processed is True
|
||||
assert updated_email.processed_at is not None
|
||||
|
||||
def test_mark_email_processed_new(self, app, mock_user):
|
||||
"""Test mark_email_processed for a new email record."""
|
||||
with app.app_context():
|
||||
service = ProcessedEmailsService(mock_user)
|
||||
|
||||
# Mock a folder
|
||||
folder = Folder(
|
||||
user_id=mock_user.id,
|
||||
name='Test Folder',
|
||||
rule_text='Test rule'
|
||||
)
|
||||
db.session.add(folder)
|
||||
db.session.commit()
|
||||
|
||||
# Mark email as processed (create new record)
|
||||
success = service.mark_email_processed('Test Folder', '789')
|
||||
assert success is True
|
||||
|
||||
# Verify the record was created
|
||||
new_email = ProcessedEmail.query.filter_by(
|
||||
user_id=mock_user.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='789'
|
||||
).first()
|
||||
assert new_email is not None
|
||||
assert new_email.is_processed is True
|
||||
assert new_email.processed_at is not None
|
||||
|
||||
def test_mark_emails_processed(self, app, mock_user):
|
||||
"""Test mark_emails_processed for multiple emails."""
|
||||
with app.app_context():
|
||||
service = ProcessedEmailsService(mock_user)
|
||||
|
||||
# Mock a folder
|
||||
folder = Folder(
|
||||
user_id=mock_user.id,
|
||||
name='Test Folder',
|
||||
rule_text='Test rule'
|
||||
)
|
||||
db.session.add(folder)
|
||||
db.session.commit()
|
||||
|
||||
# Create some pending email records
|
||||
pending_emails = [
|
||||
ProcessedEmail(
|
||||
user_id=mock_user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='123',
|
||||
is_processed=False
|
||||
),
|
||||
ProcessedEmail(
|
||||
user_id=mock_user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='456',
|
||||
is_processed=False
|
||||
)
|
||||
]
|
||||
db.session.bulk_save_objects(pending_emails)
|
||||
db.session.commit()
|
||||
|
||||
# Mark multiple emails as processed
|
||||
processed_count = service.mark_emails_processed('Test Folder', ['123', '456', '789'])
|
||||
assert processed_count == 3
|
||||
|
||||
# Verify all emails are marked as processed
|
||||
processed_uids = set()
|
||||
for email in ProcessedEmail.query.filter_by(
|
||||
user_id=mock_user.id,
|
||||
folder_name='Test Folder'
|
||||
).all():
|
||||
processed_uids.add(email.email_uid)
|
||||
assert email.is_processed is True
|
||||
|
||||
assert '123' in processed_uids
|
||||
assert '456' in processed_uids
|
||||
assert '789' in processed_uids
|
||||
|
||||
def test_sync_folder_emails(self, app, mock_user):
|
||||
"""Test sync_folder_emails for a folder."""
|
||||
with app.app_context():
|
||||
service = ProcessedEmailsService(mock_user)
|
||||
|
||||
# Mock a folder
|
||||
folder = Folder(
|
||||
user_id=mock_user.id,
|
||||
name='Test Folder',
|
||||
rule_text='Test rule'
|
||||
)
|
||||
db.session.add(folder)
|
||||
db.session.commit()
|
||||
|
||||
# Sync emails
|
||||
email_uids = ['123', '456', '789']
|
||||
new_emails_count = service.sync_folder_emails('Test Folder', email_uids)
|
||||
assert new_emails_count == 3
|
||||
|
||||
# Verify records were created
|
||||
records = ProcessedEmail.query.filter_by(
|
||||
user_id=mock_user.id,
|
||||
folder_name='Test Folder'
|
||||
).all()
|
||||
assert len(records) == 3
|
||||
|
||||
# Verify folder counts
|
||||
updated_folder = Folder.query.filter_by(id=folder.id).first()
|
||||
assert updated_folder.total_count == 3
|
||||
assert updated_folder.pending_count == 3
|
||||
|
||||
def test_get_pending_count(self, app, mock_user):
|
||||
"""Test get_pending_count for a folder."""
|
||||
with app.app_context():
|
||||
service = ProcessedEmailsService(mock_user)
|
||||
|
||||
# Mock a folder
|
||||
folder = Folder(
|
||||
user_id=mock_user.id,
|
||||
name='Test Folder',
|
||||
rule_text='Test rule'
|
||||
)
|
||||
db.session.add(folder)
|
||||
db.session.commit()
|
||||
|
||||
# Create some pending and processed emails
|
||||
emails = [
|
||||
ProcessedEmail(
|
||||
user_id=mock_user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='1',
|
||||
is_processed=False
|
||||
),
|
||||
ProcessedEmail(
|
||||
user_id=mock_user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='2',
|
||||
is_processed=False
|
||||
),
|
||||
ProcessedEmail(
|
||||
user_id=mock_user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='3',
|
||||
is_processed=True
|
||||
)
|
||||
]
|
||||
db.session.bulk_save_objects(emails)
|
||||
db.session.commit()
|
||||
|
||||
# Test pending count
|
||||
pending_count = service.get_pending_count('Test Folder')
|
||||
assert pending_count == 2
|
||||
|
||||
def test_cleanup_old_records(self, app, mock_user):
|
||||
"""Test cleanup_old_records for a folder."""
|
||||
with app.app_context():
|
||||
service = ProcessedEmailsService(mock_user)
|
||||
|
||||
# Mock a folder
|
||||
folder = Folder(
|
||||
user_id=mock_user.id,
|
||||
name='Test Folder',
|
||||
rule_text='Test rule'
|
||||
)
|
||||
db.session.add(folder)
|
||||
db.session.commit()
|
||||
|
||||
# Create some email records
|
||||
emails = [
|
||||
ProcessedEmail(
|
||||
user_id=mock_user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='1',
|
||||
is_processed=False
|
||||
),
|
||||
ProcessedEmail(
|
||||
user_id=mock_user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='2',
|
||||
is_processed=False
|
||||
),
|
||||
ProcessedEmail(
|
||||
user_id=mock_user.id,
|
||||
folder_id=folder.id,
|
||||
folder_name='Test Folder',
|
||||
email_uid='3',
|
||||
is_processed=True
|
||||
)
|
||||
]
|
||||
db.session.bulk_save_objects(emails)
|
||||
db.session.commit()
|
||||
|
||||
# Clean up records that don't exist in current UIDs
|
||||
current_uids = ['1', '2']
|
||||
deleted_count = service.cleanup_old_records('Test Folder', current_uids)
|
||||
assert deleted_count == 1
|
||||
|
||||
# Verify only existing records remain
|
||||
remaining_records = ProcessedEmail.query.filter_by(
|
||||
user_id=mock_user.id,
|
||||
folder_name='Test Folder'
|
||||
).all()
|
||||
assert len(remaining_records) == 2
|
||||
assert all(email.email_uid in current_uids for email in remaining_records)
|
||||
|
||||
# Verify folder counts
|
||||
updated_folder = Folder.query.filter_by(id=folder.id).first()
|
||||
assert updated_folder.total_count == 2
|
||||
assert updated_folder.pending_count == 2
|
||||
Reference in New Issue
Block a user