359 lines
15 KiB
Python
359 lines
15 KiB
Python
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.imap_service import IMAPService
|
|
import uuid
|
|
import logging
|
|
|
|
main = Blueprint('main', __name__)
|
|
|
|
@main.route('/')
|
|
@login_required
|
|
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)
|
|
|
|
@main.route('/api/folders/new', methods=['GET'])
|
|
@login_required
|
|
def new_folder_modal():
|
|
# Return the add folder modal
|
|
response = make_response(render_template('partials/folder_modal.html'))
|
|
response.headers['HX-Trigger'] = 'open-modal'
|
|
return response
|
|
|
|
@main.route('/api/folders', methods=['POST'])
|
|
@login_required
|
|
def add_folder():
|
|
try:
|
|
# Get form data instead of JSON
|
|
name = request.form.get('name')
|
|
rule_text = request.form.get('rule_text')
|
|
priority = request.form.get('priority')
|
|
|
|
# Server-side validation
|
|
errors = {}
|
|
if not name or not name.strip():
|
|
errors['name'] = 'Folder name is required'
|
|
elif len(name.strip()) < 3:
|
|
errors['name'] = 'Folder name must be at least 3 characters'
|
|
elif len(name.strip()) > 50:
|
|
errors['name'] = 'Folder name must be less than 50 characters'
|
|
|
|
if not rule_text or not rule_text.strip():
|
|
errors['rule_text'] = 'Rule text is required'
|
|
elif len(rule_text.strip()) < 10:
|
|
errors['rule_text'] = 'Rule text must be at least 10 characters'
|
|
elif len(rule_text.strip()) > 200:
|
|
errors['rule_text'] = 'Rule text must be less than 200 characters'
|
|
|
|
# If there are validation errors, return the modal with errors
|
|
if errors:
|
|
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'
|
|
return response
|
|
|
|
# Create new folder for the current user
|
|
folder = Folder(
|
|
user_id=current_user.id,
|
|
name=name.strip(),
|
|
rule_text=rule_text.strip(),
|
|
priority=int(priority) if priority else 0
|
|
)
|
|
|
|
db.session.add(folder)
|
|
db.session.commit()
|
|
|
|
# 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))
|
|
response.headers['HX-Trigger'] = 'close-modal'
|
|
response.status_code = 201
|
|
return response
|
|
|
|
except Exception as e:
|
|
# Print unhandled exceptions to the console as required
|
|
logging.exception("Error adding folder: %s", e)
|
|
db.session.rollback()
|
|
# Return error in modal
|
|
errors = {'general': 'An unexpected error occurred. Please try again.'}
|
|
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'
|
|
return response
|
|
|
|
@main.route('/api/folders/<folder_id>', methods=['DELETE'])
|
|
@login_required
|
|
def delete_folder(folder_id):
|
|
try:
|
|
# Find the folder by ID 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:
|
|
# Folder not found
|
|
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
|
return render_template('partials/folders_list.html', folders=folders)
|
|
|
|
# Delete the folder
|
|
db.session.delete(folder)
|
|
db.session.commit()
|
|
|
|
# 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)
|
|
|
|
except Exception as e:
|
|
# Print unhandled exceptions to the console as required
|
|
logging.exception("Error deleting folder: %s", e)
|
|
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)
|
|
|
|
@main.route('/api/folders/<folder_id>/toggle', methods=['PUT'])
|
|
@login_required
|
|
def toggle_folder_organize(folder_id):
|
|
try:
|
|
# Find the folder by ID 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
|
|
|
|
# Toggle the organize_enabled flag
|
|
folder.organize_enabled = not folder.organize_enabled
|
|
|
|
db.session.commit()
|
|
|
|
# Return just the updated folder card HTML for this specific folder
|
|
return render_template('partials/folder_card.html', folder=folder)
|
|
|
|
except Exception as e:
|
|
# Print unhandled exceptions to the console as required
|
|
logging.exception("Error toggling folder organize flag: %s", e)
|
|
db.session.rollback()
|
|
return jsonify({'error': 'An unexpected error occurred'}), 500
|
|
|
|
@main.route('/api/folders/<folder_id>/edit', methods=['GET'])
|
|
@login_required
|
|
def edit_folder_modal(folder_id):
|
|
try:
|
|
# Find the folder by ID 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
|
|
|
|
# Return the edit folder modal with folder data
|
|
response = make_response(render_template('partials/folder_modal.html', folder=folder))
|
|
response.headers['HX-Trigger'] = 'open-modal'
|
|
return response
|
|
|
|
except Exception as e:
|
|
# Print unhandled exceptions to the console as required
|
|
logging.exception("Error getting folder for edit: %s", e)
|
|
return jsonify({'error': 'Error retrieving folder'}), 500
|
|
|
|
@main.route('/api/folders/<folder_id>', methods=['PUT'])
|
|
@login_required
|
|
def update_folder(folder_id):
|
|
try:
|
|
# Find the folder by ID 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:
|
|
# Folder not found
|
|
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
|
return render_template('partials/folders_list.html', folders=folders)
|
|
|
|
# Get form data
|
|
name = request.form.get('name')
|
|
rule_text = request.form.get('rule_text')
|
|
priority = request.form.get('priority')
|
|
|
|
# Server-side validation
|
|
errors = {}
|
|
if not name or not name.strip():
|
|
errors['name'] = 'Folder name is required'
|
|
elif len(name.strip()) < 3:
|
|
errors['name'] = 'Folder name must be at least 3 characters'
|
|
elif len(name.strip()) > 50:
|
|
errors['name'] = 'Folder name must be less than 50 characters'
|
|
|
|
if not rule_text or not rule_text.strip():
|
|
errors['rule_text'] = 'Rule text is required'
|
|
elif len(rule_text.strip()) < 10:
|
|
errors['rule_text'] = 'Rule text must be at least 10 characters'
|
|
elif len(rule_text.strip()) > 200:
|
|
errors['rule_text'] = 'Rule text must be less than 200 characters'
|
|
|
|
# If there are validation errors, return the modal with errors
|
|
if errors:
|
|
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'
|
|
return response
|
|
|
|
# Update folder
|
|
folder.name = name.strip()
|
|
folder.rule_text = rule_text.strip()
|
|
folder.priority = int(priority) if priority else 0
|
|
|
|
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))
|
|
response.headers['HX-Trigger'] = 'close-modal'
|
|
return response
|
|
|
|
except Exception as e:
|
|
# Print unhandled exceptions to the console as required
|
|
logging.exception("Error updating folder: %s", e)
|
|
db.session.rollback()
|
|
# Return error in modal
|
|
errors = {'general': 'An unexpected error occurred. Please try again.'}
|
|
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'
|
|
return response
|
|
|
|
@main.route('/api/imap/config', methods=['GET'])
|
|
@login_required
|
|
def imap_config_modal():
|
|
"""Return the IMAP configuration modal."""
|
|
# Pass existing IMAP config to the template if it exists
|
|
response = make_response(render_template('partials/imap_config_modal.html',
|
|
server=current_user.imap_config.get('server') if current_user.imap_config else None,
|
|
port=current_user.imap_config.get('port') if current_user.imap_config else None,
|
|
username=current_user.imap_config.get('username') if current_user.imap_config else None,
|
|
password=current_user.imap_config.get('password') if current_user.imap_config else None,
|
|
use_ssl=current_user.imap_config.get('use_ssl', True) if current_user.imap_config else True))
|
|
response.headers['HX-Trigger'] = 'open-modal'
|
|
return response
|
|
|
|
@main.route('/api/imap/test', methods=['POST'])
|
|
@login_required
|
|
def test_imap_connection():
|
|
"""Test IMAP connection with provided configuration."""
|
|
try:
|
|
# Get form data
|
|
server = request.form.get('server')
|
|
port = request.form.get('port')
|
|
username = request.form.get('username')
|
|
password = request.form.get('password')
|
|
use_ssl = request.form.get('use_ssl') == 'on'
|
|
|
|
# Validate required fields
|
|
errors = {}
|
|
if not server:
|
|
errors['server'] = 'Server is required'
|
|
if not port:
|
|
errors['port'] = 'Port is required'
|
|
elif not port.isdigit():
|
|
errors['port'] = 'Port must be a number'
|
|
if not username:
|
|
errors['username'] = 'Username is required'
|
|
if not password:
|
|
errors['password'] = 'Password is required'
|
|
|
|
if errors:
|
|
response = make_response(render_template('partials/imap_config_modal.html', errors=errors, server=server, port=port, username=username, use_ssl=use_ssl))
|
|
response.headers['HX-Retarget'] = '#imap-modal'
|
|
response.headers['HX-Reswap'] = 'outerHTML'
|
|
return response
|
|
|
|
# Store configuration temporarily for testing
|
|
test_config = {
|
|
'server': server,
|
|
'port': int(port),
|
|
'username': username,
|
|
'password': password,
|
|
'use_ssl': False,
|
|
'use_tls': False,
|
|
'connection_timeout': 30
|
|
}
|
|
|
|
# Test connection
|
|
temp_user = type('User', (), {'imap_config': test_config})()
|
|
imap_service = IMAPService(temp_user)
|
|
print(temp_user, test_config)
|
|
success, message = imap_service.test_connection()
|
|
|
|
if success:
|
|
# Save configuration to user's profile
|
|
current_user.imap_config = test_config
|
|
db.session.commit()
|
|
|
|
response = make_response(render_template('partials/imap_config_modal.html',
|
|
success=True, message=message))
|
|
response.headers['HX-Retarget'] = '#imap-modal'
|
|
response.headers['HX-Reswap'] = 'outerHTML'
|
|
else:
|
|
print(message)
|
|
response = make_response(render_template('partials/imap_config_modal.html',
|
|
errors={'general': message}, server=server, port=port, username=username, use_ssl=use_ssl))
|
|
response.headers['HX-Retarget'] = '#imap-modal'
|
|
response.headers['HX-Reswap'] = 'outerHTML'
|
|
|
|
return response
|
|
|
|
except Exception as e:
|
|
logging.exception("Error testing IMAP connection: %s", e)
|
|
print(e)
|
|
errors = {'general': 'An unexpected error occurred. Please try again.'}
|
|
response = make_response(render_template('partials/imap_config_modal.html', errors=errors, server=server, port=port, username=username, use_ssl=use_ssl))
|
|
response.headers['HX-Retarget'] = '#imap-modal'
|
|
response.headers['HX-Reswap'] = 'outerHTML'
|
|
return response
|
|
|
|
@main.route('/api/imap/sync', methods=['POST'])
|
|
@login_required
|
|
def sync_imap_folders():
|
|
"""Sync folders from IMAP server."""
|
|
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
|
|
|
|
except Exception as e:
|
|
logging.exception("Error syncing IMAP folders: %s", e)
|
|
print(e)
|
|
|
|
@main.route('/api/folders', methods=['GET'])
|
|
@login_required
|
|
def get_folders():
|
|
"""Get folders with optional filtering."""
|
|
# Get filter parameter from query string
|
|
filter_type = request.args.get('filter', 'all')
|
|
|
|
# Get folders for the current authenticated user
|
|
if 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':
|
|
# Get normal priority folders (priority = 0 or not set)
|
|
folders = Folder.query.filter_by(user_id=current_user.id).filter(Folder.priority != 1).all()
|
|
else:
|
|
# Get all folders
|
|
folders = Folder.query.filter_by(user_id=current_user.id).all()
|
|
|
|
return render_template('partials/folders_list.html', folders=folders)
|