11from email .headerregistry import Address
2- from typing import cast
32
43from ics import Calendar , Event # type: ignore[import-untyped]
54from ics .grammar .parse import ContentLine # type: ignore[import-untyped]
87from couchers .config import config
98from couchers .email .rendering import get_emails_i18next
109from couchers .i18n import LocalizationContext
11- from couchers .proto .internal .jobs_pb2 import EmailAttachment
10+ from couchers .proto .internal .jobs_pb2 import EmailAttachmentV2
1211from couchers .proto .requests_pb2 import HostRequest
1312
1413HOST_REQUEST_ICS_FILENAME = "host_request.ics"
1514
1615
1716def create_host_request_attachment (
1817 host_request : HostRequest , other_name : str , hosting : bool , loc_context : LocalizationContext
19- ) -> EmailAttachment :
20- ics = create_host_request_ics (host_request , other_name , hosting , loc_context )
21- return ics_to_attachment ( ics , HOST_REQUEST_ICS_FILENAME )
18+ ) -> EmailAttachmentV2 :
19+ calendar = create_host_request_calendar (host_request , other_name , hosting , loc_context )
20+ return calendar_to_attachment ( calendar , HOST_REQUEST_ICS_FILENAME )
2221
2322
24- def create_host_request_ics (
23+ def create_host_request_calendar (
2524 host_request : HostRequest , other_name : str , hosting : bool , loc_context : LocalizationContext
26- ) -> str :
25+ ) -> Calendar :
2726 event = create_host_request_event (host_request , other_name , hosting , loc_context )
2827
2928 # METHOD:PUBLISH means this is part of a stream of calendar event information.
3029 # It allows for later cancellation, and doesn't expose accept/decline functionality.
31- return event_to_ics (event , "PUBLISH" , loc_context )
30+ return event_to_calendar (event , "PUBLISH" , loc_context )
3231
3332
3433def create_host_request_event (
@@ -68,14 +67,14 @@ def create_host_request_event(
6867
6968def create_host_request_cancellation_attachment (
7069 host_request : HostRequest , other_name : str , hosting : bool , loc_context : LocalizationContext
71- ) -> EmailAttachment :
72- ics = create_host_request_cancellation_ics (host_request , other_name , hosting , loc_context )
73- return ics_to_attachment ( ics , HOST_REQUEST_ICS_FILENAME )
70+ ) -> EmailAttachmentV2 :
71+ calendar = create_host_request_cancellation_calendar (host_request , other_name , hosting , loc_context )
72+ return calendar_to_attachment ( calendar , HOST_REQUEST_ICS_FILENAME )
7473
7574
76- def create_host_request_cancellation_ics (
75+ def create_host_request_cancellation_calendar (
7776 host_request : HostRequest , other_name : str , hosting : bool , loc_context : LocalizationContext
78- ) -> str :
77+ ) -> Calendar :
7978 event = create_host_request_event (host_request , other_name , hosting , loc_context , sequence = 1 )
8079 event .name = get_emails_i18next ().localize (
8180 "calendar_events.title_cancelled" , loc_context .locale , {"title" : event .name }
@@ -85,24 +84,28 @@ def create_host_request_cancellation_ics(
8584 # METHOD:PUBLISH means this is part of a stream of calendar event information.
8685 # Gmail™ will immediately remove the event from the user's calendar.
8786 # METHOD:CANCEL might leave the event in cancelled state or not work.
88- return event_to_ics (event , "PUBLISH" , loc_context )
87+ return event_to_calendar (event , "PUBLISH" , loc_context )
8988
9089
91- def event_to_ics (event : Event , method : str | None , loc_context : LocalizationContext ) -> str :
90+ def event_to_calendar (event : Event , method : str | None , loc_context : LocalizationContext ) -> Calendar :
9291 # PRODID is mandatory and generally follows "-//[Organization]//[Product Name]//[Language]"
9392 calendar = Calendar (creator = f"-//Couchers.org//Couchers//{ loc_context .locale .upper ()} " )
9493 if method :
9594 calendar .method = method
9695 calendar .events .add (event )
97- return cast ( str , calendar . serialize ())
96+ return calendar
9897
9998
100- def ics_to_attachment (ics : str , filename : str ) -> EmailAttachment :
101- return EmailAttachment (
102- filename = filename ,
103- mime_type = "text/calendar" ,
104- data = ics .encode ("utf-8" ),
105- )
99+ def calendar_to_attachment (calendar : Calendar , filename : str ) -> EmailAttachmentV2 :
100+ data = calendar .serialize ().encode ("utf-8" )
101+ content_disposition = f'attachment; filename="{ filename } "'
102+ content_type = 'text/calendar; charset="utf-8"'
103+ if calendar .method :
104+ # The SMTP Content-Type "method" parameter must match the value in the ics file.
105+ # AI recommends avoiding quotes on this parameter for backwards compatibility with old email clients.
106+ content_type += f"; method={ calendar .method } "
107+
108+ return EmailAttachmentV2 (data = data , content_disposition = content_disposition , content_type = content_type )
106109
107110
108111def get_host_request_event_uid (host_request_id : int ) -> str :
0 commit comments