mirror of
https://github.com/flectar/mail.git
synced 2026-11-11 17:57:54 +00:00
fix: match Outlook meetings with their invitations
This commit is contained in:
1 parent
bf4faa76e2
commit
b93b1bb7b9
6 files changed
+337
-14
No files matched your search
@@ -44,6 +44,12 @@ pub struct QueuedAnswer {
|
||||
pub comment: Option<String>,
|
||||
/// Whether the organizer is told, where the server lets that be chosen.
|
||||
pub notify: bool,
|
||||
/// For an invitation known from mail whose series Exchange holds: the
|
||||
/// series' Graph id, answered in its stead, and a calendar holding it.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub meeting: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub calendar_id: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -482,12 +488,13 @@ impl Cycle<'_> {
|
||||
action: &repo::actions::PendingAction,
|
||||
) -> Result<()> {
|
||||
let answer: QueuedAnswer = serde_json::from_value(action.payload.clone())?;
|
||||
let event_id = answer.event_id;
|
||||
let (event_id, held_in) = (answer.event_id, answer.calendar_id);
|
||||
let (row, calendar) = self
|
||||
.db
|
||||
.read(move |conn| {
|
||||
let row = repo::calendar::sync_row_for(conn, event_id)?;
|
||||
let calendar = match row.as_ref().and_then(|row| row.event.calendar_id) {
|
||||
let calendar = match held_in.or(row.as_ref().and_then(|row| row.event.calendar_id))
|
||||
{
|
||||
Some(id) => repo::caldav::get_calendar(conn, id)?,
|
||||
None => None,
|
||||
};
|
||||
@@ -511,7 +518,8 @@ impl Cycle<'_> {
|
||||
let comment = answer.comment.as_deref();
|
||||
match session {
|
||||
Session::Graph { token } => {
|
||||
let meeting = row.caldav_href.as_deref().ok_or_else(|| {
|
||||
let meeting = answer.meeting.as_deref().or(row.caldav_href.as_deref());
|
||||
let meeting = meeting.ok_or_else(|| {
|
||||
CoreError::Other("the meeting isn't in the Outlook calendar yet".into())
|
||||
})?;
|
||||
crate::graph::answer_event(token, meeting, &answer.partstat, comment, answer.notify)
|
||||
|
||||
@@ -144,6 +144,7 @@ CREATE INDEX idx_calendar_actions_due
|
||||
"migrations/calendar_003_invitation_dtstamp.sql"
|
||||
)),
|
||||
Migration::Plain(include_str!("migrations/calendar_004_scheduling.sql")),
|
||||
Migration::Plain(include_str!("migrations/calendar_005_series_uid.sql")),
|
||||
];
|
||||
pub const LATEST_VERSION: i64 = MIGRATIONS.len() as i64;
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-- The series an occurrence synced from Exchange belongs to, by the UID its
|
||||
-- invitation carried, so that a recurring meeting known from mail meets its
|
||||
-- occurrences there (MS-OXCICAL keeps that UID inside Exchange's own id).
|
||||
ALTER TABLE calendar_events ADD COLUMN series_uid TEXT;
|
||||
CREATE INDEX idx_events_series_uid ON calendar_events(account_id, series_uid)
|
||||
WHERE series_uid IS NOT NULL;
|
||||
@@ -176,7 +176,10 @@ pub fn apply_invitation(
|
||||
}
|
||||
Some("COUNTER" | "REFRESH" | "DECLINECOUNTER") => {}
|
||||
Some("CANCEL") => apply_cancel(conn, &invitation)?,
|
||||
_ => apply_request(conn, &invitation, own)?,
|
||||
_ => {
|
||||
apply_request(conn, &invitation, own)?;
|
||||
join_held_series(conn, account_id, uid)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -861,20 +864,64 @@ pub fn delete_by_href(conn: &Connection, calendar_id: i64, href: &str) -> Result
|
||||
|
||||
/// Record which (unstored) series master an occurrence/exception row belongs
|
||||
/// to, so a later series-wide deletion can find it via `delete_by_href`.
|
||||
/// Record that the occurrence `href` belongs to Exchange's series
|
||||
/// `series_master_id`, known by `series_uid`, the UID its invitation
|
||||
/// carried. An invitation to that series known from mail is then the
|
||||
/// series Exchange holds: it is answered there and shown from there.
|
||||
pub fn set_series_master(
|
||||
conn: &Connection,
|
||||
calendar_id: i64,
|
||||
href: &str,
|
||||
series_master_id: &str,
|
||||
series_uid: &str,
|
||||
) -> Result<()> {
|
||||
conn.execute(
|
||||
"UPDATE calendar_events SET series_master_id = ?3
|
||||
"UPDATE calendar_events SET series_master_id = ?3, series_uid = ?4
|
||||
WHERE calendar_id = ?1 AND caldav_href = ?2",
|
||||
params![calendar_id, href, series_master_id],
|
||||
params![calendar_id, href, series_master_id, series_uid],
|
||||
)?;
|
||||
conn.execute(
|
||||
"UPDATE calendar_events SET series_master_id = ?3
|
||||
WHERE calendar_id IS NULL AND message_id IS NOT NULL AND ical_uid = ?2
|
||||
AND account_id = (SELECT account_id FROM calendars WHERE id = ?1)",
|
||||
params![calendar_id, series_uid, series_master_id],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tie an invitation known from mail to the series Exchange already holds
|
||||
/// for it, if any (see [`set_series_master`]).
|
||||
fn join_held_series(conn: &Connection, account_id: i64, uid: &str) -> Result<()> {
|
||||
conn.execute(
|
||||
"UPDATE calendar_events SET series_master_id = (
|
||||
SELECT held.series_master_id FROM calendar_events held
|
||||
WHERE held.account_id = ?1 AND held.series_uid = ?2
|
||||
AND held.calendar_id IS NOT NULL AND held.series_master_id IS NOT NULL
|
||||
LIMIT 1)
|
||||
WHERE account_id = ?1 AND ical_uid = ?2 AND calendar_id IS NULL
|
||||
AND message_id IS NOT NULL AND series_master_id IS NULL",
|
||||
params![account_id, uid],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// For an invitation known from mail whose series Exchange holds: that
|
||||
/// series' Graph id, and a calendar holding its occurrences.
|
||||
pub fn held_series(conn: &Connection, event_id: i64) -> Result<Option<(String, i64)>> {
|
||||
Ok(conn
|
||||
.query_row(
|
||||
"SELECT mail.series_master_id, held.calendar_id
|
||||
FROM calendar_events mail
|
||||
JOIN calendar_events held ON held.series_master_id = mail.series_master_id
|
||||
AND held.account_id = mail.account_id AND held.calendar_id IS NOT NULL
|
||||
WHERE mail.id = ?1 AND mail.calendar_id IS NULL
|
||||
LIMIT 1",
|
||||
params![event_id],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)
|
||||
.optional()?)
|
||||
}
|
||||
|
||||
/// Record our RSVP (ACCEPTED | TENTATIVE | DECLINED).
|
||||
pub fn set_rsvp(conn: &Connection, event_id: i64, status: &str) -> Result<()> {
|
||||
conn.execute(
|
||||
@@ -927,6 +974,9 @@ pub fn list_range(conn: &Connection, start_ms: i64, end_ms: i64) -> Result<Vec<C
|
||||
AND (calendar_id IS NULL OR EXISTS (
|
||||
SELECT 1 FROM calendars c WHERE c.id = calendar_events.calendar_id AND c.enabled = 1
|
||||
))
|
||||
-- An invitation known from mail whose series Exchange holds shows
|
||||
-- from there.
|
||||
AND (calendar_id IS NOT NULL OR message_id IS NULL OR series_master_id IS NULL)
|
||||
ORDER BY starts_at ASC, id ASC"
|
||||
))?;
|
||||
let rows = stmt
|
||||
@@ -943,6 +993,9 @@ pub fn recurring_masters(conn: &Connection, end_ms: i64) -> Result<Vec<SyncRow>>
|
||||
AND (calendar_id IS NULL OR EXISTS (
|
||||
SELECT 1 FROM calendars c WHERE c.id = calendar_events.calendar_id AND c.enabled = 1
|
||||
))
|
||||
-- An invitation known from mail whose series Exchange holds shows
|
||||
-- from there.
|
||||
AND (calendar_id IS NOT NULL OR message_id IS NULL OR series_master_id IS NULL)
|
||||
ORDER BY starts_at ASC, id ASC"
|
||||
))?;
|
||||
let rows = stmt
|
||||
@@ -977,6 +1030,9 @@ pub fn upcoming_for_notify(
|
||||
AND (calendar_id IS NULL OR EXISTS (
|
||||
SELECT 1 FROM calendars c WHERE c.id = calendar_events.calendar_id AND c.enabled = 1
|
||||
))
|
||||
-- An invitation known from mail whose series Exchange holds shows
|
||||
-- from there.
|
||||
AND (calendar_id IS NOT NULL OR message_id IS NULL OR series_master_id IS NULL)
|
||||
AND starts_at > ?1 AND starts_at <= ?1 + ?2
|
||||
AND (notified_at IS NULL OR notified_at < starts_at)
|
||||
ORDER BY starts_at LIMIT 20"
|
||||
@@ -1284,6 +1340,71 @@ mod tests {
|
||||
assert_eq!(ical_raw(&c, id).unwrap(), raw);
|
||||
}
|
||||
|
||||
/// Exchange's occurrences of the SERIES meeting, as Graph sync stores
|
||||
/// them: each by its own Graph id, tied to the series' Graph id.
|
||||
fn exchange_occurrences(c: &Connection, calendar: i64) {
|
||||
for (href, start) in [
|
||||
("occ-1", at("20261102T090000Z")),
|
||||
("occ-2", at("20261109T090000Z")),
|
||||
] {
|
||||
let ev = IcsEvent {
|
||||
uid: href.into(),
|
||||
summary: Some("Weekly".into()),
|
||||
starts_at_ms: start,
|
||||
ends_at_ms: Some(start + 3_600_000),
|
||||
..Default::default()
|
||||
};
|
||||
upsert_remote(c, 1, calendar, href, "\"e\"", "", &ev).unwrap();
|
||||
set_series_master(c, calendar, href, "master-1", "weekly@test").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn shown(c: &Connection) -> Vec<i64> {
|
||||
list_range(c, 0, i64::MAX / 2)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|event| event.id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_recurring_meeting_exchange_holds_shows_from_there_alone() {
|
||||
let c = testutil::calendar_conn();
|
||||
let calendar = seed_calendar(&c);
|
||||
exchange_occurrences(&c, calendar);
|
||||
apply(&c, 80, SERIES);
|
||||
let mail = for_message(&c, 80).unwrap()[0].id;
|
||||
assert_eq!(shown(&c).len(), 2, "Exchange's two occurrences");
|
||||
assert!(!shown(&c).contains(&mail));
|
||||
assert!(
|
||||
recurring_masters(&c, i64::MAX / 2)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(|master| master.event.id != mail),
|
||||
"nor expanded from mail"
|
||||
);
|
||||
assert_eq!(
|
||||
held_series(&c, mail).unwrap(),
|
||||
Some(("master-1".into(), calendar))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_invitation_read_first_joins_its_series_when_exchange_syncs() {
|
||||
let c = testutil::calendar_conn();
|
||||
let calendar = seed_calendar(&c);
|
||||
apply(&c, 80, SERIES);
|
||||
let mail = for_message(&c, 80).unwrap()[0].id;
|
||||
assert!(shown(&c).contains(&mail));
|
||||
assert_eq!(held_series(&c, mail).unwrap(), None);
|
||||
exchange_occurrences(&c, calendar);
|
||||
assert!(!shown(&c).contains(&mail));
|
||||
assert_eq!(
|
||||
held_series(&c, mail).unwrap(),
|
||||
Some(("master-1".into(), calendar))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_invitation_to_one_occurrence_follows_that_occurrence() {
|
||||
let c = testutil::calendar_conn();
|
||||
@@ -1596,8 +1717,8 @@ mod tests {
|
||||
let mut day2 = sample_ics_event();
|
||||
day2.uid = "occ-2@graph".into();
|
||||
upsert_remote(&c, 1, cal, "occ-2", "\"e1\"", "", &day2).unwrap();
|
||||
set_series_master(&c, cal, "occ-1", "series-master-1").unwrap();
|
||||
set_series_master(&c, cal, "occ-2", "series-master-1").unwrap();
|
||||
set_series_master(&c, cal, "occ-1", "series-master-1", "series-1@test").unwrap();
|
||||
set_series_master(&c, cal, "occ-2", "series-master-1", "series-1@test").unwrap();
|
||||
|
||||
assert_eq!(list_range(&c, 0, i64::MAX / 2).unwrap().len(), 2);
|
||||
|
||||
|
||||
@@ -228,7 +228,17 @@ async fn sync_calendar(
|
||||
});
|
||||
}
|
||||
if let Some(master) = &ev.series_master_id {
|
||||
repo::calendar::set_series_master(&tx, calendar_id, &ev.id, master)?;
|
||||
let series = ev
|
||||
.ical_uid
|
||||
.as_deref()
|
||||
.map_or_else(|| master.clone(), series_uid);
|
||||
repo::calendar::set_series_master(
|
||||
&tx,
|
||||
calendar_id,
|
||||
&ev.id,
|
||||
master,
|
||||
&series,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
tx.commit()?;
|
||||
@@ -273,13 +283,75 @@ async fn sync_calendar(
|
||||
Ok((changed_any, new_events))
|
||||
}
|
||||
|
||||
/// The class id every Exchange meeting's GlobalObjectId starts with
|
||||
/// (MS-OXOCAL 2.2.1.27).
|
||||
const GLOBAL_OBJECT_ID_CLASS: [u8; 16] = [
|
||||
0x04, 0x00, 0x00, 0x00, 0x82, 0x00, 0xE0, 0x00, 0x74, 0xC5, 0xB7, 0x10, 0x1A, 0x82, 0xE0, 0x08,
|
||||
];
|
||||
|
||||
/// The iCalendar UID a meeting got from outside Exchange, when `ical_uid`
|
||||
/// is the hex form of the GlobalObjectId Exchange keeps it in. After the
|
||||
/// class id, a date, a creation time and eight reserved bytes come the
|
||||
/// data's length and the data, which for such a UID is "vCal-Uid", then
|
||||
/// 01 00 00 00, then the UID (MS-OXCICAL 2.1.3.1.1.20.26). A meeting made in
|
||||
/// Exchange has no such data: its hex id is its UID.
|
||||
fn third_party_uid(ical_uid: &str) -> Option<String> {
|
||||
if ical_uid.len() < 80 || !ical_uid.len().is_multiple_of(2) {
|
||||
return None;
|
||||
}
|
||||
let bytes = (0..ical_uid.len())
|
||||
.step_by(2)
|
||||
.map(|at| {
|
||||
ical_uid
|
||||
.get(at..at + 2)
|
||||
.and_then(|pair| u8::from_str_radix(pair, 16).ok())
|
||||
})
|
||||
.collect::<Option<Vec<u8>>>()?;
|
||||
if bytes.get(..16)? != GLOBAL_OBJECT_ID_CLASS {
|
||||
return None;
|
||||
}
|
||||
let size = u32::from_le_bytes(bytes.get(36..40)?.try_into().ok()?) as usize;
|
||||
let data = bytes.get(40..40usize.checked_add(size)?)?;
|
||||
let uid = data.strip_prefix(b"vCal-Uid\x01\x00\x00\x00")?;
|
||||
let uid = std::str::from_utf8(uid).ok()?.trim_end_matches('\0');
|
||||
(!uid.is_empty()).then(|| uid.to_string())
|
||||
}
|
||||
|
||||
/// The UID of the series an occurrence's iCalUId belongs to: the UID its
|
||||
/// invitation carried, or, for a meeting made in Exchange, Exchange's id for
|
||||
/// the series, which is the occurrence's with its date left out
|
||||
/// (PidLidCleanGlobalObjectId, MS-OXOCAL 2.2.1.28).
|
||||
fn series_uid(ical_uid: &str) -> String {
|
||||
if let Some(uid) = third_party_uid(ical_uid) {
|
||||
return uid;
|
||||
}
|
||||
let is_object_id = ical_uid.len() >= 80
|
||||
&& ical_uid.is_ascii()
|
||||
&& ical_uid[..32].eq_ignore_ascii_case(&hex(&GLOBAL_OBJECT_ID_CLASS));
|
||||
if !is_object_id {
|
||||
return ical_uid.to_string();
|
||||
}
|
||||
// Bytes 16 to 19 hold the occurrence's date.
|
||||
format!("{}00000000{}", &ical_uid[..32], &ical_uid[40..]).to_ascii_uppercase()
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|byte| format!("{byte:02X}")).collect()
|
||||
}
|
||||
|
||||
/// Map a Graph delta event onto the shared calendar row shape. Occurrences
|
||||
/// and exceptions of a series all share one iCalUId, so their local uid is
|
||||
/// the (unique) Graph id instead - only single instances keep the iCalUId,
|
||||
/// which lets the row adopt an earlier mail-invite copy of the same event.
|
||||
/// A meeting from outside Exchange is known by the UID its invitation
|
||||
/// carried, which Exchange keeps inside its own id.
|
||||
fn ics_event(ev: &graph::DeltaEvent) -> IcsEvent {
|
||||
let uid = match ev.event_type.as_deref() {
|
||||
Some("singleInstance") | None => ev.ical_uid.clone().unwrap_or_else(|| ev.id.clone()),
|
||||
Some("singleInstance") | None => ev
|
||||
.ical_uid
|
||||
.clone()
|
||||
.map(|uid| third_party_uid(&uid).unwrap_or(uid))
|
||||
.unwrap_or_else(|| ev.id.clone()),
|
||||
_ => ev.id.clone(),
|
||||
};
|
||||
IcsEvent {
|
||||
@@ -319,3 +391,65 @@ fn ics_event(ev: &graph::DeltaEvent) -> IcsEvent {
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A GlobalObjectId as Exchange builds it, in hex, around `data`.
|
||||
fn global_object_id(data: &[u8]) -> String {
|
||||
occurrence_object_id([0; 4], data)
|
||||
}
|
||||
|
||||
/// The same for one occurrence: `date` is its year (two bytes), month
|
||||
/// and day.
|
||||
fn occurrence_object_id(date: [u8; 4], data: &[u8]) -> String {
|
||||
let mut bytes = GLOBAL_OBJECT_ID_CLASS.to_vec();
|
||||
bytes.extend(date);
|
||||
bytes.extend([0x10, 0x8D, 0x2B, 0x33, 0x45, 0x67, 0xDC, 0x01]); // creation time
|
||||
bytes.extend([0; 8]);
|
||||
bytes.extend((data.len() as u32).to_le_bytes());
|
||||
bytes.extend(data);
|
||||
bytes.iter().map(|byte| format!("{byte:02X}")).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_meeting_from_outside_exchange_keeps_its_own_uid() {
|
||||
let mut data = b"vCal-Uid\x01\x00\x00\x00".to_vec();
|
||||
data.extend(b"review-1@example.com\0");
|
||||
let hex = global_object_id(&data);
|
||||
assert_eq!(
|
||||
third_party_uid(&hex).as_deref(),
|
||||
Some("review-1@example.com")
|
||||
);
|
||||
assert_eq!(
|
||||
third_party_uid(&hex.to_ascii_lowercase()).as_deref(),
|
||||
Some("review-1@example.com")
|
||||
);
|
||||
|
||||
// A meeting made in Exchange, and anything else, keeps its iCalUId.
|
||||
let native = global_object_id(&[0x3A, 0x9F, 0x01, 0xC4, 0x77, 0x0E, 0x5B, 0x12]);
|
||||
assert_eq!(third_party_uid(&native), None);
|
||||
assert_eq!(third_party_uid("review-1@example.com"), None);
|
||||
assert_eq!(third_party_uid(&hex[..hex.len() - 4]), None, "cut short");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_occurrence_belongs_to_the_series_its_invitation_named() {
|
||||
let november_16 = [0x07, 0xEA, 0x0B, 0x10];
|
||||
let mut data = b"vCal-Uid\x01\x00\x00\x00".to_vec();
|
||||
data.extend(b"weekly@example.com\0");
|
||||
assert_eq!(
|
||||
series_uid(&occurrence_object_id(november_16, &data)),
|
||||
"weekly@example.com"
|
||||
);
|
||||
|
||||
// A series made in Exchange is its occurrences' id without the date.
|
||||
let native = [0x3A, 0x9F, 0x01, 0xC4, 0x77, 0x0E, 0x5B, 0x12];
|
||||
assert_eq!(
|
||||
series_uid(&occurrence_object_id(november_16, &native).to_ascii_lowercase()),
|
||||
global_object_id(&native)
|
||||
);
|
||||
assert_eq!(series_uid("weekly@example.com"), "weekly@example.com");
|
||||
}
|
||||
}
|
||||
@@ -6508,13 +6508,19 @@ impl Core {
|
||||
_ => return Err(CoreError::Other("invalid RSVP response".into())),
|
||||
};
|
||||
let event_id = args.event_id;
|
||||
let (row, held_by, scheduling, default) = self
|
||||
let (row, held_by, held_series, scheduling, default) = self
|
||||
.calendar_db
|
||||
.read(move |conn| {
|
||||
let row = repo::calendar::sync_row_for(conn, event_id)?
|
||||
.ok_or_else(|| CoreError::NotFound("event".into()))?;
|
||||
// The calendar holding the server's copy, when there is one.
|
||||
let held_in = row.event.calendar_id.filter(|_| row.caldav_href.is_some());
|
||||
// The calendar holding the server's copy, when there is one,
|
||||
// or the series Exchange holds for an invitation from mail.
|
||||
let held_series = repo::calendar::held_series(conn, event_id)?;
|
||||
let held_in = row
|
||||
.event
|
||||
.calendar_id
|
||||
.filter(|_| row.caldav_href.is_some())
|
||||
.or(held_series.as_ref().map(|(_, calendar_id)| *calendar_id));
|
||||
let held_by = match held_in {
|
||||
Some(id) => repo::caldav::get_calendar(conn, id)?,
|
||||
None => None,
|
||||
@@ -6525,7 +6531,7 @@ impl Core {
|
||||
None => None,
|
||||
};
|
||||
let default = repo::caldav::default_calendar(conn, row.event.account_id)?;
|
||||
Ok((row, held_by, scheduling, default))
|
||||
Ok((row, held_by, held_series, scheduling, default))
|
||||
})
|
||||
.await?;
|
||||
let (uid, sequence, invitation) = (row.ical_uid, row.sequence, row.ical_raw);
|
||||
@@ -6618,6 +6624,8 @@ impl Core {
|
||||
attendee: answering.attendee.email.clone(),
|
||||
comment: args.comment.clone(),
|
||||
notify: args.notify,
|
||||
meeting: held_series.as_ref().map(|(meeting, _)| meeting.clone()),
|
||||
calendar_id: held_series.as_ref().map(|(_, calendar_id)| *calendar_id),
|
||||
};
|
||||
let payload = serde_json::to_value(&answer)?;
|
||||
self.calendar_db
|
||||
@@ -13661,6 +13669,51 @@ mod invitation_tests {
|
||||
assert_eq!(states, ["cancelled", "pending"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_invitation_whose_series_exchange_holds_is_answered_there() {
|
||||
let (core, _temp, id) = invited().await;
|
||||
let event = invitations(&core, id).await[0].id;
|
||||
core.calendar_db
|
||||
.write(|conn| {
|
||||
conn.execute(
|
||||
"INSERT INTO calendar_connections (id, account_id, kind, base_url)
|
||||
VALUES (5, 1, 'microsoft', 'https://graph.microsoft.com/')",
|
||||
[],
|
||||
)?;
|
||||
conn.execute(
|
||||
"INSERT INTO calendars (id, account_id, connection_id, url, enabled)
|
||||
VALUES (50, 1, 5, 'outlook-calendar', 1)",
|
||||
[],
|
||||
)?;
|
||||
let occurrence = calendar::IcsEvent {
|
||||
uid: "occ-1".into(),
|
||||
summary: Some("Design review".into()),
|
||||
starts_at_ms: 1_000,
|
||||
ends_at_ms: Some(2_000),
|
||||
..Default::default()
|
||||
};
|
||||
repo::calendar::upsert_remote(conn, 1, 50, "occ-1", "", "", &occurrence)?;
|
||||
repo::calendar::set_series_master(
|
||||
conn,
|
||||
50,
|
||||
"occ-1",
|
||||
"master-1",
|
||||
"review-1@example.com",
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
core.rsvp_event(answer(event, "accepted", None))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(drafts(&core).await, 0, "Exchange tells the organizer");
|
||||
let queued = queued_answers(&core).await;
|
||||
assert_eq!(queued.len(), 1);
|
||||
assert_eq!(queued[0].1["meeting"], "master-1");
|
||||
assert_eq!(queued[0].1["calendarId"], 50);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_caldav_server_that_does_not_schedule_keeps_the_answer_and_mail_tells() {
|
||||
let (core, _temp, id) = invited().await;
|
||||
|
||||
Reference in new issue
Block a user