""" Fetch and synchronize emails and calendar events from Microsoft Outlook (Graph API). """ import os import argparse import asyncio from rich.panel import Panel from rich.progress import Progress, SpinnerColumn, MofNCompleteColumn # Import the refactored modules from apis.microsoft_graph.auth import get_access_token from apis.microsoft_graph.mail import fetch_mail_async, archive_mail_async, delete_mail_async, synchronize_maildir_async from apis.microsoft_graph.calendar import fetch_calendar_events from utils.calendar_utils import save_events_to_vdir, save_events_to_file from utils.mail_utils.helpers import ensure_directory_exists # Add argument parsing for dry-run mode arg_parser = argparse.ArgumentParser(description="Fetch and synchronize emails.") arg_parser.add_argument("--dry-run", action="store_true", help="Run in dry-run mode without making changes.", default=False) arg_parser.add_argument("--vdir", help="Output calendar events in vdir format to the specified directory (each event in its own file)", default=None) arg_parser.add_argument("--icsfile", help="Output calendar events into this ics file path.", default=None) arg_parser.add_argument("--org", help="Specify the organization name for the subfolder to store emails and calendar events", default="corteva") arg_parser.add_argument("--days-back", type=int, help="Number of days to look back for calendar events", default=1) arg_parser.add_argument("--days-forward", type=int, help="Number of days to look forward for calendar events", default=6) arg_parser.add_argument("--continue-iteration", action="store_true", help="Enable interactive mode to continue fetching more date ranges", default=False) arg_parser.add_argument("--download-attachments", action="store_true", help="Download email attachments", default=False) args = arg_parser.parse_args() # Parse command line arguments dry_run = args.dry_run vdir_path = args.vdir ics_path = args.icsfile org_name = args.org days_back = args.days_back days_forward = args.days_forward continue_iteration = args.continue_iteration download_attachments = args.download_attachments async def fetch_calendar_async(headers, progress, task_id): """ Fetch calendar events and save them in the appropriate format. Args: headers: Authentication headers for Microsoft Graph API progress: Progress instance for updating progress bars task_id: ID of the task in the progress bar Returns: List of event dictionaries Raises: Exception: If there's an error fetching or saving events """ from datetime import datetime, timedelta try: # Use the utility function to fetch calendar events progress.console.print("[cyan]Fetching events from Microsoft Graph API...[/cyan]") events, total_events = await fetch_calendar_events( headers=headers, days_back=days_back, days_forward=days_forward ) progress.console.print(f"[cyan]Got {len(events)} events from API (reported total: {total_events})[/cyan]") # Update progress bar with total events progress.update(task_id, total=total_events) # Save events to appropriate format if not dry_run: if vdir_path: # Create org-specific directory within vdir path org_vdir_path = os.path.join(vdir_path, org_name) progress.console.print(f"[cyan]Saving events to vdir: {org_vdir_path}[/cyan]") save_events_to_vdir(events, org_vdir_path, progress, task_id, dry_run) progress.console.print(f"[green]Finished saving events to vdir: {org_vdir_path}[/green]") elif ics_path: # Save to a single ICS file in the output_ics directory progress.console.print(f"[cyan]Saving events to ICS file: {ics_path}/events_latest.ics[/cyan]") save_events_to_file(events, f"{ics_path}/events_latest.ics", progress, task_id, dry_run) progress.console.print(f"[green]Finished saving events to ICS file[/green]") else: # No destination specified progress.console.print("[yellow]Warning: No destination path (--vdir or --icsfile) specified for calendar events.[/yellow]") else: progress.console.print(f"[DRY-RUN] Would save {len(events)} events to {'vdir format' if vdir_path else 'single ICS file'}") progress.update(task_id, advance=len(events)) # Interactive mode: Ask if the user wants to continue with the next date range if continue_iteration: # Move to the next date range next_start_date = datetime.now() - timedelta(days=days_back) next_end_date = next_start_date + timedelta(days=days_forward) progress.console.print(f"\nCurrent date range: {next_start_date.strftime('%Y-%m-%d')} to {next_end_date.strftime('%Y-%m-%d')}") user_response = input("\nContinue to iterate? [y/N]: ").strip().lower() while user_response == 'y': progress.console.print(f"\nFetching events for {next_start_date.strftime('%Y-%m-%d')} to {next_end_date.strftime('%Y-%m-%d')}...") # Reset the progress bar for the new fetch progress.update(task_id, completed=0, total=0) # Fetch events for the next date range next_events, next_total_events = await fetch_calendar_events( headers=headers, days_back=0, days_forward=days_forward, start_date=next_start_date, end_date=next_end_date ) # Update progress bar with total events progress.update(task_id, total=next_total_events) if not dry_run: if vdir_path: save_events_to_vdir(next_events, org_vdir_path, progress, task_id, dry_run) else: save_events_to_file(next_events, f'output_ics/outlook_events_{next_start_date.strftime("%Y%m%d")}.ics', progress, task_id, dry_run) else: progress.console.print(f"[DRY-RUN] Would save {len(next_events)} events to {'vdir format' if vdir_path else 'output_ics/outlook_events_' + next_start_date.strftime("%Y%m%d") + '.ics'}") progress.update(task_id, advance=len(next_events)) # Calculate the next date range next_start_date = next_end_date next_end_date = next_start_date + timedelta(days=days_forward) progress.console.print(f"\nNext date range would be: {next_start_date.strftime('%Y-%m-%d')} to {next_end_date.strftime('%Y-%m-%d')}") user_response = input("\nContinue to iterate? [y/N]: ").strip().lower() return events except Exception as e: progress.console.print(f"[red]Error fetching or saving calendar events: {str(e)}[/red]") import traceback progress.console.print(f"[red]{traceback.format_exc()}[/red]") progress.update(task_id, completed=True) return [] # Function to create Maildir structure def create_maildir_structure(base_path): """ Create the standard Maildir directory structure. Args: base_path (str): Base path for the Maildir. Returns: None """ ensure_directory_exists(os.path.join(base_path, 'cur')) ensure_directory_exists(os.path.join(base_path, 'new')) ensure_directory_exists(os.path.join(base_path, 'tmp')) ensure_directory_exists(os.path.join(base_path, '.Archives')) ensure_directory_exists(os.path.join(base_path, '.Trash', 'cur')) async def main(): """ Main function to run the script. Returns: None """ # Save emails to Maildir maildir_path = os.getenv('MAILDIR_PATH', os.path.expanduser('~/Mail')) + f"/{org_name}" attachments_dir = os.path.join(maildir_path, 'attachments') ensure_directory_exists(attachments_dir) create_maildir_structure(maildir_path) # Define scopes for Microsoft Graph API scopes = ['https://graph.microsoft.com/Calendars.Read', 'https://graph.microsoft.com/Mail.ReadWrite'] # Authenticate and get access token access_token, headers = get_access_token(scopes) # Set up the progress bars progress = Progress( SpinnerColumn(), MofNCompleteColumn(), *Progress.get_default_columns() ) with progress: task_fetch = progress.add_task("[green]Syncing Inbox...", total=0) task_calendar = progress.add_task("[cyan]Fetching 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) await asyncio.gather( synchronize_maildir_async(maildir_path, headers, progress, task_read, dry_run), archive_mail_async(maildir_path, headers, progress, task_archive, dry_run), delete_mail_async(maildir_path, headers, progress, task_delete, dry_run), fetch_mail_async(maildir_path, attachments_dir, headers, progress, task_fetch, dry_run, download_attachments), fetch_calendar_async(headers, progress, task_calendar) ) if __name__ == "__main__": asyncio.run(main())