1pub mod batch;
4mod calendars;
5pub mod delta;
6pub mod events;
7mod mail;
8mod me;
9mod people;
10
11pub use calendars::list_calendars;
12pub use events::{
13 EventsPage, NewEvent, ProposedTime, RsvpKind, cancel_event, create_event, delete_event,
14 get_event, list_calendar_view, list_events_at, move_event_to_calendar, move_time, rsvp_event,
15 update_event,
16};
17pub use mail::{
18 InboxPage, MailFolder, Outgoing, add_attachment, create_child_folder, create_draft,
19 create_forward_draft, create_mail_folder, create_reply_all_draft, create_reply_draft,
20 delete_attachment, delete_mail_folder, delete_message, fetch_message_headers, forward_message,
21 get_attachment_bytes, get_categories, get_message, list_attachments, list_child_folders,
22 list_drafts, list_folder_messages, list_inbox, list_mail_folders, list_messages_at, mark_read,
23 mark_unread, move_message, reply_all_message, reply_message, search_folder_messages,
24 search_messages, send_draft, send_mail, set_categories, set_flag, unsubscribe_one_click,
25 update_draft, update_draft_recipients,
26};
27pub use me::{Me, get_me};
28pub use people::{Person, list_people};
29
30use crate::auth::AuthClient;
31use crate::auth::config;
32use crate::error::ClientError;
33use pidge_core::Message;
34
35const MAX_ATTEMPTS: u32 = 4;
37
38fn is_transient(status: reqwest::StatusCode) -> bool {
40 matches!(status.as_u16(), 429 | 503 | 504)
41}
42
43pub(crate) async fn send_with_retry(
54 req: reqwest::RequestBuilder,
55) -> Result<reqwest::Response, ClientError> {
56 let mut attempt: u32 = 0;
57 loop {
58 let this_try = match req.try_clone() {
59 Some(clone) => clone,
60 None => return Ok(req.send().await?),
61 };
62 let resp = this_try.send().await?;
63 let status = resp.status();
64 if !is_transient(status) {
65 return Ok(resp);
66 }
67 let retry_after = resp
68 .headers()
69 .get(reqwest::header::RETRY_AFTER)
70 .and_then(|v| v.to_str().ok())
71 .and_then(|v| v.parse::<u64>().ok());
72 attempt += 1;
73 if attempt >= MAX_ATTEMPTS {
74 return Err(ClientError::Throttled { retry_after });
75 }
76 let backoff = retry_after
77 .map(std::time::Duration::from_secs)
78 .unwrap_or_else(|| {
79 let jitter = std::time::Duration::from_millis(u64::from(attempt) * 83 % 250);
80 std::time::Duration::from_secs(1u64 << attempt.min(4)) / 2 + jitter
81 });
82 tracing::debug!(
83 status = status.as_u16(),
84 attempt,
85 ?backoff,
86 "retrying Graph request"
87 );
88 tokio::time::sleep(backoff).await;
89 }
90}
91
92pub(crate) fn check_continuation(url: &str, base_url: &str) -> Result<(), ClientError> {
98 let origin = |u: &str| url::Url::parse(u).ok().map(|u| u.origin());
99 let allowed = origin(url).is_some_and(|o| {
100 o.is_tuple()
101 && (Some(&o) == origin(config::GRAPH_BASE).as_ref()
102 || Some(&o) == origin(base_url).as_ref())
103 });
104 if allowed {
105 Ok(())
106 } else {
107 Err(ClientError::Graph {
108 status: 400,
109 message: "refusing to follow a continuation link off graph.microsoft.com".into(),
110 })
111 }
112}
113
114pub struct GraphClient {
116 auth: AuthClient,
117 http: reqwest::Client,
118 base_url: String,
119 one_click: mail::OneClickPolicy,
121}
122
123impl GraphClient {
124 pub fn new(auth: AuthClient) -> Result<Self, ClientError> {
125 Ok(Self {
126 auth,
127 http: reqwest::Client::builder()
128 .user_agent(format!("pidge/{}", env!("CARGO_PKG_VERSION")))
129 .build()?,
130 base_url: config::GRAPH_BASE.to_string(),
131 one_click: mail::OneClickPolicy::PublicOnly,
132 })
133 }
134
135 pub fn for_test(auth: AuthClient, base_url: impl Into<String>) -> Self {
136 Self {
137 auth,
138 http: reqwest::Client::new(),
139 base_url: base_url.into(),
140 one_click: mail::OneClickPolicy::AllowLoopback,
141 }
142 }
143
144 pub fn auth(&self) -> &AuthClient {
145 &self.auth
146 }
147
148 pub async fn me(&self, access_token: &str) -> Result<Me, ClientError> {
150 get_me(&self.http, &self.base_url, access_token).await
151 }
152
153 pub async fn list_inbox(
156 &self,
157 account: &str,
158 limit: usize,
159 skip: usize,
160 unread_only: bool,
161 ) -> Result<InboxPage, ClientError> {
162 let token = self.auth.get_valid_token(account).await?;
163 list_inbox(
164 &self.http,
165 &self.base_url,
166 &token,
167 account,
168 limit,
169 skip,
170 unread_only,
171 )
172 .await
173 }
174
175 pub async fn list_folder(
177 &self,
178 account: &str,
179 folder_id: &str,
180 limit: usize,
181 skip: usize,
182 unread_only: bool,
183 ) -> Result<InboxPage, ClientError> {
184 let token = self.auth.get_valid_token(account).await?;
185 list_folder_messages(
186 &self.http,
187 &self.base_url,
188 &token,
189 account,
190 folder_id,
191 limit,
192 skip,
193 unread_only,
194 )
195 .await
196 }
197
198 pub async fn mail_delta_bootstrap(
201 &self,
202 account: &str,
203 folder: &str,
204 ) -> Result<(Vec<Message>, String), ClientError> {
205 let token = self.auth.get_valid_token(account).await?;
206 delta::mail_delta_bootstrap(&self.http, &self.base_url, &token, account, folder).await
207 }
208
209 pub async fn mail_delta(
211 &self,
212 account: &str,
213 delta_link: &str,
214 ) -> Result<(Vec<delta::MailDeltaEvent>, String), ClientError> {
215 let token = self.auth.get_valid_token(account).await?;
216 delta::mail_delta(&self.http, &token, account, delta_link).await
217 }
218
219 pub async fn calendar_delta_bootstrap(
221 &self,
222 account: &str,
223 start: chrono::DateTime<chrono::Utc>,
224 end: chrono::DateTime<chrono::Utc>,
225 ) -> Result<(Vec<pidge_core::Event>, String), ClientError> {
226 let token = self.auth.get_valid_token(account).await?;
227 delta::calendar_delta_bootstrap(&self.http, &self.base_url, &token, account, start, end)
228 .await
229 }
230
231 pub async fn calendar_delta(
233 &self,
234 account: &str,
235 delta_link: &str,
236 ) -> Result<(Vec<delta::CalendarDeltaEvent>, String), ClientError> {
237 let token = self.auth.get_valid_token(account).await?;
238 delta::calendar_delta(&self.http, &token, account, delta_link).await
239 }
240
241 pub async fn batch_all(
243 &self,
244 account: &str,
245 requests: Vec<batch::BatchRequest>,
246 ) -> Result<Vec<batch::BatchResponse>, ClientError> {
247 let token = self.auth.get_valid_token(account).await?;
248 batch::batch_all(&self.http, &self.base_url, &token, requests).await
249 }
250
251 pub async fn list_conversation(
253 &self,
254 account: &str,
255 conversation_id: &str,
256 ) -> Result<Vec<Message>, ClientError> {
257 let token = self.auth.get_valid_token(account).await?;
258 mail::list_conversation(&self.http, &self.base_url, &token, account, conversation_id).await
259 }
260
261 pub async fn list_messages_at(
263 &self,
264 account: &str,
265 url: &str,
266 ) -> Result<InboxPage, ClientError> {
267 let token = self.auth.get_valid_token(account).await?;
268 list_messages_at(&self.http, &self.base_url, &token, account, url).await
269 }
270
271 pub async fn search_messages(
272 &self,
273 account: &str,
274 query: &str,
275 limit: usize,
276 ) -> Result<InboxPage, ClientError> {
277 let token = self.auth.get_valid_token(account).await?;
278 search_messages(&self.http, &self.base_url, &token, account, query, limit).await
279 }
280
281 pub async fn search_folder(
283 &self,
284 account: &str,
285 folder: &str,
286 query: &str,
287 limit: usize,
288 ) -> Result<InboxPage, ClientError> {
289 let token = self.auth.get_valid_token(account).await?;
290 search_folder_messages(
291 &self.http,
292 &self.base_url,
293 &token,
294 account,
295 folder,
296 query,
297 limit,
298 )
299 .await
300 }
301
302 pub async fn mark_unread(&self, account: &str, message_id: &str) -> Result<(), ClientError> {
304 let token = self.auth.get_valid_token(account).await?;
305 mail::mark_unread(&self.http, &self.base_url, &token, message_id).await
306 }
307
308 pub async fn set_flag(
310 &self,
311 account: &str,
312 message_id: &str,
313 flagged: bool,
314 ) -> Result<(), ClientError> {
315 let token = self.auth.get_valid_token(account).await?;
316 mail::set_flag(&self.http, &self.base_url, &token, message_id, flagged).await
317 }
318
319 pub async fn get_categories(
321 &self,
322 account: &str,
323 message_id: &str,
324 ) -> Result<Vec<String>, ClientError> {
325 let token = self.auth.get_valid_token(account).await?;
326 mail::get_categories(&self.http, &self.base_url, &token, message_id).await
327 }
328
329 pub async fn set_categories(
331 &self,
332 account: &str,
333 message_id: &str,
334 categories: &[String],
335 ) -> Result<(), ClientError> {
336 let token = self.auth.get_valid_token(account).await?;
337 mail::set_categories(&self.http, &self.base_url, &token, message_id, categories).await
338 }
339
340 pub async fn move_message(
342 &self,
343 account: &str,
344 message_id: &str,
345 destination: &str,
346 ) -> Result<(), ClientError> {
347 let token = self.auth.get_valid_token(account).await?;
348 mail::move_message(&self.http, &self.base_url, &token, message_id, destination).await
349 }
350
351 pub async fn list_mail_folders(&self, account: &str) -> Result<Vec<MailFolder>, ClientError> {
353 let token = self.auth.get_valid_token(account).await?;
354 mail::list_mail_folders(&self.http, &self.base_url, &token).await
355 }
356
357 pub async fn create_mail_folder(
359 &self,
360 account: &str,
361 display_name: &str,
362 ) -> Result<MailFolder, ClientError> {
363 let token = self.auth.get_valid_token(account).await?;
364 mail::create_mail_folder(&self.http, &self.base_url, &token, display_name).await
365 }
366
367 pub async fn list_child_folders(
369 &self,
370 account: &str,
371 parent_id: &str,
372 ) -> Result<Vec<MailFolder>, ClientError> {
373 let token = self.auth.get_valid_token(account).await?;
374 mail::list_child_folders(&self.http, &self.base_url, &token, parent_id).await
375 }
376
377 pub async fn create_child_folder(
379 &self,
380 account: &str,
381 parent_id: &str,
382 display_name: &str,
383 ) -> Result<MailFolder, ClientError> {
384 let token = self.auth.get_valid_token(account).await?;
385 mail::create_child_folder(&self.http, &self.base_url, &token, parent_id, display_name).await
386 }
387
388 pub async fn delete_mail_folder(
391 &self,
392 account: &str,
393 folder_id: &str,
394 ) -> Result<(), ClientError> {
395 let token = self.auth.get_valid_token(account).await?;
396 mail::delete_mail_folder(&self.http, &self.base_url, &token, folder_id).await
397 }
398
399 pub async fn send_mail(&self, account: &str, message: &Outgoing) -> Result<(), ClientError> {
401 let token = self.auth.get_valid_token(account).await?;
402 mail::send_mail(&self.http, &self.base_url, &token, message).await
403 }
404
405 pub async fn reply_message(
407 &self,
408 account: &str,
409 message_id: &str,
410 comment: &str,
411 ) -> Result<(), ClientError> {
412 let token = self.auth.get_valid_token(account).await?;
413 mail::reply_message(&self.http, &self.base_url, &token, message_id, comment).await
414 }
415
416 pub async fn reply_all_message(
418 &self,
419 account: &str,
420 message_id: &str,
421 comment: &str,
422 ) -> Result<(), ClientError> {
423 let token = self.auth.get_valid_token(account).await?;
424 mail::reply_all_message(&self.http, &self.base_url, &token, message_id, comment).await
425 }
426
427 pub async fn forward_message(
429 &self,
430 account: &str,
431 message_id: &str,
432 to: &[String],
433 comment: &str,
434 ) -> Result<(), ClientError> {
435 let token = self.auth.get_valid_token(account).await?;
436 mail::forward_message(&self.http, &self.base_url, &token, message_id, to, comment).await
437 }
438
439 pub async fn list_drafts(
441 &self,
442 account: &str,
443 limit: usize,
444 skip: usize,
445 ) -> Result<InboxPage, ClientError> {
446 let token = self.auth.get_valid_token(account).await?;
447 mail::list_drafts(&self.http, &self.base_url, &token, account, limit, skip).await
448 }
449
450 pub async fn create_draft(
452 &self,
453 account: &str,
454 message: &Outgoing,
455 ) -> Result<String, ClientError> {
456 let token = self.auth.get_valid_token(account).await?;
457 mail::create_draft(&self.http, &self.base_url, &token, message).await
458 }
459
460 pub async fn create_reply_draft(
462 &self,
463 account: &str,
464 message_id: &str,
465 comment: &str,
466 ) -> Result<String, ClientError> {
467 let token = self.auth.get_valid_token(account).await?;
468 mail::create_reply_draft(&self.http, &self.base_url, &token, message_id, comment).await
469 }
470
471 pub async fn create_reply_all_draft(
473 &self,
474 account: &str,
475 message_id: &str,
476 comment: &str,
477 ) -> Result<String, ClientError> {
478 let token = self.auth.get_valid_token(account).await?;
479 mail::create_reply_all_draft(&self.http, &self.base_url, &token, message_id, comment).await
480 }
481
482 pub async fn create_forward_draft(
484 &self,
485 account: &str,
486 message_id: &str,
487 to: &[String],
488 comment: &str,
489 ) -> Result<String, ClientError> {
490 let token = self.auth.get_valid_token(account).await?;
491 mail::create_forward_draft(&self.http, &self.base_url, &token, message_id, to, comment)
492 .await
493 }
494
495 pub async fn send_draft(&self, account: &str, message_id: &str) -> Result<(), ClientError> {
497 let token = self.auth.get_valid_token(account).await?;
498 mail::send_draft(&self.http, &self.base_url, &token, message_id).await
499 }
500
501 pub async fn update_draft(
503 &self,
504 account: &str,
505 message_id: &str,
506 message: &Outgoing,
507 ) -> Result<(), ClientError> {
508 let token = self.auth.get_valid_token(account).await?;
509 mail::update_draft(&self.http, &self.base_url, &token, message_id, message).await
510 }
511
512 pub async fn update_draft_recipients(
514 &self,
515 account: &str,
516 message_id: &str,
517 to: Option<&[String]>,
518 cc: Option<&[String]>,
519 bcc: Option<&[String]>,
520 ) -> Result<(), ClientError> {
521 let token = self.auth.get_valid_token(account).await?;
522 mail::update_draft_recipients(&self.http, &self.base_url, &token, message_id, to, cc, bcc)
523 .await
524 }
525
526 pub async fn delete_message(&self, account: &str, message_id: &str) -> Result<(), ClientError> {
529 let token = self.auth.get_valid_token(account).await?;
530 mail::delete_message(&self.http, &self.base_url, &token, message_id).await
531 }
532
533 pub async fn add_attachment(
535 &self,
536 account: &str,
537 message_id: &str,
538 name: &str,
539 content_type: &str,
540 bytes: &[u8],
541 ) -> Result<String, ClientError> {
542 let token = self.auth.get_valid_token(account).await?;
543 mail::add_attachment(
544 &self.http,
545 &self.base_url,
546 &token,
547 message_id,
548 name,
549 content_type,
550 bytes,
551 )
552 .await
553 }
554
555 pub async fn delete_attachment(
557 &self,
558 account: &str,
559 message_id: &str,
560 attachment_id: &str,
561 ) -> Result<(), ClientError> {
562 let token = self.auth.get_valid_token(account).await?;
563 mail::delete_attachment(
564 &self.http,
565 &self.base_url,
566 &token,
567 message_id,
568 attachment_id,
569 )
570 .await
571 }
572
573 pub async fn get_message(
575 &self,
576 account: &str,
577 message_id: &str,
578 ) -> Result<pidge_core::FullMessage, ClientError> {
579 let token = self.auth.get_valid_token(account).await?;
580 mail::get_message(&self.http, &self.base_url, &token, account, message_id).await
581 }
582
583 pub async fn fetch_message_headers(
585 &self,
586 account: &str,
587 message_id: &str,
588 ) -> Result<Vec<(String, String)>, ClientError> {
589 let token = self.auth.get_valid_token(account).await?;
590 mail::fetch_message_headers(&self.http, &self.base_url, &token, message_id).await
591 }
592
593 pub async fn list_attachments(
595 &self,
596 account: &str,
597 message_id: &str,
598 ) -> Result<Vec<pidge_core::Attachment>, ClientError> {
599 let token = self.auth.get_valid_token(account).await?;
600 mail::list_attachments(&self.http, &self.base_url, &token, message_id).await
601 }
602
603 pub async fn unsubscribe_one_click(&self, url: &str) -> Result<(), ClientError> {
610 mail::post_one_click(url, self.one_click).await
611 }
612
613 pub async fn list_people(
616 &self,
617 account: &str,
618 top: usize,
619 ) -> Result<Vec<people::Person>, ClientError> {
620 let token = self.auth.get_valid_token(account).await?;
621 people::list_people(&self.http, &self.base_url, &token, top).await
622 }
623
624 pub async fn get_attachment_bytes(
626 &self,
627 account: &str,
628 message_id: &str,
629 attachment_id: &str,
630 ) -> Result<Vec<u8>, ClientError> {
631 let token = self.auth.get_valid_token(account).await?;
632 mail::get_attachment_bytes(
633 &self.http,
634 &self.base_url,
635 &token,
636 message_id,
637 attachment_id,
638 )
639 .await
640 }
641
642 pub async fn mark_read(&self, account: &str, message_id: &str) -> Result<(), ClientError> {
644 let token = self.auth.get_valid_token(account).await?;
645 mail::mark_read(&self.http, &self.base_url, &token, message_id).await
646 }
647
648 pub async fn list_calendars(
652 &self,
653 account: &str,
654 ) -> Result<Vec<pidge_core::Calendar>, ClientError> {
655 let token = self.auth.get_valid_token(account).await?;
656 calendars::list_calendars(&self.http, &self.base_url, &token, account).await
657 }
658
659 pub async fn list_calendar_view(
661 &self,
662 account: &str,
663 calendar_id: Option<&str>,
664 start: chrono::DateTime<chrono::Utc>,
665 end: chrono::DateTime<chrono::Utc>,
666 limit: usize,
667 ) -> Result<events::EventsPage, ClientError> {
668 let token = self.auth.get_valid_token(account).await?;
669 events::list_calendar_view(
670 &self.http,
671 &self.base_url,
672 &token,
673 account,
674 calendar_id,
675 start,
676 end,
677 limit,
678 )
679 .await
680 }
681
682 pub async fn list_events_at(
684 &self,
685 account: &str,
686 url: &str,
687 ) -> Result<EventsPage, ClientError> {
688 let token = self.auth.get_valid_token(account).await?;
689 list_events_at(&self.http, &self.base_url, &token, account, url).await
690 }
691
692 pub async fn get_event(
694 &self,
695 account: &str,
696 event_id: &str,
697 ) -> Result<pidge_core::Event, ClientError> {
698 let token = self.auth.get_valid_token(account).await?;
699 events::get_event(&self.http, &self.base_url, &token, account, event_id).await
700 }
701
702 pub async fn create_event(
704 &self,
705 account: &str,
706 calendar_id: Option<&str>,
707 new_event: &events::NewEvent,
708 ) -> Result<String, ClientError> {
709 let token = self.auth.get_valid_token(account).await?;
710 events::create_event(&self.http, &self.base_url, &token, calendar_id, new_event).await
711 }
712
713 pub async fn update_event(
715 &self,
716 account: &str,
717 event_id: &str,
718 new_event: &events::NewEvent,
719 ) -> Result<(), ClientError> {
720 let token = self.auth.get_valid_token(account).await?;
721 events::update_event(&self.http, &self.base_url, &token, event_id, new_event).await
722 }
723
724 pub async fn move_time(
726 &self,
727 account: &str,
728 event_id: &str,
729 start: chrono::DateTime<chrono::Utc>,
730 end: chrono::DateTime<chrono::Utc>,
731 tz: &str,
732 ) -> Result<(), ClientError> {
733 let token = self.auth.get_valid_token(account).await?;
734 events::move_time(&self.http, &self.base_url, &token, event_id, start, end, tz).await
735 }
736
737 pub async fn delete_event(&self, account: &str, event_id: &str) -> Result<(), ClientError> {
739 let token = self.auth.get_valid_token(account).await?;
740 events::delete_event(&self.http, &self.base_url, &token, event_id).await
741 }
742
743 pub async fn cancel_event(
745 &self,
746 account: &str,
747 event_id: &str,
748 comment: &str,
749 ) -> Result<(), ClientError> {
750 let token = self.auth.get_valid_token(account).await?;
751 events::cancel_event(&self.http, &self.base_url, &token, event_id, comment).await
752 }
753
754 #[allow(clippy::too_many_arguments)]
759 pub async fn rsvp_event(
760 &self,
761 account: &str,
762 event_id: &str,
763 kind: events::RsvpKind,
764 comment: &str,
765 send_response: bool,
766 proposed: Option<&events::ProposedTime>,
767 ) -> Result<(), ClientError> {
768 let token = self.auth.get_valid_token(account).await?;
769 events::rsvp_event(
770 &self.http,
771 &self.base_url,
772 &token,
773 event_id,
774 kind,
775 comment,
776 send_response,
777 proposed,
778 )
779 .await
780 }
781
782 pub async fn move_event_to_calendar(
784 &self,
785 account: &str,
786 event_id: &str,
787 destination_calendar_id: &str,
788 ) -> Result<(), ClientError> {
789 let token = self.auth.get_valid_token(account).await?;
790 events::move_event_to_calendar(
791 &self.http,
792 &self.base_url,
793 &token,
794 event_id,
795 destination_calendar_id,
796 )
797 .await
798 }
799}
800
801#[cfg(test)]
802mod retry_tests {
803 use super::*;
804 use wiremock::matchers::{method, path};
805 use wiremock::{Mock, MockServer, Request, Respond, ResponseTemplate};
806
807 struct FlakyResponder {
809 failures: std::sync::atomic::AtomicU32,
810 }
811
812 impl Respond for FlakyResponder {
813 fn respond(&self, _req: &Request) -> ResponseTemplate {
814 let n = self
815 .failures
816 .fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
817 if n > 0 {
818 ResponseTemplate::new(429).insert_header("Retry-After", "0")
819 } else {
820 ResponseTemplate::new(200).set_body_json(serde_json::json!({"ok": true}))
821 }
822 }
823 }
824
825 #[tokio::test]
826 async fn retries_transient_429_until_success() {
827 let server = MockServer::start().await;
828 Mock::given(method("GET"))
829 .and(path("/thing"))
830 .respond_with(FlakyResponder {
831 failures: std::sync::atomic::AtomicU32::new(2),
832 })
833 .expect(3)
834 .mount(&server)
835 .await;
836
837 let http = reqwest::Client::new();
838 let resp = send_with_retry(http.get(format!("{}/thing", server.uri())))
839 .await
840 .unwrap();
841 assert_eq!(resp.status(), 200);
842 }
843
844 #[tokio::test]
845 async fn persistent_429_becomes_throttled_after_max_attempts() {
846 let server = MockServer::start().await;
847 Mock::given(method("GET"))
848 .and(path("/thing"))
849 .respond_with(ResponseTemplate::new(429).insert_header("Retry-After", "0"))
850 .expect(4)
851 .mount(&server)
852 .await;
853
854 let http = reqwest::Client::new();
855 let err = send_with_retry(http.get(format!("{}/thing", server.uri())))
856 .await
857 .unwrap_err();
858 match err {
859 ClientError::Throttled { retry_after } => assert_eq!(retry_after, Some(0)),
860 other => panic!("expected Throttled, got {other:?}"),
861 }
862 }
863
864 #[tokio::test]
865 async fn non_transient_errors_pass_through_without_retry() {
866 let server = MockServer::start().await;
867 Mock::given(method("GET"))
868 .and(path("/thing"))
869 .respond_with(ResponseTemplate::new(400).set_body_string("bad"))
870 .expect(1)
871 .mount(&server)
872 .await;
873
874 let http = reqwest::Client::new();
875 let resp = send_with_retry(http.get(format!("{}/thing", server.uri())))
876 .await
877 .unwrap();
878 assert_eq!(resp.status(), 400);
879 }
880}
881
882#[cfg(test)]
883mod continuation_tests {
884 use super::*;
885 use wiremock::matchers::method;
886 use wiremock::{Mock, MockServer, ResponseTemplate};
887
888 const REFUSAL: &str = "refusing to follow a continuation link off graph.microsoft.com";
889
890 fn refused(r: Result<(), ClientError>) -> bool {
891 matches!(r, Err(ClientError::Graph { status: 400, ref message }) if message == REFUSAL)
892 }
893
894 #[test]
895 fn graph_links_are_followed_and_others_refused_in_production() {
896 let base = config::GRAPH_BASE;
897 assert!(
898 check_continuation(
899 "https://graph.microsoft.com/v1.0/me/messages?$skiptoken=x",
900 base
901 )
902 .is_ok()
903 );
904 for url in [
905 "https://evil.example.com/v1.0/me/messages",
906 "http://graph.microsoft.com/v1.0/me/messages",
907 "https://graph.microsoft.com.evil.example.com/v1.0",
908 "https://graph.microsoft.com:8443/v1.0",
909 "not a url",
910 ] {
911 assert!(refused(check_continuation(url, base)), "{url}");
912 }
913 }
914
915 #[test]
916 fn a_test_base_url_allows_its_own_origin_only() {
917 let base = "http://127.0.0.1:4000/v1.0";
918 assert!(check_continuation("http://127.0.0.1:4000/v1.0/page-2", base).is_ok());
919 assert!(refused(check_continuation(
920 "http://127.0.0.1:4001/v1.0/page-2",
921 base
922 )));
923 }
924
925 #[tokio::test]
928 async fn off_graph_next_links_are_refused_without_a_request() {
929 let graph = MockServer::start().await;
930 let other = MockServer::start().await;
931 Mock::given(method("GET"))
932 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
933 "value": []
934 })))
935 .expect(0)
936 .mount(&other)
937 .await;
938 let http = reqwest::Client::new();
939 let base = format!("{}/v1.0", graph.uri());
940 let link = format!("{}/v1.0/page-2", other.uri());
941
942 let Err(err) = events::list_events_at(&http, &base, "tok", "a@b.se", &link).await else {
943 panic!("list_events_at followed an off-Graph link");
944 };
945 assert!(refused(Err(err)));
946 let Err(err) = mail::list_messages_at(&http, &base, "tok", "a@b.se", &link).await else {
947 panic!("list_messages_at followed an off-Graph link");
948 };
949 assert!(refused(Err(err)));
950 assert!(other.received_requests().await.unwrap().is_empty());
951 }
952}