import os import msal import requests import json from datetime import datetime from dateutil import parser from dateutil.tz import UTC # Read Azure app credentials from environment variables client_id = os.getenv('AZURE_CLIENT_ID') tenant_id = os.getenv('AZURE_TENANT_ID') if not client_id or not tenant_id: raise ValueError("Please set the AZURE_CLIENT_ID and AZURE_TENANT_ID environment variables.") # Token cache cache = msal.SerializableTokenCache() cache_file = 'token_cache.bin' if os.path.exists(cache_file): cache.deserialize(open(cache_file, 'r').read()) # Authentication authority = f'https://login.microsoftonline.com/{tenant_id}' scopes = ['https://graph.microsoft.com/Calendars.Read'] app = msal.PublicClientApplication(client_id, authority=authority, token_cache=cache) accounts = app.get_accounts() if accounts: token_response = app.acquire_token_silent(scopes, account=accounts[0]) else: flow = app.initiate_device_flow(scopes=scopes) if 'user_code' not in flow: raise Exception("Failed to create device flow") print(flow['message']) token_response = app.acquire_token_by_device_flow(flow) if 'access_token' not in token_response: raise Exception("Failed to acquire token") # Save token cache with open(cache_file, 'w') as f: f.write(cache.serialize()) access_token = token_response['access_token'] # Fetch events with pagination and expand recurring events headers = {'Authorization': f'Bearer {access_token}'} events_url = 'https://graph.microsoft.com/v1.0/me/events?$top=100&$expand=instances' events = [] print("Fetching events...") while events_url: response = requests.get(events_url, headers=headers) response_data = response.json() events.extend(response_data.get('value', [])) print(f"Fetched {len(events)} events so far...", end='\r') events_url = response_data.get('@odata.nextLink') # Save events to a file in iCalendar format output_file = f'output_ics/outlook_events_latest.ics' print(f"Saving events to {output_file}...") with open(output_file, 'w') as f: f.write("BEGIN:VCALENDAR\nVERSION:2.0\n") for event in events: if 'start' in event and 'end' in event: start = parser.isoparse(event['start']['dateTime']) end = parser.isoparse(event['end']['dateTime']) f.write(f"BEGIN:VEVENT\nSUMMARY:{event['subject']}\n") f.write(f"DTSTART:{start.strftime('%Y%m%dT%H%M%S')}\n") f.write(f"DTEND:{end.strftime('%Y%m%dT%H%M%S')}\n") if 'recurrence' in event and event['recurrence']: # Check if 'recurrence' exists and is not None for rule in event['recurrence']: if rule.startswith('RRULE'): rule_parts = rule.split(';') new_rule_parts = [] for part in rule_parts: if part.startswith('UNTIL='): until_value = part.split('=')[1] until_date = parser.isoparse(until_value) if start.tzinfo is not None and until_date.tzinfo is None: until_date = until_date.replace(tzinfo=UTC) new_rule_parts.append(f"UNTIL={until_date.strftime('%Y%m%dT%H%M%SZ')}") else: new_rule_parts.append(part) rule = ';'.join(new_rule_parts) f.write(f"{rule}\n") f.write("END:VEVENT\n") f.write("END:VCALENDAR\n")