CalDAV Scheduling (closes #1514)
This commit is contained in:
@@ -110,7 +110,8 @@ pub async fn test(params: &mut JMAPTest) {
|
||||
ai_apis: Default::default(),
|
||||
spam_filter_llm: None,
|
||||
template_calendar_alarm: None,
|
||||
template_calendar_invite: None,
|
||||
template_scheduling_email: None,
|
||||
template_scheduling_web: None,
|
||||
}
|
||||
.into();
|
||||
config.assert_no_errors();
|
||||
@@ -180,7 +181,8 @@ impl EnterpriseCore for Core {
|
||||
ai_apis: Default::default(),
|
||||
spam_filter_llm: None,
|
||||
template_calendar_alarm: None,
|
||||
template_calendar_invite: None,
|
||||
template_scheduling_email: None,
|
||||
template_scheduling_web: None,
|
||||
}
|
||||
.into();
|
||||
self
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
use ahash::AHashMap;
|
||||
use calcard::{
|
||||
common::PartialDateTime,
|
||||
icalendar::{ICalendar, ICalendarMethod, ICalendarProperty, ICalendarValue},
|
||||
icalendar::{ICalendar, ICalendarProperty, ICalendarValue},
|
||||
};
|
||||
use groupware::scheduling::{
|
||||
ItipMessage,
|
||||
ItipMessage, ItipSummary,
|
||||
event_cancel::itip_cancel,
|
||||
event_create::itip_create,
|
||||
event_update::itip_update,
|
||||
@@ -337,14 +337,13 @@ pub fn test() {
|
||||
Command::Itip => {
|
||||
let mut commands = command.parameters.iter();
|
||||
last_itip = Some(Ok(vec![ItipMessage {
|
||||
method: ICalendarMethod::Request,
|
||||
from_organizer: false,
|
||||
from: commands
|
||||
.next()
|
||||
.expect("From parameter is required")
|
||||
.to_string(),
|
||||
to: commands.map(|s| s.to_string()).collect::<Vec<_>>(),
|
||||
changed_properties: vec![],
|
||||
summary: ItipSummary::Invite(vec![]),
|
||||
message: ICalendar::parse(&command.payload)
|
||||
.expect("Failed to parse iCalendar payload"),
|
||||
}]))
|
||||
@@ -369,15 +368,45 @@ impl ItipMessageExt for ItipMessage<ICalendar> {
|
||||
let mut f = String::new();
|
||||
let mut to = self.to.iter().map(|t| t.as_str()).collect::<Vec<_>>();
|
||||
to.sort_unstable();
|
||||
let mut changed = self
|
||||
.changed_properties
|
||||
.iter()
|
||||
.map(|p| p.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
changed.sort_unstable();
|
||||
writeln!(&mut f, "from: {}", self.from).unwrap();
|
||||
writeln!(&mut f, "to: {}", to.join(", ")).unwrap();
|
||||
writeln!(&mut f, "changes: {}", changed.join(", ")).unwrap();
|
||||
write!(&mut f, "summary: ").unwrap();
|
||||
let mut fields = Vec::new();
|
||||
match &self.summary {
|
||||
ItipSummary::Invite(itip_fields) => {
|
||||
writeln!(&mut f, "invite").unwrap();
|
||||
fields.push(itip_fields);
|
||||
}
|
||||
ItipSummary::Update {
|
||||
method,
|
||||
current,
|
||||
previous,
|
||||
} => {
|
||||
writeln!(&mut f, "update {}", method.as_str()).unwrap();
|
||||
fields.push(current);
|
||||
fields.push(previous);
|
||||
}
|
||||
ItipSummary::Cancel(itip_fields) => {
|
||||
writeln!(&mut f, "cancel").unwrap();
|
||||
fields.push(itip_fields);
|
||||
}
|
||||
ItipSummary::Rsvp { part_stat, current } => {
|
||||
writeln!(&mut f, "rsvp {}", part_stat.as_str()).unwrap();
|
||||
fields.push(current);
|
||||
}
|
||||
}
|
||||
for (pos, fields) in fields.into_iter().enumerate() {
|
||||
let prefix = if pos > 0 { "~summary." } else { "summary." };
|
||||
let mut fields = fields
|
||||
.iter()
|
||||
.map(|f| format!("{}: {:?}", f.name.as_str().to_lowercase(), f.value))
|
||||
.collect::<Vec<_>>();
|
||||
fields.sort_unstable();
|
||||
for field in fields {
|
||||
writeln!(&mut f, "{prefix}{}", field).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
write!(&mut f, "{}", normalize_ical(self.message.clone(), map)).unwrap();
|
||||
f
|
||||
}
|
||||
|
||||
@@ -4,18 +4,32 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::WebDavTest;
|
||||
use crate::{
|
||||
jmap::mailbox::destroy_all_mailboxes_for_account,
|
||||
webdav::{DummyWebDavClient, prop::ALL_DAV_PROPERTIES},
|
||||
};
|
||||
|
||||
use super::WebDavTest;
|
||||
use calcard::{
|
||||
common::timezone::Tz,
|
||||
icalendar::{
|
||||
ICalendarDay, ICalendarFrequency, ICalendarMethod, ICalendarParticipationStatus,
|
||||
ICalendarProperty, ICalendarRecurrenceRule, ICalendarWeekday,
|
||||
},
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use dav_proto::schema::property::{CalDavProperty, DavProperty, WebDavProperty};
|
||||
use email::cache::MessageCacheFetch;
|
||||
use groupware::cache::GroupwareCache;
|
||||
use groupware::{
|
||||
cache::GroupwareCache,
|
||||
scheduling::{
|
||||
ArchivedItipSummary, ItipField, ItipParticipant, ItipSummary, ItipTime, ItipValue,
|
||||
},
|
||||
};
|
||||
use hyper::StatusCode;
|
||||
use jmap_proto::types::collection::SyncCollection;
|
||||
use mail_parser::{DateTime, MessageParser};
|
||||
use services::task_manager::{Task, TaskAction, imip::build_itip_template};
|
||||
use std::str::FromStr;
|
||||
use store::write::now;
|
||||
|
||||
pub async fn test(test: &WebDavTest) {
|
||||
@@ -341,7 +355,7 @@ pub async fn test(test: &WebDavTest) {
|
||||
.body
|
||||
.unwrap();
|
||||
assert!(
|
||||
response.contains("Lunch") && response.contains("ACCEPTED"),
|
||||
response.contains("Lunch") && response.contains("RSVP has been recorded"),
|
||||
"failed for response: {response}"
|
||||
);
|
||||
let cals = fetch_icals(john_client).await;
|
||||
@@ -578,6 +592,224 @@ async fn fetch_icals(client: &DummyWebDavClient) -> Vec<CalEntry> {
|
||||
cals
|
||||
}
|
||||
|
||||
pub async fn test_build_itip_templates(server: &Server) {
|
||||
let dummy_access_token = AccessToken::from_id(0);
|
||||
let dummy_task = Task {
|
||||
account_id: 123,
|
||||
document_id: 156,
|
||||
due: 0,
|
||||
action: TaskAction::SendImip,
|
||||
};
|
||||
|
||||
for (idx, summary) in [
|
||||
ItipSummary::Invite(vec![
|
||||
ItipField {
|
||||
name: ICalendarProperty::Summary,
|
||||
value: ItipValue::Text("Lunch".to_string()),
|
||||
},
|
||||
ItipField {
|
||||
name: ICalendarProperty::Description,
|
||||
value: ItipValue::Text("Lunch at the cafe".to_string()),
|
||||
},
|
||||
ItipField {
|
||||
name: ICalendarProperty::Location,
|
||||
value: ItipValue::Text("Cafe Corner".to_string()),
|
||||
},
|
||||
ItipField {
|
||||
name: ICalendarProperty::Dtstart,
|
||||
value: ItipValue::Time(ItipTime {
|
||||
start: 1750616068,
|
||||
tz_id: Tz::from_str("New Zealand").unwrap().as_id(),
|
||||
}),
|
||||
},
|
||||
ItipField {
|
||||
name: ICalendarProperty::Attendee,
|
||||
value: ItipValue::Participants(vec![
|
||||
ItipParticipant {
|
||||
email: "jdoe@domain.com".to_string(),
|
||||
name: Some("John Doe".to_string()),
|
||||
is_organizer: true,
|
||||
},
|
||||
ItipParticipant {
|
||||
email: "jane@domain.com".to_string(),
|
||||
name: Some("Jane Smith".to_string()),
|
||||
is_organizer: false,
|
||||
},
|
||||
]),
|
||||
},
|
||||
]),
|
||||
ItipSummary::Cancel(vec![
|
||||
ItipField {
|
||||
name: ICalendarProperty::Summary,
|
||||
value: ItipValue::Text("Lunch".to_string()),
|
||||
},
|
||||
ItipField {
|
||||
name: ICalendarProperty::Description,
|
||||
value: ItipValue::Text("Lunch at the cafe".to_string()),
|
||||
},
|
||||
ItipField {
|
||||
name: ICalendarProperty::Location,
|
||||
value: ItipValue::Text("Cafe Corner".to_string()),
|
||||
},
|
||||
ItipField {
|
||||
name: ICalendarProperty::Dtstart,
|
||||
value: ItipValue::Time(ItipTime {
|
||||
start: 1750616068,
|
||||
tz_id: Tz::from_str("New Zealand").unwrap().as_id(),
|
||||
}),
|
||||
},
|
||||
]),
|
||||
ItipSummary::Rsvp {
|
||||
part_stat: ICalendarParticipationStatus::Accepted,
|
||||
current: vec![
|
||||
ItipField {
|
||||
name: ICalendarProperty::Summary,
|
||||
value: ItipValue::Text("Lunch".to_string()),
|
||||
},
|
||||
ItipField {
|
||||
name: ICalendarProperty::Description,
|
||||
value: ItipValue::Text("Lunch at the cafe".to_string()),
|
||||
},
|
||||
ItipField {
|
||||
name: ICalendarProperty::Location,
|
||||
value: ItipValue::Text("Cafe Corner".to_string()),
|
||||
},
|
||||
ItipField {
|
||||
name: ICalendarProperty::Dtstart,
|
||||
value: ItipValue::Time(ItipTime {
|
||||
start: 1750616068,
|
||||
tz_id: Tz::from_str("New Zealand").unwrap().as_id(),
|
||||
}),
|
||||
},
|
||||
ItipField {
|
||||
name: ICalendarProperty::Rrule,
|
||||
value: ItipValue::Rrule(Box::new(ICalendarRecurrenceRule {
|
||||
freq: ICalendarFrequency::Weekly,
|
||||
until: None,
|
||||
count: Some(2),
|
||||
interval: Some(3),
|
||||
bysecond: Default::default(),
|
||||
byday: vec![
|
||||
ICalendarDay {
|
||||
ordwk: None,
|
||||
weekday: ICalendarWeekday::Monday,
|
||||
},
|
||||
ICalendarDay {
|
||||
ordwk: None,
|
||||
weekday: ICalendarWeekday::Wednesday,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
ItipSummary::Rsvp {
|
||||
part_stat: ICalendarParticipationStatus::Declined,
|
||||
current: vec![
|
||||
ItipField {
|
||||
name: ICalendarProperty::Summary,
|
||||
value: ItipValue::Text("Lunch".to_string()),
|
||||
},
|
||||
ItipField {
|
||||
name: ICalendarProperty::Description,
|
||||
value: ItipValue::Text("Lunch at the cafe".to_string()),
|
||||
},
|
||||
ItipField {
|
||||
name: ICalendarProperty::Location,
|
||||
value: ItipValue::Text("Cafe Corner".to_string()),
|
||||
},
|
||||
ItipField {
|
||||
name: ICalendarProperty::Dtstart,
|
||||
value: ItipValue::Time(ItipTime {
|
||||
start: 1750616068,
|
||||
tz_id: Tz::from_str("New Zealand").unwrap().as_id(),
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
ItipSummary::Update {
|
||||
method: ICalendarMethod::Request,
|
||||
current: vec![
|
||||
ItipField {
|
||||
name: ICalendarProperty::Summary,
|
||||
value: ItipValue::Text("Lunch".to_string()),
|
||||
},
|
||||
ItipField {
|
||||
name: ICalendarProperty::Description,
|
||||
value: ItipValue::Text("Lunch at the cafe".to_string()),
|
||||
},
|
||||
ItipField {
|
||||
name: ICalendarProperty::Location,
|
||||
value: ItipValue::Text("Cafe Corner".to_string()),
|
||||
},
|
||||
ItipField {
|
||||
name: ICalendarProperty::Dtstart,
|
||||
value: ItipValue::Time(ItipTime {
|
||||
start: 1750616068,
|
||||
tz_id: Tz::from_str("New Zealand").unwrap().as_id(),
|
||||
}),
|
||||
},
|
||||
ItipField {
|
||||
name: ICalendarProperty::Attendee,
|
||||
value: ItipValue::Participants(vec![
|
||||
ItipParticipant {
|
||||
email: "jdoe@domain.com".to_string(),
|
||||
name: Some("John Doe".to_string()),
|
||||
is_organizer: true,
|
||||
},
|
||||
ItipParticipant {
|
||||
email: "jane@domain.com".to_string(),
|
||||
name: Some("Jane Smith".to_string()),
|
||||
is_organizer: false,
|
||||
},
|
||||
]),
|
||||
},
|
||||
],
|
||||
previous: vec![
|
||||
ItipField {
|
||||
name: ICalendarProperty::Summary,
|
||||
value: ItipValue::Text("Dinner".to_string()),
|
||||
},
|
||||
ItipField {
|
||||
name: ICalendarProperty::Description,
|
||||
value: ItipValue::Text("Dinner at the cafe".to_string()),
|
||||
},
|
||||
ItipField {
|
||||
name: ICalendarProperty::Dtstart,
|
||||
value: ItipValue::Time(ItipTime {
|
||||
start: 1750916068,
|
||||
tz_id: Tz::from_str("New Zealand").unwrap().as_id(),
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&summary)
|
||||
.unwrap()
|
||||
.to_vec();
|
||||
let summary = rkyv::access::<ArchivedItipSummary, rkyv::rancor::Error>(&bytes).unwrap();
|
||||
|
||||
let html = build_itip_template(
|
||||
server,
|
||||
&dummy_access_token,
|
||||
&dummy_task,
|
||||
"john.doe@example.org",
|
||||
"jane.smith@example.net",
|
||||
summary,
|
||||
"124",
|
||||
)
|
||||
.await;
|
||||
|
||||
println!("iTIP template {idx}: {}", html.subject);
|
||||
std::fs::write(format!("itip_template_{idx}.html"), html.body)
|
||||
.expect("Failed to write iTIP template to file");
|
||||
}
|
||||
}
|
||||
|
||||
const TEST_ITIP: &str = r#"BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Example Corp.//CalDAV Client//EN
|
||||
|
||||
@@ -74,7 +74,9 @@ pub async fn webdav_tests() {
|
||||
)
|
||||
.await;
|
||||
|
||||
basic::test(&handle).await;
|
||||
//test_build_itip_templates(&handle.server).await;
|
||||
|
||||
/* basic::test(&handle).await;
|
||||
put_get::test(&handle).await;
|
||||
mkcol::test(&handle).await;
|
||||
copy_move::test(&handle).await;
|
||||
@@ -87,7 +89,7 @@ pub async fn webdav_tests() {
|
||||
card_query::test(&handle).await;
|
||||
cal_query::test(&handle).await;
|
||||
cal_alarm::test(&handle).await;
|
||||
cal_itip::test();
|
||||
cal_itip::test();*/
|
||||
cal_scheduling::test(&handle).await;
|
||||
|
||||
// Print elapsed time
|
||||
|
||||
Reference in New Issue
Block a user