diff --git a/src/cli/sync.py b/src/cli/sync.py index e139479..6016d52 100644 --- a/src/cli/sync.py +++ b/src/cli/sync.py @@ -10,6 +10,8 @@ from src.utils.calendar_utils import save_events_to_vdir, save_events_to_file from src.services.microsoft_graph.calendar import ( fetch_calendar_events, sync_local_calendar_changes, + get_last_sync_time, + detect_deleted_events, ) from src.services.microsoft_graph.mail import ( fetch_mail_async, @@ -420,6 +422,72 @@ def sync( ) +def check_calendar_changes(vdir_path, org): + """ + Check if there are local calendar changes that need syncing. + + Args: + vdir_path (str): Base vdir path + org (str): Organization name + + Returns: + tuple: (has_changes, change_description) + """ + if not vdir_path: + return False, "No vdir path configured" + + org_vdir_path = os.path.join(os.path.expanduser(vdir_path), org) + + if not os.path.exists(org_vdir_path): + return False, "Calendar directory does not exist" + + try: + # Get last sync time + last_sync_time = get_last_sync_time(org_vdir_path) + + # Check if vdir directory has been modified since last sync + vdir_mtime = os.path.getmtime(org_vdir_path) + + if vdir_mtime > last_sync_time: + # Check for specific types of changes + deleted_events = detect_deleted_events(org_vdir_path) + + # Count .ics files to detect new events + import glob + + ics_files = glob.glob(os.path.join(org_vdir_path, "*.ics")) + + # Load previous state to compare + state_file = os.path.join(org_vdir_path, ".sync_state.json") + previous_state = {} + if os.path.exists(state_file): + try: + import json + + with open(state_file, "r") as f: + previous_state = json.load(f) + except Exception: + pass + + new_event_count = len(ics_files) - len(previous_state) + len(deleted_events) + + if deleted_events or new_event_count > 0: + changes = [] + if new_event_count > 0: + changes.append(f"{new_event_count} new events") + if deleted_events: + changes.append(f"{len(deleted_events)} deleted events") + + return True, ", ".join(changes) + else: + return True, "directory modified" + + return False, "no changes detected" + + except Exception as e: + return False, f"error checking calendar: {str(e)}" + + async def daemon_mode( dry_run, vdir, @@ -432,7 +500,7 @@ async def daemon_mode( two_way_calendar, ): """ - Run the script in daemon mode, periodically syncing emails. + Run the script in daemon mode, periodically syncing emails and calendar. """ from src.services.microsoft_graph.mail import get_inbox_count_async from rich.console import Console @@ -455,7 +523,10 @@ async def daemon_mode( content.append(status_text, style=status_color) return Panel( - content, title="📧 Email Sync Daemon", border_style="blue", padding=(0, 1) + content, + title="📧 Email & Calendar Sync Daemon", + border_style="blue", + padding=(0, 1), ) # Initial display @@ -486,10 +557,10 @@ async def daemon_mode( else: # Show checking status console.clear() - console.print(create_status_display("Checking for new messages...", "cyan")) + console.print(create_status_display("Checking for changes...", "cyan")) try: - # Authenticate and get access token + # Authenticate and get access token for mail check scopes = ["https://graph.microsoft.com/Mail.Read"] access_token, headers = get_access_token(scopes) remote_message_count = await get_inbox_count_async(headers) @@ -509,14 +580,41 @@ async def daemon_mode( [f for f in os.listdir(cur_dir) if ".eml" in f] ) - if remote_message_count != local_message_count: + mail_changes = remote_message_count != local_message_count + + # Check for calendar changes if two-way sync is enabled + calendar_changes = False + calendar_change_desc = "" + if two_way_calendar and vdir: + calendar_changes, calendar_change_desc = check_calendar_changes( + vdir, org + ) + + # Determine what changed and show appropriate status + if mail_changes and calendar_changes: + console.print( + create_status_display( + f"Changes detected! Mail: Remote {remote_message_count}, Local {local_message_count} | Calendar: {calendar_change_desc}. Starting sync...", + "yellow", + ) + ) + elif mail_changes: console.print( create_status_display( f"New messages detected! Remote: {remote_message_count}, Local: {local_message_count}. Starting sync...", "yellow", ) ) + elif calendar_changes: + console.print( + create_status_display( + f"Calendar changes detected! {calendar_change_desc}. Starting sync...", + "yellow", + ) + ) + # Sync if any changes detected + if mail_changes or calendar_changes: await _sync_outlook_data( dry_run, vdir, @@ -531,9 +629,15 @@ async def daemon_mode( last_sync_time = time.time() console.print(create_status_display("Sync completed ✅", "green")) else: + status_parts = [ + f"Mail: Remote {remote_message_count}, Local {local_message_count}" + ] + if two_way_calendar: + status_parts.append(f"Calendar: {calendar_change_desc}") + console.print( create_status_display( - f"No new messages (Remote: {remote_message_count}, Local: {local_message_count})", + f"No changes detected ({', '.join(status_parts)})", "green", ) )