Files
luk/fetch_outlook.py
2025-05-12 17:19:34 -06:00

203 lines
9.3 KiB
Python

"""
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())