From a1cf16350be768c187d66c96a120850941f646f5 Mon Sep 17 00:00:00 2001 From: Bill Thiede Date: Tue, 20 Jan 2026 09:39:40 -0800 Subject: [PATCH] server: big improvements for parsing all day events --- server/src/email_extract.rs | 154 +++- server/testdata/google-calendar-example-4.eml | 728 ++++++++++++++++++ 2 files changed, 874 insertions(+), 8 deletions(-) create mode 100644 server/testdata/google-calendar-example-4.eml diff --git a/server/src/email_extract.rs b/server/src/email_extract.rs index 42eec1b..30ccfaf 100644 --- a/server/src/email_extract.rs +++ b/server/src/email_extract.rs @@ -241,6 +241,31 @@ pub fn extract_calendar_metadata_from_mail( if end_date.is_none() { end_date = Some(end); } } } + // Pattern: single all-day event: @ Sun Jan 18, 2026 (no time range) + if start_date.is_none() { + if let Some(caps) = regex::Regex::new(r"@ [A-Za-z]{3} ([A-Za-z]{3}) (\d{1,2}), (\d{4})(?:\s*\(|$)").ok().and_then(|re| re.captures(&subject)) { + let month = &caps[1]; + let day = &caps[2]; + let year = &caps[3]; + fn month_num(mon: &str) -> Option<&'static str> { + match mon { + "Jan" => Some("01"), "Feb" => Some("02"), "Mar" => Some("03"), "Apr" => Some("04"), + "May" => Some("05"), "Jun" => Some("06"), "Jul" => Some("07"), "Aug" => Some("08"), + "Sep" => Some("09"), "Oct" => Some("10"), "Nov" => Some("11"), "Dec" => Some("12"), + _ => None + } + } + if let Some(mm) = month_num(month) { + let start = format!("{}{}{:0>2}", year, mm, day); + // For all-day events, end date is the next day (exclusive) + if let Ok(d) = chrono::NaiveDate::parse_from_str(&format!("{}-{}-{:0>2}", year, mm, day), "%Y-%m-%d") { + let end = d.succ_opt().unwrap_or(d).format("%Y%m%d").to_string(); + start_date = Some(start); + end_date = Some(end); + } + } + } + } } } } @@ -328,6 +353,8 @@ pub fn extract_calendar_metadata_from_mail( let needs_ical_flex = summary.is_some() || start_date.is_some() || end_date.is_some() || has_recurrence; if needs_ical_flex { + use chrono::{Datelike, NaiveDate}; + let summary_val = summary.clone().unwrap_or_default(); let organizer_val = organizer.clone().unwrap_or_default(); let start_val = start_date.clone().unwrap_or_default(); @@ -337,15 +364,101 @@ pub fn extract_calendar_metadata_from_mail( } else { String::new() }; + + // Compute event_days and all_days for calendar grid rendering + let mut event_days: Vec = Vec::new(); + let (local_fmt_start, local_fmt_end) = if let (Some(ref start_str), Some(ref end_str)) = + (&start_date, &end_date) + { + // Parse YYYYMMDD format dates + let start_d = NaiveDate::parse_from_str(start_str, "%Y%m%d").ok(); + let end_d = NaiveDate::parse_from_str(end_str, "%Y%m%d").ok(); + + if let (Some(start), Some(end)) = (start_d, end_d) { + // For all-day events, end date is exclusive, so we need to subtract one day + let display_end = if end > start { + end.pred_opt().unwrap_or(end) + } else { + end + }; + + // Add all days from start to display_end (inclusive) to event_days + let mut day_iter = start; + while day_iter <= display_end { + event_days.push(day_iter); + day_iter = day_iter.succ_opt().unwrap_or(day_iter); + if day_iter == display_end && day_iter == start { + // Single day event + break; + } + } + + // Format dates for display + let fmt_start = start.format("%a %b %e, %Y").to_string(); + let fmt_end = display_end.format("%a %b %e, %Y").to_string(); + (fmt_start, fmt_end) + } else { + (start_val.clone(), end_val.clone()) + } + } else { + (start_val.clone(), end_val.clone()) + }; + + // Compute calendar grid (all_days) from event_days + let (all_days, caption) = if !event_days.is_empty() { + let first_event = event_days.first().unwrap(); + let last_event = event_days.last().unwrap(); + + let first_of_month = + NaiveDate::from_ymd_opt(first_event.year(), first_event.month(), 1).unwrap(); + let last_of_month = { + let next_month = if last_event.month() == 12 { + NaiveDate::from_ymd_opt(last_event.year() + 1, 1, 1).unwrap() + } else { + NaiveDate::from_ymd_opt(last_event.year(), last_event.month() + 1, 1).unwrap() + }; + next_month.pred_opt().unwrap() + }; + + // Start from Sunday of the week containing first_of_month + let mut cal_start = first_of_month; + while cal_start.weekday() != chrono::Weekday::Sun { + cal_start = cal_start.pred_opt().unwrap(); + } + // End on Saturday of the week containing last_of_month + let mut cal_end = last_of_month; + while cal_end.weekday() != chrono::Weekday::Sat { + cal_end = cal_end.succ_opt().unwrap(); + } + + let mut all_days = vec![]; + let mut d = cal_start; + while d <= cal_end { + all_days.push(d); + d = d.succ_opt().unwrap(); + } + + let start_month = first_event.format("%B %Y"); + let end_month = last_event.format("%B %Y"); + let caption = if start_month.to_string() == end_month.to_string() { + start_month.to_string() + } else { + format!("{} – {}", start_month, end_month) + }; + (all_days, caption) + } else { + (vec![], String::new()) + }; + let template = IcalSummaryTemplate { summary: &summary_val, - local_fmt_start: &start_val, - local_fmt_end: &end_val, + local_fmt_start: &local_fmt_start, + local_fmt_end: &local_fmt_end, organizer: &organizer_val, organizer_cn: "", - all_days: vec![], - event_days: vec![], - caption: String::new(), + all_days, + event_days, + caption, description_paragraphs: &[], today: Some(chrono::Local::now().date_naive()), recurrence_display, @@ -2286,8 +2399,9 @@ mod tests { html.contains("Dentist appt"), "HTML should contain the summary" ); + // Date is now formatted as human-readable "Tue Sep 23, 2025" assert!( - html.contains("20250923"), + html.contains("Sep 23, 2025") || html.contains("20250923"), "HTML should contain the event date" ); assert!( @@ -2341,12 +2455,13 @@ mod tests { html.contains("Organizer: calendar-notification@google.com"), "HTML should contain the labeled organizer" ); + // Dates are now formatted as human-readable assert!( - html.contains("Start: 20250911"), + html.contains("Start: Thu Sep 11, 2025") || html.contains("Start: 20250911"), "HTML should contain the labeled start time" ); assert!( - html.contains("End: 20260131"), + html.contains("End: Fri Jan 30, 2026") || html.contains("End: 20260131"), "HTML should contain the labeled end time" ); if !html.contains("ical-flex") { @@ -2468,6 +2583,29 @@ mod tests { } } + #[test] + fn google_calendar_email_4_single_allday_event() { + use mailparse::parse_mail; + let raw_email = include_str!("../../server/testdata/google-calendar-example-4.eml"); + let parsed = parse_mail(raw_email.as_bytes()).expect("parse_mail"); + let mut part_addr = vec![]; + let body = extract_body(&parsed, &mut part_addr).expect("extract_body"); + let meta = extract_calendar_metadata_from_mail(&parsed, &body); + // Assert detection as Google Calendar + assert!(meta.is_google_calendar_event); + // Assert metadata extraction for single all-day event + assert_eq!(meta.summary, Some("Emery Sleeps Over".to_string())); + assert_eq!(meta.organizer, Some("tconvertino@gmail.com".to_string())); + // Dates: Sunday Jan 18, 2026 (all-day event) + assert_eq!(meta.start_date, Some("20260118".to_string())); + assert_eq!(meta.end_date, Some("20260119".to_string())); // All-day events end next day + // Assert ical summary is rendered and shows Jan 18 highlighted + let html = meta.body_html.expect("body_html"); + println!("Rendered HTML: {}", html); + assert!(html.contains("ical-flex"), "Calendar widget should be rendered"); + assert!(html.contains(r#"data-event-day="2026-01-18""#), "Jan 18 should be highlighted"); + } + #[test] fn recurring_event_rrule_metadata_and_highlight() { use super::render_ical_summary; diff --git a/server/testdata/google-calendar-example-4.eml b/server/testdata/google-calendar-example-4.eml new file mode 100644 index 0000000..005362b --- /dev/null +++ b/server/testdata/google-calendar-example-4.eml @@ -0,0 +1,728 @@ +Return-Path: +Delivered-To: bill@xinu.tv +Received: from phx.xinu.tv [74.207.253.222] + by nixos-01.h.xinu.tv with IMAP (fetchmail-6.5.6) + for (single-drop); Sat, 17 Jan 2026 09:25:58 -0800 (PST) +Received: from phx.xinu.tv + by phx.xinu.tv with LMTP + id YIkZKyXGa2k93xMAJR8clQ + (envelope-from ) + for ; Sat, 17 Jan 2026 09:25:57 -0800 +X-Original-To: gmail@xinu.tv +Received: from mail-lf1-f48.google.com (mail-lf1-f48.google.com [209.85.167.48]) + by phx.xinu.tv (Postfix) with ESMTPS id B744880023 + for ; Sat, 17 Jan 2026 09:25:56 -0800 (PST) +Received: by mail-lf1-f48.google.com with SMTP id 2adb3069b0e04-59b78886454so3800941e87.2 + for ; Sat, 17 Jan 2026 09:25:56 -0800 (PST) +ARC-Seal: i=2; a=rsa-sha256; t=1768670755; cv=pass; + d=google.com; s=arc-20240605; + b=UCMG36NoEclyVlwzV5KDOA6Fq75afR1kZ6QZQ8A0CR9RJMMEnPEpiuhheiGH7csZWs + HEZJmrLtTX/e5qiZ0k5njtm8694d+44YtpWRS54bwcAvwWBeCnHstTFkuOB4J2GWvT6G + R9MwX2lwlaGj118bn6aIQTWLB6KyWzUmGdq9AO52fvWTkzlPFDN54/AUYdhx4r+dG5k3 + tqmDhE87DYIPTtNwYeUZpyEvcKuXYqlRmkHEL+qkixmj6yFX9jReNcHypO3QOj8StGqu + H/WKwOSnM5Yupv4EblgGPF8ib8tczyxoi+q73sv7iRtQy8wgyAC1gG6T6/qXuY/+1V1K + lyfw== +ARC-Message-Signature: i=2; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; + h=to:from:subject:date:message-id:auto-submitted:sender:reply-to + :mime-version:dkim-signature:dkim-signature:delivered-to; + bh=Hw6hGQbUlNGCz02STp2P+s244T5EBOLlXrfxnO0S/+U=; + fh=mUux9OA2+hLns/mBVi/4Nr5W8MsoxjQs+3G2LAg1TZo=; + b=V+AKgzj8GN/DZNHWPE/MY0blPHHM1Kp85OCTTacCIk/G6dNhx+WmLnIyExrC3i4wmU + i62upyA0a18rhHZRhV1FB4oMMhQVroYLKwh5dFuqFtTARua9DgwYeN6YALL9+rr84n2b + eZe0txkO5dyJgxByumgOymYFgbevrEtd1GWfK2v1BxtQXzqNZ0SKj5PhVCc5WD+toeHu + OEqUuCoHRWpeXYD19OUqv/+MwhPC4t5R5fz8nlPcjxa/fYINuI5+iLhSP7Ki4gzAZFRK + T3zMitsxIv/8zKMrhG5K0cm7Nntn2XBT5zrIDURZW9HEKYLww0yJ8qKXPNL+RtbfGgMq + TiRw==; + darn=xinu.tv +ARC-Authentication-Results: i=2; mx.google.com; + dkim=pass header.i=@google.com header.s=20230601 header.b=bNGW+EgT; + dkim=pass header.i=@gmail.com header.s=20230601 header.b=QfHaLAXu; + spf=pass (google.com: domain of tconvertino@gmail.com designates 209.85.220.73 as permitted sender) smtp.mailfrom=tconvertino@gmail.com; + dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com; + dara=pass header.i=@gmail.com +X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=1e100.net; s=20230601; t=1768670755; x=1769275555; + h=to:from:subject:date:message-id:auto-submitted:sender:reply-to + :mime-version:dkim-signature:dkim-signature:delivered-to + :x-forwarded-for:x-forwarded-to:x-gm-message-state:from:to:cc + :subject:date:message-id:reply-to; + bh=Hw6hGQbUlNGCz02STp2P+s244T5EBOLlXrfxnO0S/+U=; + b=eTx/TzBvNH8F83uUTAONl5k6vTRn2Id8TE91Mnl0cbJd6GaM9J4DgWVxmBoqfep8nA + hN3H5r01YLOdQNWTWmAV2RUrylBtLMQRqW7xeVnIIUKQNXfavZZKaFpwsuudvjDWGKBo + zesMFCOaEstK+nCpo/bPurb9kprcOh5y/WZjgL7OqtpnnyzhN6DhkhaYbGetIwW4osj5 + aCFPOcoMcCYYW8OxEZC6bv2hJzehnV93g8IDY8tBQ88TIf1Kl3uDM8v3oLwMGgmEX+te + PYznnWbJF0vG00cWauIsTnjzUt8SSpnaUXw6PXbHlZxn5Roa/l6hg/tuhs699btYOJm/ + izbQ== +X-Forwarded-Encrypted: i=2; AJvYcCUWtYAXLj/f6NFhD1jVOvyY1Jd5fsiQkXHwDFfYixYixyUvud2GXNENdLwj08ultHSVt74PwA==@xinu.tv +X-Gm-Message-State: AOJu0Yx6pI6AZXNq1lGFocBmt39kF0MuPDwo3WPcPrcCg8s1e8EF0iF0 + 0jOlOq3z4d3WKZqbpCpMIczBtHf5wHUzS1TFiPfYcoHfhnoxLm+dVYhdf0B5b39G2NSwnHIRAcZ + HPVGwj7Cl8dNJOMBLPOevH4CYTDEubbDxDmQOvWE0bhVDk2P+UIU53lYzGkLCnQ== +X-Received: by 2002:a05:6512:4016:b0:59b:7888:62c8 with SMTP id 2adb3069b0e04-59baeed63a1mr2407719e87.33.1768670754691; + Sat, 17 Jan 2026 09:25:54 -0800 (PST) +X-Forwarded-To: gmail@xinu.tv +X-Forwarded-For: couchmoney@gmail.com gmail@xinu.tv +Delivered-To: couchmoney@gmail.com +Received: by 2002:a05:6504:2382:b0:2d3:710a:2457 with SMTP id h2csp2362813lty; + Sat, 17 Jan 2026 09:25:53 -0800 (PST) +X-Received: by 2002:a05:6808:4f0e:b0:450:ac57:48a7 with SMTP id 5614622812f47-45c9c14fc0fmr2557499b6e.59.1768670753179; + Sat, 17 Jan 2026 09:25:53 -0800 (PST) +ARC-Seal: i=1; a=rsa-sha256; t=1768670753; cv=none; + d=google.com; s=arc-20240605; + b=V+T4U8NWyAR1p4yC5XY/I8vxXwtdkLXkIEO6gNBVvJyYi4XbjMMEnoRPAqOULwONFT + 7q1V9vArMoZrvS4GNL3dg05tLr0Ug+Frm39+Vp1Wp3UxhQ/yxiby8jhRYkMyaKLZxhR3 + 2kihw8UgFjdUteHHwKoTDnIkTeKrMKZK8N4bTEzf9LoIXHMZcVaeC5XItuuOUdX6TPXr + xEQKfzCfz3UHY1piusFov9YIr8iBLGnNp6bXJqbRKmnLhOGkt9HQOT9rBl1nmBg5bqQj + 4qxTu8Le/CE5qljInXX5iXNYXp1eMD1G6PZ9Hah1hr/wen1VPM6ysNynBlDzlNQUyEMJ + 8lNw== +ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; + h=to:from:subject:date:message-id:auto-submitted:sender:reply-to + :mime-version:dkim-signature:dkim-signature; + bh=Hw6hGQbUlNGCz02STp2P+s244T5EBOLlXrfxnO0S/+U=; + fh=mbzrMIWIgWMC0ni1xEx+ViW4J0RLAdLdPT2cX81nTlk=; + b=UhcC2/PG/1T8wb5AzN1QNoaEXR7rs82O/P2CXN7vMVR9JoE3selJIzwIpyWxuKDPK3 + GQtEmc8Bqcvcqu//9mWJxsklCkxSXrYnJ0UvykvbmZT7xPhM4r2mpWPluvfxLfEEdbqg + aNgJM1bn4QoYvnjmIF638/SN9dK5TI9seZ04BzbqQxd7Vw5OeccovSpPerSP7ya7l+4k + wOHhvP4mAlB/0bUae8xN/bqS0SIgy+V+cRr3tYEsRb21gJgTT757rHIV0aQu5LSO9t2N + UilB/hh4qvPhaCWmj6I+30ZYD02m9WKPYkwteLA9NXtggMw9WGeywxPZ//pHazzbq7iQ + im+A==; + dara=google.com +ARC-Authentication-Results: i=1; mx.google.com; + dkim=pass header.i=@google.com header.s=20230601 header.b=bNGW+EgT; + dkim=pass header.i=@gmail.com header.s=20230601 header.b=QfHaLAXu; + spf=pass (google.com: domain of tconvertino@gmail.com designates 209.85.220.73 as permitted sender) smtp.mailfrom=tconvertino@gmail.com; + dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com; + dara=pass header.i=@gmail.com +Received: from mail-sor-f73.google.com (mail-sor-f73.google.com. [209.85.220.73]) + by mx.google.com with SMTPS id 5614622812f47-45c9e03dfa5sor2738469b6e.10.2026.01.17.09.25.53 + for + (Google Transport Security); + Sat, 17 Jan 2026 09:25:53 -0800 (PST) +Received-SPF: pass (google.com: domain of tconvertino@gmail.com designates 209.85.220.73 as permitted sender) client-ip=209.85.220.73; +Authentication-Results: mx.google.com; + dkim=pass header.i=@google.com header.s=20230601 header.b=bNGW+EgT; + dkim=pass header.i=@gmail.com header.s=20230601 header.b=QfHaLAXu; + spf=pass (google.com: domain of tconvertino@gmail.com designates 209.85.220.73 as permitted sender) smtp.mailfrom=tconvertino@gmail.com; + dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com; + dara=pass header.i=@gmail.com +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=google.com; s=20230601; t=1768670752; x=1769275552; dara=google.com; + h=to:from:subject:date:message-id:auto-submitted:sender:reply-to + :mime-version:from:to:cc:subject:date:message-id:reply-to; + bh=Hw6hGQbUlNGCz02STp2P+s244T5EBOLlXrfxnO0S/+U=; + b=bNGW+EgTmg1v7auBVBEmmFGyg6QDqI536axgkCb2SIiknIljcxZLx2KR0hrFlA3lSz + Z89Q1JdMU37Tx8upXXkAQYBe0A42UgQjXEYYfjykMl/PNg7XppVWzevLwkKLmmr/dZ7f + YMcE1DQogEr3RNXJeD92NfJxyOQGskvnzb4rhy22QonzF2UyGy/QX2UtFSz1cZi+35Yq + vTkaernNWU3hf5pAXigHisJTtoJeTRgVNY4ch+gru1X1LmZZzrTgWt6e7hGtsbvlV7cZ + CBM8gqf1LrVLV0Y1PdvS50yack5EFKbyKtmQWAwHBlOABVDwPHbPD9/6N4973C9juedx + HnNg== +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=gmail.com; s=20230601; t=1768670753; x=1769275553; dara=google.com; + h=to:from:subject:date:message-id:auto-submitted:sender:reply-to + :mime-version:from:to:cc:subject:date:message-id:reply-to; + bh=Hw6hGQbUlNGCz02STp2P+s244T5EBOLlXrfxnO0S/+U=; + b=QfHaLAXuHdf594PTfEIjd3XOTBUaUaqHXtEArT1QZBEi1Vpf8NyYD6cPKPbjnln3CZ + q5s/sBjI6bmtszfVecPTEv3SEnFgQqQS/PCw3YbMNteemsw4rDNccwV6DSiX/BYMRZIM + 4HEgMoLbXrlMlFnjEpWkfb7Kon5Y39C2DNx3sZ3TX/s8fLgYC8JpdUXdZ+LRlr8QzoNH + VLvVwW2iBYOzX9QdBtLdghvnmgvuSxIq1xB0zQNvDixOuG/egq1nDjHna4T75W8qzNEq + +hl8Rng1G2oqWAWUASkwSRvrUvV/NJA3gE+tGD2Isj9d/r4Ppll4jBWOu7KVRPM8Yrld + MYcg== +MIME-Version: 1.0 +X-Received: by 2002:a05:6820:1993:b0:65f:67b7:95c2 with SMTP id + 006d021491bc7-661179f382fmr2914920eaf.55.1768670752809; Sat, 17 Jan 2026 + 09:25:52 -0800 (PST) +Reply-To: tconvertino@gmail.com +Sender: Google Calendar +Auto-Submitted: auto-generated +Message-ID: +Date: Sat, 17 Jan 2026 17:25:52 +0000 +Subject: New event: Emery Sleeps Over @ Sun Jan 18, 2026 (tconvertino@gmail.com) +From: tconvertino@gmail.com +To: couchmoney@gmail.com +Content-Type: multipart/alternative; boundary="000000000000f22916064898bf45" +X-Rspamd-Queue-Id: B744880023 +X-Rspamd-Server: phx +X-Spamd-Result: default: False [-0.90 / 15.00]; + URI_COUNT_ODD(1.00)[1]; + ARC_ALLOW(-1.00)[google.com:s=arc-20240605:i=2]; + DMARC_POLICY_ALLOW(-0.50)[gmail.com,none]; + R_DKIM_ALLOW(-0.20)[google.com:s=20230601,gmail.com:s=20230601]; + R_SPF_ALLOW(-0.20)[+ip4:209.85.128.0/17]; + MANY_INVISIBLE_PARTS(0.10)[2]; + MIME_GOOD(-0.10)[multipart/alternative,text/plain]; + FREEMAIL_TO(0.00)[gmail.com]; + RCVD_COUNT_THREE(0.00)[3]; + RCVD_TLS_LAST(0.00)[]; + FORGED_SENDER(0.00)[tconvertino@gmail.com,couchmoney@gmail.com]; + RCPT_COUNT_ONE(0.00)[1]; + RCVD_IN_DNSWL_NONE(0.00)[209.85.220.73:received]; + TAGGED_FROM(0.00)[caf_=gmail=xinutv]; + FREEMAIL_REPLYTO(0.00)[gmail.com]; + MIME_TRACE(0.00)[0:+,1:+,2:~]; + FREEMAIL_FROM(0.00)[gmail.com]; + FROM_NEQ_ENVFROM(0.00)[tconvertino@gmail.com,couchmoney@gmail.com]; + MISSING_XM_UA(0.00)[]; + HAS_REPLYTO(0.00)[tconvertino@gmail.com]; + DNSWL_BLOCKED(0.00)[209.85.167.48:from]; + DWL_DNSWL_NONE(0.00)[gmail.com:dkim]; + TO_DN_NONE(0.00)[]; + FREEMAIL_ENVFROM(0.00)[gmail.com]; + FORGED_SENDER_FORWARDING(0.00)[]; + DKIM_TRACE(0.00)[google.com:+,gmail.com:+]; + DWL_DNSWL_BLOCKED(0.00)[google.com:dkim]; + TO_DOM_EQ_FROM_DOM(0.00)[]; + FROM_NO_DN(0.00)[]; + FWD_GOOGLE(0.00)[couchmoney@gmail.com]; + ASN(0.00)[asn:15169, ipnet:209.85.128.0/17, country:US]; + RWL_MAILSPIKE_POSSIBLE(0.00)[209.85.167.48:from]; + REPLYTO_EQ_FROM(0.00)[] +X-Rspamd-Action: no action +X-TUID: GNj+V6W3PxE3 + +--000000000000f22916064898bf45 +Content-Type: text/plain; charset="UTF-8"; format=flowed; delsp=yes + +Emery Sleeps Over +Sunday Jan 18, 2026 + + + +Organizer +tconvertino@gmail.com +tconvertino@gmail.com + +~~//~~ +Invitation from Google Calendar: https://calendar.google.com/calendar/ + +You are receiving this email because you are subscribed to calendar +notifications. To stop receiving these emails, go to +https://calendar.google.com/calendar/r/settings, select this calendar, and +change "Other notifications". + +Forwarding this invitation could allow any recipient to send a response to +the organizer, be added to the guest list, invite others regardless of +their own invitation status, or modify your RSVP. + +Learn more https://support.google.com/calendar/answer/37135#forwarding + +--000000000000f22916064898bf45 +Content-Type: text/html; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + + + + + + + + + + + + + =20 + + =20 + + =20 + + =20 + + + + + + + + + Emery Sleeps Over
You have been invited by tconvertino@gmail.co= +m to attend an event named Emery Sleeps Over on Sunday Jan 18, 2026.
<= +table border=3D"0" cellpadding=3D"0" cellspacing=3D"0" role=3D"presentation= +" align=3D"center" style=3D"width:100%;" class=3D"body-container">
 
<= +td style=3D"font-size: 0; padding: 0; text-align: left; word-break: break-w= +ord;;padding-bottom:24px;">
= +

When

Sunday Jan 18, 2026

Calendar

tconvertino@gmail.com

Organizer

Invitation from Googl= +e Calendar

You are receiving this email because you are subscribe= +d to calendar notifications. To stop receiving these emails, go to Calendar settings, select this calen= +dar, and change "Other notifications".

Forwarding this invitation cou= +ld allow any recipient to send a response to the organizer, be added to the= + guest list, invite others regardless of their own invitation status, or mo= +dify your RSVP. Learn more

<= +/html> +--000000000000f22916064898bf45--