203 lines
9.3 KiB
Python
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())
|