feat: Add CalendarInvitePanel to display invite details in mail app

- Create CalendarInvitePanel widget showing event summary, time, location,
  organizer, and attendees with accept/decline/tentative buttons
- Add is_calendar_email() to notification_detector for detecting invite emails
- Add get_raw_message() to himalaya client for exporting full MIME content
- Refactor calendar_parser.py with proper icalendar parsing (METHOD at
  VCALENDAR level, not VEVENT)
- Integrate calendar panel into ContentContainer.display_content flow
- Update tests for new calendar parsing API
- Minor: fix today's header style in calendar WeekGrid
This commit is contained in:
Bendt
2025-12-29 08:41:46 -05:00
parent b89f72cd28
commit db58cb7a2f
8 changed files with 680 additions and 93 deletions

View File

@@ -1,7 +1,14 @@
"""Unit tests for calendar email detection and ICS parsing."""
import pytest
from src.mail.utils import calendar_parser
from src.mail.utils.calendar_parser import (
parse_ics_content,
parse_calendar_from_raw_message,
extract_ics_from_mime,
is_cancelled_event,
is_event_request,
ParsedCalendarEvent,
)
from src.mail.notification_detector import is_calendar_email
@@ -44,58 +51,91 @@ class TestICSParsing:
"""Test ICS file parsing."""
def test_parse_cancellation_ics(self):
"""Test parsing of cancellation ICS from test fixture."""
import base64
from pathlib import Path
"""Test parsing of cancellation ICS."""
ics_content = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
METHOD:CANCEL
BEGIN:VEVENT
UID:test-uid-001
SUMMARY:Technical Refinement Meeting
DTSTART:20251230T140000Z
DTEND:20251230T150000Z
STATUS:CANCELLED
ORGANIZER;CN=Test Organizer:mailto:organizer@example.com
ATTENDEE;CN=Test Attendee:mailto:attendee@example.com
END:VEVENT
END:VCALENDAR"""
fixture_path = Path(
"tests/fixtures/test_mailbox/INBOX/cur/17051226-calendar-invite-001.test:2"
)
if not fixture_path.exists():
pytest.skip("Test fixture file not found")
return
with open(fixture_path, "r") as f:
content = f.read()
event = parse_calendar_part(content)
event = parse_ics_content(ics_content)
assert event is not None
assert is_cancelled_event(event) is True
assert event.method == "CANCEL"
assert event.summary == "Technical Refinement Meeting"
def test_parse_invite_ics(self):
"""Test parsing of invite ICS from test fixture."""
import base64
from pathlib import Path
"""Test parsing of invite/request ICS."""
ics_content = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//Test//EN
METHOD:REQUEST
BEGIN:VEVENT
UID:test-uid-002
SUMMARY:Team Standup
DTSTART:20251230T100000Z
DTEND:20251230T103000Z
STATUS:CONFIRMED
ORGANIZER;CN=Test Organizer:mailto:organizer@example.com
ATTENDEE;CN=Test Attendee:mailto:attendee@example.com
LOCATION:Conference Room A
END:VEVENT
END:VCALENDAR"""
fixture_path = Path(
"tests/fixtures/test_mailbox/INBOX/cur/17051226-calendar-invite-001.test:2"
)
if not fixture_path.exists():
pytest.skip("Test fixture file not found")
return
with open(fixture_path, "r") as f:
content = f.read()
event = parse_calendar_part(content)
event = parse_ics_content(ics_content)
assert event is not None
assert is_event_request(event) is True
assert event.method == "REQUEST"
assert event.summary == "Technical Refinement Meeting"
assert event.summary == "Team Standup"
assert event.location == "Conference Room A"
def test_invalid_ics(self):
"""Test parsing of invalid ICS content."""
event = parse_calendar_part("invalid ics content")
event = parse_ics_content("invalid ics content")
assert event is None # Should return None for invalid ICS
def test_base64_decoding(self):
"""Test base64 decoding of ICS attachment."""
# Test that we can decode base64
encoded = "BASE64ENCODED_I_TEST"
import base64
def test_extract_ics_from_mime(self):
"""Test extraction of ICS from raw MIME message."""
raw_message = """From: organizer@example.com
To: attendee@example.com
Subject: Meeting Invite
Content-Type: multipart/mixed; boundary="boundary123"
decoded = base64.b64decode(encoded)
assert decoded == encoded
--boundary123
Content-Type: text/plain
You have been invited to a meeting.
--boundary123
Content-Type: text/calendar
BEGIN:VCALENDAR
VERSION:2.0
METHOD:REQUEST
BEGIN:VEVENT
UID:mime-test-001
SUMMARY:MIME Test Meeting
DTSTART:20251230T140000Z
DTEND:20251230T150000Z
END:VEVENT
END:VCALENDAR
--boundary123--
"""
ics = extract_ics_from_mime(raw_message)
assert ics is not None
assert "BEGIN:VCALENDAR" in ics
assert "MIME Test Meeting" in ics
event = parse_ics_content(ics)
assert event is not None
assert event.summary == "MIME Test Meeting"
assert event.method == "REQUEST"