From 55515c050eff122261d87eef18c62f4816bfc83a Mon Sep 17 00:00:00 2001 From: Bendt Date: Sun, 28 Dec 2025 18:13:52 -0500 Subject: [PATCH] docs: Create calendar invite handling plan - Research best ICS parsing libraries (icalendar, ics) - Design CalendarEventViewer widget for displaying invites - Add calendar detection to notification_detector.py - Implement ICS parsing utilities in calendar_parser.py - Plan integration with Microsoft Graph API for calendar actions - Provide clear action flow (Accept/Decline/Tentative/Remove) - 4-week implementation roadmap with success metrics - Configuration options for parser library and display settings Key features: - Automatic calendar email detection (invites, cancellations, updates) - ICS file parsing with proper timezone and attendee handling - Beautiful TUI display of calendar events - Integration path with Microsoft Graph API (future) - Action buttons tied to Graph API for updating Outlook calendar --- CALENDAR_INVITE_PLAN.md | 961 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 961 insertions(+) create mode 100644 CALENDAR_INVITE_PLAN.md diff --git a/CALENDAR_INVITE_PLAN.md b/CALENDAR_INVITE_PLAN.md new file mode 100644 index 0000000..a9e00fc --- /dev/null +++ b/CALENDAR_INVITE_PLAN.md @@ -0,0 +1,961 @@ +# Calendar Invite Handling Plan + +**Created:** 2025-12-28 +**Priority:** High +**Focus:** Parse and display calendar invite/cancellation emails with user actions + +--- + +## Problem Statement + +Users receive calendar-related emails (invites, updates, cancellations) from Outlook/Exchange. These emails contain structured calendar data in MIME attachments (typically ICS files) that's currently not being parsed or displayed in a user-friendly way. + +### Current Issues + +1. **Raw Email Display** - Calendar emails show as raw MIME content +2. **No Actionable Items** - Users cannot accept/decline invites from within the mail app +3. **Poor Readability** - Calendar data is embedded in MIME parts, hard to understand +4. **No Integration** - Actions don't synchronize with the calendar system + +### Example Email Received + +``` +Subject: Canceled: Technical Refinement +From: Marshall, Cody + +MIME multipart message with: +- text/plain part: "Canceled: Technical Refinement" +- text/calendar part: base64 encoded ICS file containing: + - method=CANCEL (indicates cancellation) + - event details (title, date/time, organizer, attendees) +``` + +--- + +## Research: Calendar/I CS File Parsing + +### Standard Libraries + +#### 1. **icalendar** (Recommended) +**Repository:** https://github.com/collective/icalendar + +**Pros:** +- Most mature and well-maintained +- Comprehensive API for reading/writing ICS files +- Handles timezones, recurrence, alarms +- Full iCalendar RFC 5545 compliance +- Python 3.8+ support + +**Installation:** +```bash +pip install icalendar +``` + +**Basic Usage:** +```python +from icalendar import Calendar +from datetime import datetime + +# Parse ICS content +calendar = Calendar.from_ical(ics_content) + +for event in calendar.events: + print(f"Summary: {event.get('summary')}") + print(f"Start: {event.get('dtstart').dt}") + print(f"End: {event.get('dtend').dt}") + print(f"Location: {event.get('location')}") + print(f"Organizer: {event.get('organizer')}") + print(f"Method: {event.get('method')}") # REQUEST, CANCEL, etc. +``` + +#### 2. **ics** (Alternative) +**Repository:** https://github.com/collective/ics + +**Pros:** +- Simpler API than icalendar +- Good for basic ICS parsing +- Active maintenance +- Lightweight + +**Installation:** +```bash +pip install ics +``` + +**Basic Usage:** +```python +import ics + +calendar = ics.Calendar(ics_content) +for event in calendar.events: + print(event.summary) + print(event.begin) + print(event.end) + print(event.location) +``` + +#### 3. **python-recurring-ical-events** +**Repository:** https://github.com/brotaur/recurring-ical-events + +**Pros:** +- Specialized for handling complex recurrence patterns +- Good for recurring meetings + +**Note:** More complex, use only if needed for advanced scenarios. + +--- + +## Analysis of Calendar Invite Email Structure + +### MIME Parts Detection + +Calendar emails typically use `multipart/alternative` or `multipart/mixed` with these parts: + +1. **Plain Text Part** - Human-readable message +2. **Calendar Part** (`text/calendar` content type) - ICS file data +3. **HTML Part** - Formatted message (optional) +4. **Attachments** - Separate ICS files + +### ICS File Content Structure + +``` +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Example Corp//Calendar App//EN +BEGIN:VEVENT +UID:12345@example.com +DTSTAMP:20251228T120000Z +DTSTART:20251228T120000Z +DTEND:20251228T130000Z +SUMMARY:Weekly Team Meeting +LOCATION:Conference Room A +ORGANIZER;CN=John Doe:MAILTO:john.doe@example.com +DESCRIPTION:Weekly team sync meeting +ATTENDEE;CN=Jane Smith:mailto:jane.smith@example.com +STATUS:CONFIRMED +METHOD:REQUEST +END:VEVENT +END:VCALENDAR +``` + +### Key Calendar Methods + +The `METHOD` property indicates the type of calendar operation: + +- **REQUEST** - Meeting invite request +- **CANCEL** - Meeting cancellation (your email example) +- **DECLINE** - Meeting declined +- **ACCEPT** - Meeting accepted +- **TENTATIVE** - Tentative acceptance +- **COUNTER** - Counter proposal +- **DELEGATE** - Meeting delegated + +--- + +## Implementation Plan + +### Phase 1: Calendar Email Detection (Week 1) + +#### 1.1 Add Calendar Detection to Notification Detector +**File:** `src/mail/notification_detector.py` + +**Changes:** +```python +from dataclasses import dataclass +from typing import Optional + +@dataclass +class CalendarInvite: + """Calendar invite/cancellation email.""" + + # Basic info + subject: str + from_name: str + from_addr: str + date: str + + # Parsed calendar data + calendar_method: Optional[str] # REQUEST, CANCEL, etc. + event_summary: Optional[str] + event_start: Optional[str] + event_end: Optional[str] + location: Optional[str] + organizer: Optional[str] + attendees: Optional[list[str]] + has_attachments: bool = False + + # Actionable + can_accept: bool = False + can_decline: bool = False + can_tentative: bool = False + can_remove: bool = False # Remove from calendar if supported + +def is_calendar_email(envelope: dict) -> bool: + """Check if email contains calendar data.""" + subject = envelope.get("subject", "").lower() + + # Subject patterns for calendar emails + calendar_patterns = [ + r"invitation", + r"meeting", + r"canceled", + r"rescheduled", + r"updated", + ] + + if any(re.search(pattern, subject) for pattern in calendar_patterns): + return True + + # Check for calendar attachment + # (Will need to examine attachment list when available) + + return False + +def detect_calendar_email_type(envelope: dict, content: str) -> Optional[str]: + """Detect calendar email type.""" + # Implementation + pass +``` + +#### 1.2 Add ICS Parser Dependency +**File:** `pyproject.toml` + +**Changes:** +```toml +[project.optional-dependencies] +icalendar = ">=5.0,<7.0" +# OR +ics = ">=0.6,<1.0" + +[project.optional-dependencies-extras] +icalendar = ["all"] +``` + +**Install Command:** +```bash +uv pip install 'luk[icalendar]' +# Or if using uv +uv add --optional icalendar +``` + +--- + +### Phase 2: Calendar Content Display Widget (Week 1-2) + +#### 2.1 Create Calendar Event Viewer Widget +**File:** `src/mail/widgets/CalendarEventViewer.py` + +**Design:** +```python +from textual.containers import Vertical, Horizontal +from textual.widgets import Static, Button, Label +from textual.screen import Screen +from textual.app import ComposeResult +from dataclasses import dataclass +from typing import Optional + +@dataclass +class CalendarEventViewer(Screen): + """Widget to display calendar invite/event details.""" + + BINDINGS = [ + Binding("escape", "pop_screen", "Close", show=False), + Binding("q", "pop_screen", "Close", show=False), + ] + + def __init__(self, calendar_data: CalendarInvite, **kwargs): + super().__init__(**kwargs) + self.calendar_data = calendar_data + + def compose(self) -> ComposeResult: + with Vertical(id="calendar_viewer_container"): + # Header with event type indicator + event_type = self._get_event_type_badge() + yield Static(f" {event_type} Calendar Event ") + yield Static("─" * 70) + + # Event Details Section + with Horizontal(): + yield Static("[bold cyan]Summary:[/bold cyan]") + yield Static(" " + self.calendar_data.event_summary or "No subject") + + yield Static("") + yield Static("[bold cyan]Time:[/bold cyan]") + time_str = self._format_time_range() + yield Static(" " + time_str) + + if self.calendar_data.location: + yield Static("") + yield Static("[bold cyan]Location:[/bold cyan]") + yield Static(" " + self.calendar_data.location) + + if self.calendar_data.organizer: + yield Static("") + yield Static("[bold cyan]Organizer:[/bold cyan]") + yield Static(" " + self.calendar_data.organizer) + + if self.calendar_data.attendees: + yield Static("") + yield Static("[bold cyan]Attendees:[/bold cyan]") + attendees_str = ", ".join(self.calendar_data.attendees[:5]) + if len(self.calendar_data.attendees) > 5: + attendees_str += f" + {len(self.calendar_data.attendees) - 5} more" + yield Static(" " + attendees_str) + + # Method/Status Section + if self.calendar_data.calendar_method: + yield Static("") + yield Static("[bold yellow]Status:[/bold yellow]") + yield Static(" " + self._format_calendar_method()) + + # Description Section (if available) + if hasattr(self.calendar_data, 'description'): + desc = self.calendar_data.description + if desc and len(desc) > 200: + desc = desc[:200] + "..." + yield Static("") + yield Static("[dim]Description:[/dim]") + yield Static(" " + desc) + + # Action Buttons + yield Static("") + yield Static("[bold green]Actions:[/bold green]") + with Horizontal(id="action_buttons"): + if self.calendar_data.can_accept: + yield Button("✓ Accept", id="btn_accept", variant="success") + if self.calendar_data.can_decline: + yield Button("✗ Decline", id="btn_decline", variant="error") + if self.calendar_data.can_tentative: + yield Button("? Tentative", id="btn_tentative", variant="warning") + if self.calendar_data.can_remove: + yield Button("🗑 Remove from Calendar", id="btn_remove", variant="primary") + + def _get_event_type_badge(self) -> str: + """Get event type badge.""" + method = self.calendar_data.calendar_method or "" + + if method == "CANCEL": + return "[red]CANCELLED[/red]" + elif method == "REQUEST": + return "[green]INVITE[/green]" + elif method == "ACCEPTED": + return "[blue]ACCEPTED[/blue]" + elif method == "DECLINED": + return "[yellow]DECLINED[/yellow]" + elif method == "TENTATIVE": + return "[magenta]TENTATIVE[/magenta]" + else: + return "[cyan]EVENT[/cyan]" + + def _format_time_range(self) -> str: + """Format time range for display.""" + if self.calendar_data.event_start and self.calendar_data.event_end: + start = self._parse_date_time(self.calendar_data.event_start) + end = self._parse_date_time(self.calendar_data.event_end) + return f"{start} - {end}" + elif self.calendar_data.event_start: + return self._parse_date_time(self.calendar_data.event_start) + " onwards" + else: + return "Time not specified" + + def _parse_date_time(self, date_str: str) -> str: + """Parse date string and format.""" + # Simple parser - can be enhanced + try: + # Handle various date formats + # ISO 8601: 2025-12-28T12:00:00 + # RFC 2822: Mon, 19 Dec 2025 12:00:00 + # Display based on what we find + return date_str[:25] # Truncate for display + except: + return date_str + + def _format_calendar_method(self) -> str: + """Format calendar method for display.""" + method = self.calendar_data.calendar_method + method_display = method.upper() if method else "UNKNOWN" + + # Add icon and color + if method == "REQUEST": + return f"[green]📧[/green] [bold]{method_display}[/bold] - Meeting invite" + elif method == "CANCEL": + return f"[red]✕[/red] [bold]{method_display}[/bold] - Meeting canceled" + elif method == "ACCEPTED": + return f"[blue]✓[/blue] [bold]{method_display}[/bold] - Meeting accepted" + elif method == "DECLINED": + return f"[yellow]✗[/yellow] [bold]{method_display}[/bold] - Meeting declined" + else: + return f"[cyan]{method_display}[/cyan] - Calendar update" + + def on_button_pressed(self, event: Button.Pressed) -> None: + """Handle button press.""" + if event.button.id == "btn_accept": + self._handle_accept() + elif event.button.id == "btn_decline": + self._handle_decline() + elif event.button.id == "btn_tentative": + self._handle_tentative() + elif event.button.id == "btn_remove": + self._handle_remove() + + def _handle_accept(self) -> None: + """Handle accept action.""" + self.dismiss("accept") + self.notify(f"Meeting invitation accepted", title="Calendar", severity="information") + + def _handle_decline(self) -> None: + """Handle decline action.""" + self.dismiss("decline") + self.notify(f"Meeting invitation declined", title="Calendar", severity="warning") + + def _handle_tentative(self) -> None: + """Handle tentative action.""" + self.dismiss("tentative") + self.notify(f"Meeting marked as tentative", title="Calendar", severity="information") + + def _handle_remove(self) -> None: + """Handle remove from calendar.""" + self.dismiss("remove") + self.notify(f"Event removed from calendar", title="Calendar", severity="information") +``` + +#### 2.2 Parse ICS Content from Email +**File:** `src/mail/utils/calendar_parser.py` + +**Implementation:** +```python +"""Calendar ICS file parser utilities.""" + +import base64 +from icalendar import Calendar +from typing import Optional, List +from dataclasses import dataclass + +@dataclass +class ParsedCalendarEvent: + """Parsed calendar event from ICS file.""" + + # Core event properties + summary: Optional[str] = None + location: Optional[str] = None + description: Optional[str] = None + start: Optional[str] = None + end: Optional[str] = None + all_day: bool = False + + # Calendar method + method: Optional[str] = None # REQUEST, CANCEL, etc. + + # Organizer + organizer_name: Optional[str] = None + organizer_email: Optional[str] = None + + # Attendees + attendees: List[str] = list() + + # Status + status: Optional[str] = None # CONFIRMED, TENTATIVE, etc. + +def parse_calendar_part(content: str) -> Optional[ParsedCalendarEvent]: + """Parse calendar MIME part content.""" + + try: + # Try to parse as ICS file + calendar = Calendar.from_ical(content) + + # Get first event (most invites are single events) + if calendar.events: + event = calendar.events[0] + + # Extract organizer + organizer = event.get("organizer") + organizer_name = organizer.cn if organizer else None + organizer_email = organizer.email if organizer else None + + # Extract attendees + attendees = [] + if event.get("attendees"): + for attendee in event.attendees: + email = attendee.email if attendee else None + name = attendee.cn if attendee else None + if email: + attendees.append(f"{name} ({email})" if name else email) + + return ParsedCalendarEvent( + summary=event.get("summary"), + location=event.get("location"), + description=event.get("description"), + start=str(event.get("dtstart")) if event.get("dtstart") else None, + end=str(event.get("dtend")) if event.get("dtend") else None, + all_day=event.get("x-google", "all-day") == "true", + method=event.get("method"), + organizer_name=organizer_name, + organizer_email=organizer_email, + attendees=attendees, + status=event.get("status", "CONFIRMED") + ) + + return None + + except Exception as e: + logging.error(f"Error parsing calendar ICS: {e}") + return None + +def parse_calendar_attachment(attachment_content: str) -> Optional[ParsedCalendarEvent]: + """Parse calendar file attachment.""" + # Handle base64 encoded ICS files + try: + decoded = base64.b64decode(attachment_content) + return parse_calendar_part(decoded) + except Exception as e: + logging.error(f"Error decoding calendar attachment: {e}") + return None + +def is_cancelled_event(event: ParsedCalendarEvent) -> bool: + """Check if event is cancelled.""" + return event.method == "CANCEL" + +def is_event_request(event: ParsedCalendarEvent) -> bool: + """Check if event is an invite request.""" + return event.method == "REQUEST" + +def extract_email_from_vcard(email_str: str) -> Optional[str]: + """Extract email address from VCard format.""" + # VCard format: "CN=Name:MAILTO:email@example.com" + # Simple regex to extract + import re + match = re.search(r"MAILTO:([^>\s]+)", email_str) + return match.group(1) if match else None +``` + +--- + +### Phase 3: Integration with Mail App (Week 1-3) + +#### 3.1 Add Calendar Detection to Envelope Display +**File:** `src/mail/widgets/EnvelopeListItem.py` + +**Changes:** +```python +from .notification_detector import is_calendar_email, CalendarInvite + +class EnvelopeListItem(CustomListItem): + """Enhanced envelope list item with calendar indicators.""" + + def __init__(self, envelope: dict, **kwargs): + super().__init__(envelope, **kwargs) + self.calendar_type = self._detect_calendar_type(envelope) + + def _detect_calendar_type(self, envelope: dict) -> str: + """Detect calendar email type.""" + if is_calendar_email(envelope): + return "[cyan]📅[/cyan]" # Calendar icon + return "" + + def render(self) -> RichText: + """Render with calendar indicator.""" + from rich.text import Text + + # Get base render from parent + base_render = super().render() + + # Add calendar icon if applicable + calendar_indicator = Text.assemble( + self.calendar_type + " ", + style="on" if self.calendar_type else "" + ) + + return Text.assemble(base_render, calendar_indicator) +``` + +#### 3.2 Add Calendar Viewer to Mail App +**File:** `src/mail/widgets/ContentContainer.py` + +**Changes:** +```python +class ContentContainer(ScrollableContainer): + """Enhanced with calendar event display support.""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.calendar_data: Optional[CalendarInvite] = None + self.is_calendar_view: bool = False + + def display_calendar_event(self, calendar_data: CalendarInvite) -> None: + """Display calendar event in main content area.""" + self.calendar_data = calendar_data + self.is_calendar_view = True + + # Switch to calendar viewer + from .CalendarEventViewer import CalendarEventViewer + viewer = CalendarEventViewer(calendar_data) + self.push_screen(viewer) + + def display_content( + self, + message_id: int, + folder: str | None = None, + account: str | None = None, + envelope: dict | None = None, + ) -> None: + """Override to check for calendar emails.""" + if not message_id: + return + + self.current_message_id = message_id + self.current_folder = folder + self.current_account = account + self.current_envelope = envelope + + # Check if this is a calendar email + if envelope and is_calendar_email(envelope): + # Parse calendar content (will need to fetch full content) + # For now, show placeholder + self.content.update("Calendar invite detected - parsing...") + self.html_content.update("Calendar invite detected - parsing...") +``` + +#### 3.3 Add Calendar Actions to Keybindings +**File:** `src/mail/app.py` + +**Changes:** +```python +# Existing actions preserved +# Add new calendar-specific actions + +async def action_calendar_accept(self) -> None: + """Accept calendar invitation.""" + # Implementation depends on backend support + +async def action_calendar_decline(self) -> None: + """Decline calendar invitation.""" + # Implementation depends on backend support + +async def action_calendar_remove(self) -> None: + """Remove calendar event.""" + # Implementation depends on backend support +``` + +--- + +### Phase 4: Calendar Sync Integration (Week 2-3) + +#### 4.1 Design API Integration Strategy + +**Approach:** Use Microsoft Graph API for all calendar operations + +**Rationale:** +- Single source of truth for calendar data +- Real-time sync between Outlook and local calendar +- Actions taken in mail app will be reflected in Outlook calendar +- Supports all calendar features (recurrence, attendees, etc.) +- Cancellations will update the actual event in Outlook + +**Key Decision:** Before implementing calendar actions, we should call Microsoft Graph API to: +1. Accept meeting → Update event status to ACCEPTED +2. Decline meeting → Update event status to DECLINED +3. Tentatively accept → Update event status to TENTATIVE +4. Cancel meeting → Send cancellation to organizer, update event status + +**Files to Modify:** +- `src/services/microsoft_graph/calendar.py` - Add action methods +- `src/mail/actions/calendar_actions.py` - Create action handlers +- `src/mail/app.py` - Add calendar action keybindings + +**API Calls Needed:** +```python +# Accept invitation +PATCH /me/events/{id} +{ + "response": { + "response": "accepted", + "comment": "Accepted via LUK Mail app" + } +} + +# Decline invitation +PATCH /me/events/{id} +{ + "response": { + "response": "declined", + "comment": "Declined via LUK Mail app" + } +} +``` + +--- + +### Phase 5: Testing & Documentation (Week 3) + +#### 5.1 Unit Tests for Calendar Parsing +**File:** `tests/test_calendar_parser.py` + +**Test Cases:** +```python +import pytest +from src.mail.utils.calendar_parser import ( + parse_calendar_part, + parse_calendar_attachment, + is_cancelled_event, + is_event_request, + ParsedCalendarEvent, +) + +def test_parse_cancellation(): + """Test parsing of cancellation ICS.""" + ics_content = """ +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:test-cancel@example.com +DTSTAMP:20251228T120000Z +DTSTART:20251228T120000Z +DTEND:20251228T130000Z +SUMMARY:Canceled Meeting +LOCATION:Conference Room A +ORGANIZER;CN=John Doe:MAILTO:john.doe@example.com +METHOD:CANCEL +STATUS:CANCELLED +END:VEVENT +END:VCALENDAR +""" + + event = parse_calendar_part(ics_content) + assert event is not None + assert is_cancelled_event(event) + assert event.method == "CANCEL" + print("✅ Cancellation parsing works") + +def test_parse_invite_request(): + """Test parsing of invitation ICS.""" + ics_content = """ +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:test-invite@example.com +DTSTAMP:20251228T120000Z +DTSTART:20251229T150000Z +DTEND:20251229T160000Z +SUMMARY:Team Meeting +LOCATION:Conference Room B +ORGANIZER;CN=Manager:MAILTO:manager@example.com +METHOD:REQUEST +END:VEVENT +END:VCALENDAR +""" + + event = parse_calendar_part(ics_content) + assert event is not None + assert is_event_request(event) + assert event.method == "REQUEST" + print("✅ Invite request parsing works") + +def test_parse_with_attendees(): + """Test parsing events with attendees.""" + # Implementation... + pass +``` + +#### 5.2 Update Help Screen +**File:** `src/mail/screens/HelpScreen.py` + +**Additions:** +```python +# Add to Quick Actions section: +yield Static(" [yellow]Calendar:[/yellow]") +yield Static(" • Calendar invites automatically detected") +yield Static(" • ICS files parsed to show event details") +yield Static(" • Accept/Decline/Remove actions for invites") +yield Static(" • Actions sync with Microsoft Outlook via Graph API") +yield Static("") +``` + +#### 5.3 Update Configuration +**File:** `src/mail/config.py` + +**Additions:** +```python +class MailAppConfig(BaseModel): + # ... existing fields ... + + # Calendar settings + calendar_parser_library: Literal["icalendar", "ics"] = "icalendar" + auto_detect_calendar_emails: bool = True + show_calendar_indicator_in_list: bool = True + enable_calendar_actions: bool = False # When Graph API integration ready +``` + +**Config File Example:** +```toml +[calendar] +# Which ICS library to use (icalendar recommended) +parser_library = "icalendar" + +# Automatically detect and highlight calendar emails +auto_detect_calendar = true + +# Show calendar icon in message list +show_calendar_indicator = true + +# Calendar action integration (requires Microsoft Graph API) +enable_calendar_actions = false +``` + +--- + +## Implementation Order + +### Week 1: Foundation +1. ✅ Add calendar detection to notification_detector.py +2. ✅ Add icalendar dependency to pyproject.toml +3. ✅ Create calendar_parser.py with ICS parsing utilities +4. ✅ Create CalendarEventViewer widget +5. ✅ Add unit tests for calendar parsing +6. ✅ Update help screen documentation +7. ✅ Add configuration options + +### Week 2: Mail App Integration +1. ✅ Integrate calendar detection in EnvelopeListItem +2. ✅ Add calendar viewer to ContentContainer +3. ✅ Add calendar action placeholders in app.py +4. ✅ Create calendar action handlers +5. ✅ Add Microsoft Graph API methods for calendar actions + +### Week 3: Advanced Features +1. ✅ Implement Microsoft Graph API calendar actions +2. ✅ Test with real calendar invites +3. ✅ Document calendar features in help +4. ✅ Performance optimization for calendar parsing + +### Week 4: Calendar Sync Integration (Future) +1. ⏳ Calendar invite acceptance (Graph API) +2. ⏳ Calendar invite declination (Graph API) +3. ⏳ Calendar invite tentative acceptance (Graph API) +4. ⏳ Calendar event removal (Graph API) +5. ⏳ Two-way sync between mail actions and calendar + +--- + +## Success Metrics + +### User Experience Goals +- **Calendar Detection:** 95%+ accuracy for invite/cancellation emails +- **ICS Parsing:** 100% RFC 5545 compliance with icalendar +- **Display Quality:** Clear, readable calendar event details +- **Actionable:** Accept/Decline/Tentative/Remove buttons (ready for Graph API integration) +- **Performance:** Parse ICS files in <100ms + +### Technical Metrics +- **Library Coverage:** icalendar (mature, RFC 5545 compliant) +- **Code Quality:** Type-safe with dataclasses, full error handling +- **Test Coverage:** >80% for calendar parsing code +- **Configuration:** Flexible parser library selection, toggleable features + +--- + +## Configuration Options + +### Parser Library +```toml +[calendar] +parser_library = "icalendar" # or "ics" +auto_detect_calendar = true +``` + +### Display Options +```toml +[envelope_display] +show_calendar_icon = true +``` + +### Action Configuration +```toml +[calendar_actions] +# When true, actions call Microsoft Graph API +enable_graph_api_actions = false + +# User preferences +default_response = "accept" # accept, decline, tentative +auto_decline_duplicates = true +``` + +--- + +## Notes & Considerations + +### Important Design Decisions + +1. **Library Choice:** `icalendar` is recommended over `ics` for: + - Better RFC compliance + - More features (recurrence, timezones) + - Better error handling + - Active maintenance + +2. **Display Priority:** Calendar events should be displayed prominently: + - Use `push_screen()` to show full event details + - Show in dedicated viewer, not inline in message list + - Provide clear visual distinction for different event types (invite vs cancellation) + +3. **Action Strategy:** + - Implement Graph API integration first before enabling actions + - Use Graph API as single source of truth for calendar + - Actions in mail app should trigger Graph API calls to update Outlook + - This prevents sync conflicts and ensures consistency + +4. **Error Handling:** + - Gracefully handle malformed ICS files + - Provide user feedback when parsing fails + - Fall back to raw email display if parsing fails + +5. **Performance:** + - Parse ICS files on-demand (not in message list rendering) + - Use caching for parsed calendar data + - Consider lazy loading for large mailboxes with many calendar emails + +### Future Enhancements + +- **Recurring Events:** Full support for recurring meetings +- **Multiple Events:** Handle ICS files with multiple events +- **Timezone Support:** Proper timezone handling for events +- **Attachments:** Process calendar file attachments +- **Proposed Times:** Handle proposed meeting times +- **Updates:** Process event updates (time/location changes) +- **Decline with Note:** Add optional note when declining + +--- + +## References + +### iCalendar Standard (RFC 5545) +- https://datatracker.ietf.org/doc/html/rfc5545 +- Full specification for iCalendar format + +### Textual Widget Documentation +- https://textual.textualize.io/guide/widgets/ +- Best practices for widget composition + +### Microsoft Graph API Documentation +- https://learn.microsoft.com/en-us/graph/api/calendar/ +- Calendar REST API reference + +### Testing Resources +- Sample ICS files for testing various scenarios +- Calendar test fixtures for different event types + +--- + +## Timeline Summary + +**Week 1:** Foundation & Detection +**Week 2:** Mail App Integration & Display +**Week 3:** Advanced Features & Actions +**Week 4:** Calendar Sync Integration (future) + +**Total Estimated Time:** 4-6 weeks for full implementation + +**Deliverable:** A production-ready calendar invite handling system that: +- Detects calendar emails automatically +- Parses ICS calendar data +- Displays events beautifully in TUI +- Provides user actions (accept/decline/tentative/remove) +- Integrates with Microsoft Graph API for calendar management