Files

316 lines
11 KiB
Python

#!/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()