add vdir sync feature

This commit is contained in:
Tim Bendt
2025-07-15 23:39:53 -04:00
parent df4c49c3ef
commit 1f306fffd7
9 changed files with 1212 additions and 521 deletions

View File

@@ -7,7 +7,10 @@ from datetime import datetime, timedelta
from src.utils.mail_utils.helpers import ensure_directory_exists
from src.utils.calendar_utils import save_events_to_vdir, save_events_to_file
from src.services.microsoft_graph.calendar import fetch_calendar_events
from src.services.microsoft_graph.calendar import (
fetch_calendar_events,
sync_local_calendar_changes,
)
from src.services.microsoft_graph.mail import (
fetch_mail_async,
archive_mail_async,
@@ -214,6 +217,7 @@ async def _sync_outlook_data(
days_forward,
continue_iteration,
download_attachments,
two_way_calendar,
):
"""Synchronize data from external sources."""
@@ -243,14 +247,30 @@ async def _sync_outlook_data(
with progress:
task_fetch = progress.add_task("[green]Syncing Inbox...", total=0)
task_calendar = progress.add_task("[cyan]Fetching calendar...", total=0)
task_local_calendar = progress.add_task(
"[magenta]Syncing local calendar...", total=0
)
task_read = progress.add_task("[blue]Marking as read...", total=0)
task_archive = progress.add_task("[yellow]Archiving mail...", total=0)
task_delete = progress.add_task("[red]Deleting mail...", total=0)
# Stage 1: Synchronize local changes (read, archive, delete) to the server
# Stage 1: Synchronize local changes (read, archive, delete, calendar) to the server
progress.console.print(
"[bold cyan]Step 1: Syncing local changes to server...[/bold cyan]"
)
# Handle calendar sync first (if vdir is specified and two-way sync is enabled)
calendar_sync_results = (0, 0)
if vdir and two_way_calendar:
org_vdir_path = os.path.join(os.path.expanduser(vdir), org)
progress.console.print(
f"[magenta]Checking for local calendar changes in {org_vdir_path}...[/magenta]"
)
calendar_sync_results = await sync_local_calendar_changes(
headers, org_vdir_path, progress, task_local_calendar, dry_run
)
# Handle mail changes in parallel
await asyncio.gather(
synchronize_maildir_async(
maildir_path, headers, progress, task_read, dry_run
@@ -260,6 +280,17 @@ async def _sync_outlook_data(
)
progress.console.print("[bold green]Step 1: Local changes synced.[/bold green]")
# Report calendar sync results
created, deleted = calendar_sync_results
if two_way_calendar and (created > 0 or deleted > 0):
progress.console.print(
f"[magenta]📅 Two-way calendar sync: {created} events created, {deleted} events deleted[/magenta]"
)
elif two_way_calendar:
progress.console.print(
"[magenta]📅 Two-way calendar sync: No local changes detected[/magenta]"
)
# Stage 2: Fetch new data from the server
progress.console.print(
"\n[bold cyan]Step 2: Fetching new data from server...[/bold cyan]"
@@ -335,6 +366,12 @@ async def _sync_outlook_data(
help="Download email attachments",
default=False,
)
@click.option(
"--two-way-calendar",
is_flag=True,
help="Enable two-way calendar sync (sync local changes to server)",
default=False,
)
@click.option(
"--daemon",
is_flag=True,
@@ -350,6 +387,7 @@ def sync(
days_forward,
continue_iteration,
download_attachments,
two_way_calendar,
daemon,
):
if daemon:
@@ -363,6 +401,7 @@ def sync(
days_forward,
continue_iteration,
download_attachments,
two_way_calendar,
)
)
else:
@@ -376,6 +415,7 @@ def sync(
days_forward,
continue_iteration,
download_attachments,
two_way_calendar,
)
)
@@ -389,20 +429,44 @@ async def daemon_mode(
days_forward,
continue_iteration,
download_attachments,
two_way_calendar,
):
"""
Run the script in daemon mode, periodically syncing emails.
"""
from src.services.microsoft_graph.mail import get_inbox_count_async
from rich.console import Console
from rich.live import Live
from rich.panel import Panel
from rich.text import Text
from datetime import datetime
import time
console = Console()
sync_interval = 300 # 5 minutes
check_interval = 10 # 10 seconds
last_sync_time = time.time() - sync_interval # Force initial sync
def create_status_display(status_text, status_color="cyan"):
"""Create a status panel for daemon mode."""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
content = Text()
content.append(f"[{timestamp}] ", style="dim")
content.append(status_text, style=status_color)
return Panel(
content, title="📧 Email Sync Daemon", border_style="blue", padding=(0, 1)
)
# Initial display
console.print(create_status_display("Starting daemon mode...", "green"))
while True:
if time.time() - last_sync_time >= sync_interval:
click.echo("[green]Performing full sync...[/green]")
# Show full sync status
console.clear()
console.print(create_status_display("Performing full sync...", "green"))
# Perform a full sync
await _sync_outlook_data(
dry_run,
@@ -413,45 +477,69 @@ async def daemon_mode(
days_forward,
continue_iteration,
download_attachments,
two_way_calendar,
)
last_sync_time = time.time()
# Show completion
console.print(create_status_display("Full sync completed ✅", "green"))
else:
# Perform a quick check
click.echo("[cyan]Checking for new messages...[/cyan]")
# Authenticate and get access token
scopes = ["https://graph.microsoft.com/Mail.Read"]
access_token, headers = get_access_token(scopes)
remote_message_count = await get_inbox_count_async(headers)
maildir_path = os.path.expanduser(f"~/Mail/{org}")
local_message_count = len(
[
f
for f in os.listdir(os.path.join(maildir_path, "new"))
if ".eml" in f
]
) + len(
[
f
for f in os.listdir(os.path.join(maildir_path, "cur"))
if ".eml" in f
]
)
if remote_message_count != local_message_count:
click.echo(
f"[yellow]New messages detected ({remote_message_count} / {local_message_count}), performing full sync...[/yellow]"
# Show checking status
console.clear()
console.print(create_status_display("Checking for new messages...", "cyan"))
try:
# Authenticate and get access token
scopes = ["https://graph.microsoft.com/Mail.Read"]
access_token, headers = get_access_token(scopes)
remote_message_count = await get_inbox_count_async(headers)
maildir_path = os.path.expanduser(f"~/Mail/{org}")
# Count local messages
new_dir = os.path.join(maildir_path, "new")
cur_dir = os.path.join(maildir_path, "cur")
local_message_count = 0
if os.path.exists(new_dir):
local_message_count += len(
[f for f in os.listdir(new_dir) if ".eml" in f]
)
if os.path.exists(cur_dir):
local_message_count += len(
[f for f in os.listdir(cur_dir) if ".eml" in f]
)
if remote_message_count != local_message_count:
console.print(
create_status_display(
f"New messages detected! Remote: {remote_message_count}, Local: {local_message_count}. Starting sync...",
"yellow",
)
)
await _sync_outlook_data(
dry_run,
vdir,
icsfile,
org,
days_back,
days_forward,
continue_iteration,
download_attachments,
two_way_calendar,
)
last_sync_time = time.time()
console.print(create_status_display("Sync completed ✅", "green"))
else:
console.print(
create_status_display(
f"No new messages (Remote: {remote_message_count}, Local: {local_message_count})",
"green",
)
)
except Exception as e:
console.print(
create_status_display(f"Error during check: {str(e)}", "red")
)
await _sync_outlook_data(
dry_run,
vdir,
icsfile,
org,
days_back,
days_forward,
continue_iteration,
download_attachments,
)
last_sync_time = time.time()
else:
click.echo("[green]No new messages detected.[/green]")
time.sleep(check_interval)