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

29 KiB

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

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:

pip install icalendar

Basic Usage:

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:

pip install ics

Basic Usage:

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:

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:

[project.optional-dependencies]
icalendar = ">=5.0,<7.0"
# OR
ics = ">=0.6,<1.0"

[project.optional-dependencies-extras]
icalendar = ["all"]

Install Command:

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:

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:

"""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:

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:

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:

# 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:

# 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:

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:

# 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:

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:

[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

[calendar]
parser_library = "icalendar"  # or "ics"
auto_detect_calendar = true

Display Options

[envelope_display]
show_calendar_icon = true

Action Configuration

[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)

Textual Widget Documentation

Microsoft Graph API Documentation

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