@@ -229,8 +229,18 @@ fn convert_to_utc(
229229}
230230
231231/// Generate an .ics VCALENDAR string for a booking
232+ /// Extract first name (first word) from a full name.
233+ fn first_name ( full_name : & str ) -> & str {
234+ full_name. split_whitespace ( ) . next ( ) . unwrap_or ( full_name)
235+ }
236+
232237pub fn generate_ics ( details : & BookingDetails , method : & str ) -> String {
233- let summary = sanitize_ics ( & details. event_title ) ;
238+ let guest_first = first_name ( & details. guest_name ) ;
239+ let host_first = first_name ( & details. host_name ) ;
240+ let summary = sanitize_ics ( & format ! (
241+ "{} \u{2014} {} & {}" ,
242+ details. event_title, guest_first, host_first
243+ ) ) ;
234244 let host_name = sanitize_ics ( & details. host_name ) ;
235245 let guest_name = sanitize_ics ( & details. guest_name ) ;
236246 let host_email = sanitize_ics ( & details. host_email ) ;
@@ -240,6 +250,12 @@ pub fn generate_ics(details: &BookingDetails, method: &str) -> String {
240250 . as_ref ( )
241251 . map ( |l| format ! ( "LOCATION:{}\r \n " , sanitize_ics( l) ) )
242252 . unwrap_or_default ( ) ;
253+ let description_line = details
254+ . notes
255+ . as_ref ( )
256+ . filter ( |n| !n. trim ( ) . is_empty ( ) )
257+ . map ( |n| format ! ( "DESCRIPTION:{}\r \n " , sanitize_ics( n) ) )
258+ . unwrap_or_default ( ) ;
243259 let valarm = details
244260 . reminder_minutes
245261 . filter ( |& m| m > 0 )
@@ -271,6 +287,7 @@ pub fn generate_ics(details: &BookingDetails, method: &str) -> String {
271287 DTSTART:{dtstart}\r \n \
272288 DTEND:{dtend}\r \n \
273289 SUMMARY:{summary}\r \n \
290+ {description_line}\
274291 {location_line}\
275292 ORGANIZER;CN={host_name}:mailto:{host_email}\r \n \
276293 ATTENDEE;CN={guest_name};RSVP=TRUE:mailto:{guest_email}\r \n \
@@ -283,6 +300,8 @@ pub fn generate_ics(details: &BookingDetails, method: &str) -> String {
283300 dtstart = dtstart,
284301 dtend = dtend,
285302 summary = summary,
303+ description_line = description_line,
304+ location_line = location_line,
286305 host_name = host_name,
287306 host_email = host_email,
288307 guest_name = guest_name,
@@ -292,7 +311,12 @@ pub fn generate_ics(details: &BookingDetails, method: &str) -> String {
292311
293312/// Generate an .ics VCALENDAR for cancellation (METHOD:CANCEL)
294313fn generate_cancel_ics ( details : & CancellationDetails ) -> String {
295- let summary = sanitize_ics ( & details. event_title ) ;
314+ let guest_first = first_name ( & details. guest_name ) ;
315+ let host_first = first_name ( & details. host_name ) ;
316+ let summary = sanitize_ics ( & format ! (
317+ "{} \u{2014} {} & {}" ,
318+ details. event_title, guest_first, host_first
319+ ) ) ;
296320 let host_name = sanitize_ics ( & details. host_name ) ;
297321 let guest_name = sanitize_ics ( & details. guest_name ) ;
298322 let host_email = sanitize_ics ( & details. host_email ) ;
@@ -1479,7 +1503,7 @@ mod tests {
14791503 // Europe/Paris is UTC+1 in March (CET), so 14:00 Paris = 13:00 UTC
14801504 assert ! ( ics. contains( "DTSTART:20260310T130000Z" ) ) ;
14811505 assert ! ( ics. contains( "DTEND:20260310T133000Z" ) ) ;
1482- assert ! ( ics. contains( "SUMMARY:Intro Call" ) ) ;
1506+ assert ! ( ics. contains( "SUMMARY:Intro Call \u{2014} Jane & Alice " ) ) ;
14831507 assert ! ( ics. contains( "ORGANIZER;CN=Alice:mailto:alice@cal.rs" ) ) ;
14841508 assert ! ( ics. contains( "ATTENDEE;CN=Jane Doe;RSVP=TRUE:mailto:jane@example.com" ) ) ;
14851509 assert ! ( ics. contains( "STATUS:CONFIRMED" ) ) ;
@@ -1506,10 +1530,55 @@ mod tests {
15061530 let ics = generate_ics ( & details, "REQUEST" ) ;
15071531 assert ! ( ics. contains( "METHOD:REQUEST" ) ) ;
15081532 assert ! ( ics. contains( "LOCATION:https://meet.example.com/room\r \n " ) ) ;
1533+ assert ! ( ics. contains( "DESCRIPTION:Discuss roadmap\r \n " ) ) ;
15091534 // ORGANIZER must be its own line, not folded into LOCATION
15101535 assert ! ( ics. contains( "\r \n ORGANIZER;" ) ) ;
15111536 }
15121537
1538+ #[ test]
1539+ fn generate_ics_no_description_when_no_notes ( ) {
1540+ let details = BookingDetails {
1541+ event_title : "Call" . to_string ( ) ,
1542+ date : "2026-03-10" . to_string ( ) ,
1543+ start_time : "09:00" . to_string ( ) ,
1544+ end_time : "10:00" . to_string ( ) ,
1545+ guest_name : "Bob" . to_string ( ) ,
1546+ guest_email : "bob@test.com" . to_string ( ) ,
1547+ guest_timezone : "UTC" . to_string ( ) ,
1548+ host_name : "Alice" . to_string ( ) ,
1549+ host_email : "alice@test.com" . to_string ( ) ,
1550+ uid : "uid-no-notes" . to_string ( ) ,
1551+ notes : None ,
1552+ location : None ,
1553+ reminder_minutes : None ,
1554+ } ;
1555+
1556+ let ics = generate_ics ( & details, "PUBLISH" ) ;
1557+ assert ! ( !ics. contains( "DESCRIPTION:" ) ) ;
1558+ }
1559+
1560+ #[ test]
1561+ fn generate_ics_summary_includes_first_names ( ) {
1562+ let details = BookingDetails {
1563+ event_title : "30min call" . to_string ( ) ,
1564+ date : "2026-03-10" . to_string ( ) ,
1565+ start_time : "09:00" . to_string ( ) ,
1566+ end_time : "09:30" . to_string ( ) ,
1567+ guest_name : "Jean-Baptiste Piacentino" . to_string ( ) ,
1568+ guest_email : "jb@test.com" . to_string ( ) ,
1569+ guest_timezone : "UTC" . to_string ( ) ,
1570+ host_name : "Olivier Lambert" . to_string ( ) ,
1571+ host_email : "olivier@test.com" . to_string ( ) ,
1572+ uid : "uid-names" . to_string ( ) ,
1573+ notes : None ,
1574+ location : None ,
1575+ reminder_minutes : None ,
1576+ } ;
1577+
1578+ let ics = generate_ics ( & details, "PUBLISH" ) ;
1579+ assert ! ( ics. contains( "SUMMARY:30min call \u{2014} Jean-Baptiste & Olivier" ) ) ;
1580+ }
1581+
15131582 #[ test]
15141583 fn generate_ics_escapes_special_chars ( ) {
15151584 let details = BookingDetails {
@@ -1529,7 +1598,7 @@ mod tests {
15291598 } ;
15301599
15311600 let ics = generate_ics ( & details, "PUBLISH" ) ;
1532- assert ! ( ics. contains( "SUMMARY:Meet\\ ; discuss\\ , plan" ) ) ;
1601+ assert ! ( ics. contains( "SUMMARY:Meet\\ ; discuss\\ , plan \u{2014} O'Brien & Host " ) ) ;
15331602 }
15341603
15351604 // --- h (HTML escaping) ---
@@ -1631,7 +1700,7 @@ mod tests {
16311700 assert ! ( ics. contains( "UID:cancel-uid-123" ) ) ;
16321701 assert ! ( ics. contains( "DTSTART:20260310T140000Z" ) ) ;
16331702 assert ! ( ics. contains( "DTEND:20260310T143000Z" ) ) ;
1634- assert ! ( ics. contains( "SUMMARY:Intro Call" ) ) ;
1703+ assert ! ( ics. contains( "SUMMARY:Intro Call \u{2014} Jane & Alice " ) ) ;
16351704 }
16361705
16371706 #[ test]
@@ -1884,7 +1953,7 @@ mod tests {
18841953 cancelled_by_host : true ,
18851954 } ;
18861955 let ics = generate_cancel_ics ( & details) ;
1887- assert ! ( ics. contains( "SUMMARY:Team sync\\ ; weekly\\ , recurring" ) ) ;
1956+ assert ! ( ics. contains( "SUMMARY:Team sync\\ ; weekly\\ , recurring \u{2014} Bob & Alice " ) ) ;
18881957 assert ! ( ics. contains( "METHOD:CANCEL" ) ) ;
18891958 assert ! ( ics. contains( "STATUS:CANCELLED" ) ) ;
18901959 assert ! ( ics. contains( "DTSTART:20260520T160000Z" ) ) ;
0 commit comments