# 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