lots of configuration progress.

This commit is contained in:
2025-08-06 15:38:49 -07:00
parent c6102dda45
commit 41ea8fb3bd
24 changed files with 2566 additions and 51 deletions

View File

@@ -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:

View File

@@ -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}>'

View 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

View File

@@ -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

View File

@@ -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>

View 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>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>