diff --git a/.roo/rules/01-general.md b/.roo/rules/01-general.md
index e45dcbc..74c8936 100644
--- a/.roo/rules/01-general.md
+++ b/.roo/rules/01-general.md
@@ -22,3 +22,12 @@ Here are special rules you must follow:
5. When validation is done outside of a modal, it should cause a notification banner with the details.
6. Testing is done with pytest.
7. Testing is done with beautifulsoup4
+8. Only use comments where necessary. Prefer self-documenting code. For example:
+```
+is_adult = age >= 18
+```
+is preferred, and this is less preferred:
+```
+# check if the user is an adult
+x = age >= 18
+```
diff --git a/QWEN.md b/QWEN.md
index e45dcbc..74c8936 100644
--- a/QWEN.md
+++ b/QWEN.md
@@ -22,3 +22,12 @@ Here are special rules you must follow:
5. When validation is done outside of a modal, it should cause a notification banner with the details.
6. Testing is done with pytest.
7. Testing is done with beautifulsoup4
+8. Only use comments where necessary. Prefer self-documenting code. For example:
+```
+is_adult = age >= 18
+```
+is preferred, and this is less preferred:
+```
+# check if the user is an adult
+x = age >= 18
+```
diff --git a/app/imap_service.py b/app/imap_service.py
new file mode 100644
index 0000000..bb124b5
--- /dev/null
+++ b/app/imap_service.py
@@ -0,0 +1,167 @@
+import imaplib
+import ssl
+import logging
+from typing import List, Dict, Optional, Tuple
+from email.header import decode_header
+from app.models import db, Folder, User
+from app import create_app
+
+class IMAPService:
+ def __init__(self, user: User):
+ self.user = user
+ self.config = user.imap_config or {}
+ self.connection = None
+
+ def test_connection(self) -> Tuple[bool, str]:
+ """Test IMAP connection with current configuration."""
+ try:
+ if not self.config:
+ return False, "No IMAP configuration found"
+
+ # Create SSL context
+ context = ssl.create_default_context()
+
+ # Connect to IMAP server
+ self.connection = imaplib.IMAP4_SSL(
+ self.config.get('server', 'imap.gmail.com'),
+ self.config.get('port', 993)
+ )
+
+ # Login
+ self.connection.login(
+ self.config.get('username', ''),
+ self.config.get('password', '')
+ )
+
+ # Select inbox to verify connection
+ self.connection.select('INBOX')
+
+ # Close connection
+ self.connection.close()
+ self.connection.logout()
+
+ return True, "Connection successful"
+
+ except imaplib.IMAP4.error as e:
+ return False, f"IMAP connection error: {str(e)}"
+ except Exception as e:
+ return False, f"Connection error: {str(e)}"
+ finally:
+ if self.connection:
+ try:
+ self.connection.logout()
+ except:
+ pass
+
+ def get_folders(self) -> List[Dict[str, str]]:
+ """Get list of folders from IMAP server."""
+ try:
+ if not self.config:
+ return []
+
+ # Create SSL context
+ context = ssl.create_default_context()
+
+ # Connect to IMAP server
+ self.connection = imaplib.IMAP4_SSL(
+ self.config.get('server', 'imap.gmail.com'),
+ self.config.get('port', 993)
+ )
+
+ # Login
+ self.connection.login(
+ self.config.get('username', ''),
+ self.config.get('password', '')
+ )
+
+ # List folders
+ status, folder_data = self.connection.list()
+
+ if status != 'OK':
+ return []
+
+ folders = []
+ for folder_item in folder_data:
+ if isinstance(folder_item, bytes):
+ folder_item = folder_item.decode('utf-8')
+
+ # Parse folder name (handle different IMAP server formats)
+ parts = folder_item.split('"')
+ if len(parts) >= 3:
+ folder_name = parts[-1] if parts[-1] else parts[-2]
+ else:
+ folder_name = folder_item.split()[-1]
+
+ # Handle nested folders (convert to slash notation)
+ if folder_name.startswith('"') and folder_name.endswith('"'):
+ folder_name = folder_name[1:-1]
+
+ folders.append({
+ 'name': folder_name,
+ 'full_path': folder_name
+ })
+
+ # Close connection
+ self.connection.close()
+ self.connection.logout()
+
+ return folders
+
+ except Exception as e:
+ logging.error(f"Error fetching IMAP folders: {str(e)}")
+ return []
+ finally:
+ if self.connection:
+ try:
+ self.connection.logout()
+ except:
+ pass
+
+ def sync_folders(self) -> Tuple[bool, str]:
+ """Sync IMAP folders with local database."""
+ try:
+ if not self.config:
+ return False, "No IMAP configuration found"
+
+ # Get folders from IMAP server
+ imap_folders = self.get_folders()
+
+ if not imap_folders:
+ return False, "No folders found on IMAP server"
+
+ # Process each folder
+ synced_count = 0
+ for imap_folder in imap_folders:
+ folder_name = imap_folder['name']
+
+ # Skip special folders that might not be needed
+ if folder_name.lower() in ['inbox', 'sent', 'drafts', 'spam', 'trash']:
+ continue
+
+ # 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=self.user.id,
+ name=display_name
+ ).first()
+
+ if not existing_folder:
+ # Create new folder
+ new_folder = Folder(
+ user_id=self.user.id,
+ name=display_name,
+ rule_text=f"Auto-synced from IMAP folder: {folder_name}",
+ priority=0 # Default priority
+ )
+ db.session.add(new_folder)
+ synced_count += 1
+
+ db.session.commit()
+ return True, f"Successfully synced {synced_count} folders"
+
+ except Exception as e:
+ db.session.rollback()
+ return False, f"Sync error: {str(e)}"
\ No newline at end of file
diff --git a/app/routes.py b/app/routes.py
index b79a617..de74468 100644
--- a/app/routes.py
+++ b/app/routes.py
@@ -2,6 +2,7 @@ from flask import Blueprint, render_template, request, jsonify, make_response, f
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
@@ -174,7 +175,7 @@ def update_folder(folder_id):
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()
@@ -198,4 +199,107 @@ def update_folder(folder_id):
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
\ No newline at end of file
+ return response
+
+@main.route('/api/imap/config', methods=['GET'])
+@login_required
+def imap_config_modal():
+ """Return the IMAP configuration modal."""
+ response = make_response(render_template('partials/imap_config_modal.html'))
+ 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))
+ 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': use_ssl,
+ 'use_tls': False,
+ 'connection_timeout': 30
+ }
+
+ # Test connection
+ temp_user = type('User', (), {'imap_config': test_config})()
+ imap_service = IMAPService(temp_user)
+ 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:
+ response = make_response(render_template('partials/imap_config_modal.html',
+ errors={'general': message}))
+ 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)
+ errors = {'general': 'An unexpected error occurred. Please try again.'}
+ response = make_response(render_template('partials/imap_config_modal.html', errors=errors))
+ 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)
+ return jsonify({'error': 'An unexpected error occurred. Please try again.'}), 500
\ No newline at end of file
diff --git a/app/templates/partials/folders_list.html b/app/templates/partials/folders_list.html
index 51bf63d..7170c8e 100644
--- a/app/templates/partials/folders_list.html
+++ b/app/templates/partials/folders_list.html
@@ -3,7 +3,7 @@
-
{{ folder.name }}
+
{{ folder.name }}
\ No newline at end of file
diff --git a/app/templates/partials/sidebar.html b/app/templates/partials/sidebar.html
index d9313bb..6347726 100644
--- a/app/templates/partials/sidebar.html
+++ b/app/templates/partials/sidebar.html
@@ -14,9 +14,9 @@