#!/usr/bin/env python3 """ Simple CalDAV Calendar Tool for Google Calendar Works with Gmail app passwords - no OAuth needed! """ import sys import argparse from datetime import datetime, timedelta from pathlib import Path # This will be run with: uv run --with caldav cal.py def get_credentials(): """Get credentials from environment or .env file""" import os # Try to load from skills/imap-smtp-email/.env since we already have Gmail creds there env_file = Path(__file__).parent.parent / 'imap-smtp-email' / '.env' if env_file.exists(): for line in env_file.read_text().splitlines(): if line.strip() and not line.startswith('#') and '=' in line: key, _, value = line.partition('=') key = key.strip() value = value.strip() if key not in os.environ: os.environ[key] = value email = os.environ.get('IMAP_USER') or os.environ.get('SMTP_USER') password = os.environ.get('IMAP_PASS') or os.environ.get('SMTP_PASS') if not email or not password: print("Error: Email credentials not found. Set IMAP_USER and IMAP_PASS.", file=sys.stderr) sys.exit(1) return email, password def connect_caldav(): """Connect to Calendar via CalDAV (Google, iCloud, or Work)""" import caldav import os calendar_type = os.environ.get('CALENDAR_TYPE', 'google') if calendar_type == 'icloud': # iCloud CalDAV email = os.environ.get('CALENDAR_ICLOUD_ID', 'anthonym_au@icloud.com') password = os.environ.get('CALENDAR_ICLOUD_PASS', 'mvas-vwsk-ktiv-anex') url = "https://caldav.icloud.com/" print(f"Connecting to iCloud calendar for {email}...", file=sys.stderr) client = caldav.DAVClient(url=url, username=email, password=password) principal = client.principal() return principal elif calendar_type == 'work': # Work calendar (Pacific Energy M365) email = os.environ.get('CALENDAR_WORK_EMAIL', 'Anthony.martin@pacificenergy.com.au') password = os.environ.get('CALENDAR_WORK_PASS', 'RecOvery2026!') url = os.environ.get('CALENDAR_WORK_URL', 'https://outlook.office365.com/EWS/Exchange.asmx') if not all([email, password, url]): print("Error: Work calendar credentials not configured", file=sys.stderr) sys.exit(1) print(f"Connecting to work calendar ({email})...", file=sys.stderr) client = caldav.DAVClient(url=url, username=email, password=password) principal = client.principal() return principal else: # Google Calendar (default) email, password = get_credentials() url = f"https://calendar.google.com/calendar/dav/{email}/events/" print(f"Connecting to Google calendar ({email})...", file=sys.stderr) client = caldav.DAVClient(url=url, username=email, password=password) principal = client.principal() return principal def cmd_list(args): """List all calendars""" principal = connect_caldav() calendars = principal.calendars() if not calendars: print("No calendars found") return print("Available calendars:") for cal in calendars: print(f" {cal.name}") if args.verbose: print(f" URL: {cal.url}") print() def cmd_agenda(args): """Show upcoming events""" principal = connect_caldav() calendars = principal.calendars() # Time range start = datetime.now() if args.days: end = start + timedelta(days=int(args.days)) else: end = start + timedelta(days=7) print(f"Events from {start.strftime('%Y-%m-%d')} to {end.strftime('%Y-%m-%d')}:\n") for calendar in calendars: if args.calendar and args.calendar.lower() not in calendar.name.lower(): continue events = calendar.search(start=start, end=end, event=True, expand=True) if not events: continue print(f"📅 {calendar.name}") print("-" * 80) for event in events: try: vevent = event.icalendar_component summary = str(vevent.get('SUMMARY', 'No title')) dtstart = vevent.get('DTSTART') dtend = vevent.get('DTEND') location = vevent.get('LOCATION', '') description = vevent.get('DESCRIPTION', '') # Format datetime if hasattr(dtstart.dt, 'strftime'): start_str = dtstart.dt.strftime('%Y-%m-%d %H:%M') else: start_str = str(dtstart.dt) if hasattr(dtend.dt, 'strftime'): end_str = dtend.dt.strftime('%H:%M') else: end_str = str(dtend.dt) print(f"\n {summary}") print(f" When: {start_str} - {end_str}") if location: print(f" Where: {location}") if args.details and description: print(f" Details: {description[:200]}{'...' if len(str(description)) > 200 else ''}") except Exception as e: print(f" [Error parsing event: {e}]") print() def cmd_today(args): """Show today's events""" principal = connect_caldav() calendars = principal.calendars() start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) end = start + timedelta(days=1) print(f"Today's events ({start.strftime('%Y-%m-%d')}):\n") all_events = [] for calendar in calendars: if args.calendar and args.calendar.lower() not in calendar.name.lower(): continue events = calendar.search(start=start, end=end, event=True, expand=True) for event in events: try: vevent = event.icalendar_component summary = str(vevent.get('SUMMARY', 'No title')) dtstart = vevent.get('DTSTART') dtend = vevent.get('DTEND') location = vevent.get('LOCATION', '') all_events.append({ 'summary': summary, 'start': dtstart.dt, 'end': dtend.dt, 'location': location, 'calendar': calendar.name }) except: pass # Sort by start time all_events.sort(key=lambda x: x['start']) if not all_events: print("No events today") return for evt in all_events: if hasattr(evt['start'], 'strftime'): start_str = evt['start'].strftime('%H:%M') end_str = evt['end'].strftime('%H:%M') print(f" {start_str}-{end_str} {evt['summary']}") else: print(f" All day {evt['summary']}") if evt['location']: print(f" 📍 {evt['location']}") print(f" 📅 {evt['calendar']}") print() def cmd_create(args): """Create a new event""" from icalendar import Calendar, Event as ICalEvent from datetime import datetime import pytz principal = connect_caldav() calendars = principal.calendars() # Find calendar target_cal = None if args.calendar: for cal in calendars: if args.calendar.lower() in cal.name.lower(): target_cal = cal break else: # Use first calendar target_cal = calendars[0] if calendars else None if not target_cal: print(f"Error: Calendar '{args.calendar}' not found", file=sys.stderr) sys.exit(1) # Parse datetime try: start_dt = datetime.fromisoformat(args.start) end_dt = datetime.fromisoformat(args.end) except: print("Error: Invalid datetime format. Use YYYY-MM-DD HH:MM", file=sys.stderr) sys.exit(1) # Create event cal = Calendar() event = ICalEvent() event.add('summary', args.summary) event.add('dtstart', start_dt) event.add('dtend', end_dt) if args.location: event.add('location', args.location) if args.description: event.add('description', args.description) cal.add_component(event) # Save to calendar target_cal.save_event(cal.to_ical()) print(f"✅ Event created: {args.summary}") print(f" Calendar: {target_cal.name}") print(f" When: {start_dt} - {end_dt}") def main(): parser = argparse.ArgumentParser(description='Simple CalDAV Calendar Tool') subparsers = parser.add_subparsers(dest='command', help='Command') # list list_parser = subparsers.add_parser('list', help='List all calendars') list_parser.add_argument('-v', '--verbose', action='store_true', help='Show URLs') # agenda agenda_parser = subparsers.add_parser('agenda', help='Show upcoming events') agenda_parser.add_argument('--days', default='7', help='Days ahead (default: 7)') agenda_parser.add_argument('--calendar', help='Filter by calendar name') agenda_parser.add_argument('--details', action='store_true', help='Show descriptions') # today today_parser = subparsers.add_parser('today', help='Show today\'s events') today_parser.add_argument('--calendar', help='Filter by calendar name') # create create_parser = subparsers.add_parser('create', help='Create new event') create_parser.add_argument('summary', help='Event title') create_parser.add_argument('start', help='Start time (YYYY-MM-DD HH:MM)') create_parser.add_argument('end', help='End time (YYYY-MM-DD HH:MM)') create_parser.add_argument('--calendar', help='Calendar name') create_parser.add_argument('--location', help='Location') create_parser.add_argument('--description', help='Description') args = parser.parse_args() if not args.command: parser.print_help() sys.exit(1) try: if args.command == 'list': cmd_list(args) elif args.command == 'agenda': cmd_agenda(args) elif args.command == 'today': cmd_today(args) elif args.command == 'create': cmd_create(args) except Exception as e: print(f"Error: {e}", file=sys.stderr) if '--verbose' in sys.argv or '-v' in sys.argv: import traceback traceback.print_exc() sys.exit(1) if __name__ == '__main__': main()