lots of configuration progress.
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user