Fix Calendar: Uppercase MAILTO calendar addresses become invalid SMTP recipients

This commit is contained in:
Maurus Decimus
2026-07-19 10:29:07 +02:00
parent bd44943661
commit 18dbbda298
8 changed files with 34 additions and 21 deletions

View File

@@ -13,6 +13,7 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If
## Fixed
- IMAP: Mailbox object-quota only enforced in JMAP.
- JMAP: Read-only sharee cannot set `isSubscribed` on a shared mailbox.
- Calendar: Uppercase `MAILTO` calendar addresses become invalid SMTP recipients.
## [0.16.13] - 2026-07-12

View File

@@ -29,7 +29,9 @@ use dav_proto::{
response::{CalCondition, Href, ScheduleResponse, ScheduleResponseItem},
},
};
use groupware::{DestroyArchive, cache::GroupwareCache, calendar::CalendarEventNotification};
use groupware::{
DestroyArchive, cache::GroupwareCache, calendar::CalendarEventNotification, strip_mailto_scheme,
};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::{
@@ -325,9 +327,7 @@ impl CalendarEventNotificationHandler for Server {
ICalendarValue::Text(value) | ICalendarValue::Uri(Uri::Location(value)),
),
) => {
if let Some(email) =
sanitize_email(value.strip_prefix("mailto:").unwrap_or(value.as_str()))
{
if let Some(email) = sanitize_email(strip_mailto_scheme(value.as_str())) {
attendees.insert(email, entry);
}
}

View File

@@ -8,9 +8,12 @@ use super::{
ArchivedCalendar, ArchivedCalendarEvent, ArchivedCalendarPreferences, ArchivedDefaultAlert,
ArchivedTimezone, Calendar, CalendarEvent, CalendarPreferences, DefaultAlert, Timezone,
};
use crate::calendar::{
ArchivedCalendarEventNotification, ArchivedChangedBy, ArchivedEventPreferences,
CalendarEventNotification, ChangedBy, EventPreferences,
use crate::{
calendar::{
ArchivedCalendarEventNotification, ArchivedChangedBy, ArchivedEventPreferences,
CalendarEventNotification, ChangedBy, EventPreferences,
},
strip_mailto_scheme,
};
use ahash::AHashSet;
use calcard::icalendar::{
@@ -463,7 +466,7 @@ impl ArchivedCalendarEvent {
_ => None,
}))
{
let value = value.strip_prefix("mailto:").unwrap_or(value).trim();
let value = strip_mailto_scheme(value);
let lang = if is_lang {
detector.detect(value, MIN_LANGUAGE_SCORE);
Language::Unknown

View File

@@ -149,3 +149,10 @@ impl DavCalendarResource for DavResources {
.map(|p| p.tz)
}
}
pub fn strip_mailto_scheme(value: &str) -> &str {
value
.split_once(':')
.filter(|(scheme, _)| scheme.eq_ignore_ascii_case("mailto"))
.map_or(value, |(_, address)| address.trim())
}

View File

@@ -4,6 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::strip_mailto_scheme;
use ahash::{AHashMap, AHashSet};
use calcard::{
common::{IanaString, PartialDateTime},
@@ -238,7 +239,7 @@ impl Attendee<'_> {
impl Email {
pub fn new(email: &str, local_addresses: &[String]) -> Option<Self> {
email.contains('@').then(|| {
let email = email.trim().trim_start_matches("mailto:").to_lowercase();
let email = strip_mailto_scheme(email.trim()).to_lowercase();
let is_local = local_addresses.contains(&email);
Email { email, is_local }
})

View File

@@ -6,7 +6,10 @@
use crate::participant_identity::get::ParticipantIdentityGet;
use common::Server;
use groupware::calendar::{ParticipantIdentities, ParticipantIdentity};
use groupware::{
calendar::{ParticipantIdentities, ParticipantIdentity},
strip_mailto_scheme,
};
use jmap_proto::{
error::set::{SetError, SetErrorType},
method::set::{SetRequest, SetResponse},
@@ -226,11 +229,7 @@ fn validate_identity_value(
}
(ParticipantIdentityProperty::CalendarAddress, Value::Str(value)) => {
if identity.calendar_address != value {
let email = if let Some(email) = value.strip_prefix("mailto:") {
sanitize_email(email)
} else {
sanitize_email(&value)
};
let email = sanitize_email(strip_mailto_scheme(&value));
if let Some(email) = email {
if allowed_emails.iter().any(|e| e == &email) {

View File

@@ -21,6 +21,7 @@ use common::{
use groupware::{
cache::GroupwareCache,
calendar::{CALENDAR_SUBSCRIBED, CalendarEvent},
strip_mailto_scheme,
};
use jmap_proto::{
method::availability::{
@@ -258,9 +259,7 @@ impl PrincipalGetAvailability for Server {
if include_in_availability == IncludeInAvailability::Attending =>
{
if let Some(attendee) = value.as_text().and_then(|attendee| {
sanitize_email(
attendee.strip_prefix("mailto:").unwrap_or(attendee),
)
sanitize_email(strip_mailto_scheme(attendee))
}) {
// Condition: the Principal is a participant of the event, and has a "participationStatus" of "accepted" or "tentative".
if principal_account.addresses().contains(&attendee) {

View File

@@ -17,7 +17,10 @@ use common::{
ipc::{CalendarAlert, PushNotification},
network::{ServerInstance, stream::NullIo},
};
use groupware::calendar::{ArchivedCalendarEvent, CalendarEvent};
use groupware::{
calendar::{ArchivedCalendarEvent, CalendarEvent},
strip_mailto_scheme,
};
use mail_builder::{
MessageBuilder,
headers::{HeaderType, content_type::ContentType},
@@ -478,7 +481,7 @@ async fn build_template(
.values
.first()
.and_then(|v| v.as_text())
.map(|v| v.strip_prefix("mailto:").unwrap_or(v))
.map(strip_mailto_scheme)
.and_then(sanitize_email);
}
_ => {}
@@ -501,7 +504,7 @@ async fn build_template(
.values
.first()
.and_then(|v| v.as_text())
.map(|v| v.strip_prefix("mailto:").unwrap_or(v));
.map(strip_mailto_scheme);
let name = entry.params.iter().find_map(|param| {
if let ArchivedICalendarParameterName::Cn = param.name {
param.value.as_text()