Files
luk/CALENDAR_INVITE_PLAN.md
Bendt fc5c61ddd6 feat: Add calendar invite detection and handling foundation
- Create calendar_parser.py module with ICS parsing support
- Add test_calendar_parsing.py with unit tests for ICS files
- Create test ICS fixture with calendar invite example
- Add icalendar dependency to pyproject.toml
- Add calendar detection to notification_detector.py
- Research and document best practices for ICS parsing libraries
- 4-week implementation roadmap:
  - Week 1: Foundation (detection, parsing, basic display)
  - Week 2: Mail App Integration (viewer, actions)
  - Week 3: Advanced Features (Graph API sync)
  - Week 4: Calendar Sync Integration (two-way sync)

Key capabilities:
- Parse ICS calendar files (text/calendar content type)
- Extract event details (summary, attendees, method, status)
- Detect cancellation vs invite vs update vs request
- Display calendar events in TUI with beautiful formatting
- Accept/Decline/Tentative/Remove actions
- Integration path with Microsoft Graph API (future)

Testing:
- Unit tests for parsing cancellations and invites
- Test fixture with real Outlook calendar example
- All tests passing

This addresses your need for handling calendar invites like:
"CANCELED: Technical Refinement"
with proper detection, parsing, and display capabilities.
2025-12-28 22:02:50 -05:00

962 lines
29 KiB
Markdown

# 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 <john.marshall@corteva.com>
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 calendar detection to EnvelopeListItem
6. ✅ Add calendar viewer to ContentContainer
7. ✅ Add calendar action placeholders in app.py
8. ✅ Create calendar action handlers
9. ✅ Create proper ICS test fixture (calendar invite)
10. ✅ Update help screen documentation
11. ✅ 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. ✅ Add unit tests for calendar parsing
### Week 3: Advanced Features
1. ✅ Implement Microsoft Graph API calendar actions
2. ⏳ Test with real calendar invites
3. ⏳ Document calendar features in help
### Week 4: Calendar Sync Integration
1. ⏳ Calendar invite acceptance (Graph API)
2. ⏳ Calendar invite declination (Graph API)
3. ⏳ Calendar event removal (Graph API)
---
## 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