From 6076659e4dcf9c95682749702689f889a04d2e3f Mon Sep 17 00:00:00 2001 From: Maurus Decimus <11444311+mdecimus@users.noreply.github.com> Date: Sun, 19 Jul 2026 12:19:50 +0200 Subject: [PATCH] Fix Calendar: Scheduling invitations on a shared, non-owned calendar fail with `MAIL FROM unauthorized` --- CHANGELOG.md | 4 ++- crates/common/src/cache/principals.rs | 22 ++++++++++++++ crates/dav/src/calendar/delete.rs | 4 ++- crates/dav/src/calendar/update.rs | 3 +- crates/jmap/src/calendar_event/set.rs | 2 +- crates/services/src/task_manager/imip.rs | 37 ++++++++++++++++++++++-- 6 files changed, 65 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5711eced..b0abbc4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,9 @@ 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. +- Calendar: + - Uppercase `MAILTO` calendar addresses become invalid SMTP recipients. + - Scheduling invitations on a shared, non-owned calendar fail with `MAIL FROM unauthorized`. ## [0.16.13] - 2026-07-12 diff --git a/crates/common/src/cache/principals.rs b/crates/common/src/cache/principals.rs index aa3fd414..e0a8419b 100644 --- a/crates/common/src/cache/principals.rs +++ b/crates/common/src/cache/principals.rs @@ -651,6 +651,28 @@ impl Server { self.build_account_info(account).await } + pub async fn scheduling_account_info( + &self, + authenticated_account_id: u32, + owner_account_id: u32, + ) -> trc::Result { + let account = self.account(authenticated_account_id).await?; + let mut account_info = self.build_account_info(account).await?; + + if owner_account_id != authenticated_account_id { + let owner_account = self.account(owner_account_id).await?; + let owner_account_info = self.build_account_info(owner_account).await?; + + for address in owner_account_info.addresses { + if !account_info.addresses.contains(&address) { + account_info.addresses.push(address); + } + } + } + + Ok(account_info) + } + pub async fn build_account_info(&self, account: Arc) -> trc::Result { let mut addresses = Vec::with_capacity(account.id_member_of.len() + account.addresses.len()); diff --git a/crates/dav/src/calendar/delete.rs b/crates/dav/src/calendar/delete.rs index 4c209924..520c3da2 100644 --- a/crates/dav/src/calendar/delete.rs +++ b/crates/dav/src/calendar/delete.rs @@ -72,7 +72,9 @@ impl CalendarDeleteRequestHandler for Server { .by_path(delete_path) .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; let document_id = delete_resource.document_id(); - let account_info = self.account_info(access_token.account_id()).await?; + let account_info = self + .scheduling_account_info(access_token.account_id(), account_id) + .await?; let send_itip = self.core.groupware.itip_enabled && !headers.no_schedule_reply && !account_info.addresses().is_empty() diff --git a/crates/dav/src/calendar/update.rs b/crates/dav/src/calendar/update.rs index b5217a3a..90aa8573 100644 --- a/crates/dav/src/calendar/update.rs +++ b/crates/dav/src/calendar/update.rs @@ -112,8 +112,9 @@ impl CalendarUpdateRequestHandler for Server { )); } }; + let account_info = self - .account_info(access_token.account_id()) + .scheduling_account_info(access_token.account_id(), account_id) .await .caused_by(trc::location!())?; diff --git a/crates/jmap/src/calendar_event/set.rs b/crates/jmap/src/calendar_event/set.rs index b8690217..9646b221 100644 --- a/crates/jmap/src/calendar_event/set.rs +++ b/crates/jmap/src/calendar_event/set.rs @@ -93,7 +93,7 @@ impl CalendarEventSet for Server { ) .await?; let account_info = self - .account_info(access_token.account_id()) + .scheduling_account_info(access_token.account_id(), account_id) .await .caused_by(trc::location!())?; let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?; diff --git a/crates/services/src/task_manager/imip.rs b/crates/services/src/task_manager/imip.rs index bbc6ed7f..e3983e7d 100644 --- a/crates/services/src/task_manager/imip.rs +++ b/crates/services/src/task_manager/imip.rs @@ -123,6 +123,37 @@ async fn send_imip( )); }; + let organizer_info = match server + .account_id_from_email(itip_message.from.as_str(), true) + .await + { + Ok(Some(sender_id)) if sender_id != account_id => { + match server.account_info(sender_id).await { + Ok(info) => Some(info), + Err(err) => { + trc::error!( + err.account_id(account_id) + .document_id(document_id) + .caused_by(trc::location!()) + .details("Failed to load organizer account for iMIP sender") + ); + None + } + } + } + Ok(_) => None, + Err(err) => { + trc::error!( + err.account_id(account_id) + .document_id(document_id) + .caused_by(trc::location!()) + .details("Failed to resolve organizer account for iMIP sender") + ); + None + } + }; + let sender_info = organizer_info.as_ref().unwrap_or(&account_info); + for recipient in itip_message.to.iter() { // Build template let tpl = build_itip_template( @@ -141,7 +172,7 @@ async fn send_imip( // Build message let message = MessageBuilder::new() .from(( - account_info.description().unwrap_or(account_info.name()), + sender_info.description().unwrap_or(sender_info.name()), itip_message.from.as_str(), )) .to(recipient.as_str()) @@ -188,14 +219,14 @@ async fn send_imip( // Send message let server_ = server.clone(); let server_instance = server_instance.clone(); - let account_info = account_info.clone(); + let sender_info = sender_info.clone(); let from = itip_message.from.to_string(); let to = recipient.to_string(); tokio::spawn(async move { let mut session = Session::::local( server_, server_instance, - SessionData::local(account_info, None, vec![], vec![], 0), + SessionData::local(sender_info, None, vec![], vec![], 0), ); // MAIL FROM