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_folder_messages_where, list_inbox, list_mail_folders,
23 list_messages_at, mark_read, mark_unread, move_message, reply_all_message, reply_message,
24 search_folder_messages, search_messages, send_draft, send_mail, set_categories, set_flag,
25 unsubscribe_one_click, 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 list_folder_where(
201 &self,
202 account: &str,
203 folder_id: &str,
204 limit: usize,
205 filter: &str,
206 ) -> Result<InboxPage, ClientError> {
207 let token = self.auth.get_valid_token(account).await?;
208 list_folder_messages_where(
209 &self.http,
210 &self.base_url,
211 &token,
212 account,
213 folder_id,
214 limit,
215 filter,
216 )
217 .await
218 }
219
220 pub async fn mail_delta_bootstrap(
223 &self,
224 account: &str,
225 folder: &str,
226 ) -> Result<(Vec<Message>, String), ClientError> {
227 let token = self.auth.get_valid_token(account).await?;
228 delta::mail_delta_bootstrap(&self.http, &self.base_url, &token, account, folder).await
229 }
230
231 pub async fn mail_delta(
233 &self,
234 account: &str,
235 delta_link: &str,
236 ) -> Result<(Vec<delta::MailDeltaEvent>, String), ClientError> {
237 let token = self.auth.get_valid_token(account).await?;
238 delta::mail_delta(&self.http, &token, account, delta_link).await
239 }
240
241 pub async fn calendar_delta_bootstrap(
243 &self,
244 account: &str,
245 start: chrono::DateTime<chrono::Utc>,
246 end: chrono::DateTime<chrono::Utc>,
247 ) -> Result<(Vec<pidge_core::Event>, String), ClientError> {
248 let token = self.auth.get_valid_token(account).await?;
249 delta::calendar_delta_bootstrap(&self.http, &self.base_url, &token, account, start, end)
250 .await
251 }
252
253 pub async fn calendar_delta(
255 &self,
256 account: &str,
257 delta_link: &str,
258 ) -> Result<(Vec<delta::CalendarDeltaEvent>, String), ClientError> {
259 let token = self.auth.get_valid_token(account).await?;
260 delta::calendar_delta(&self.http, &token, account, delta_link).await
261 }
262
263 pub async fn batch_all(
265 &self,
266 account: &str,
267 requests: Vec<batch::BatchRequest>,
268 ) -> Result<Vec<batch::BatchResponse>, ClientError> {
269 let token = self.auth.get_valid_token(account).await?;
270 batch::batch_all(&self.http, &self.base_url, &token, requests).await
271 }
272
273 pub async fn list_conversation(
275 &self,
276 account: &str,
277 conversation_id: &str,
278 ) -> Result<Vec<Message>, ClientError> {
279 let token = self.auth.get_valid_token(account).await?;
280 mail::list_conversation(&self.http, &self.base_url, &token, account, conversation_id).await
281 }
282
283 pub async fn list_messages_at(
285 &self,
286 account: &str,
287 url: &str,
288 ) -> Result<InboxPage, ClientError> {
289 let token = self.auth.get_valid_token(account).await?;
290 list_messages_at(&self.http, &self.base_url, &token, account, url).await
291 }
292
293 pub async fn search_messages(
294 &self,
295 account: &str,
296 query: &str,
297 limit: usize,
298 ) -> Result<InboxPage, ClientError> {
299 let token = self.auth.get_valid_token(account).await?;
300 search_messages(&self.http, &self.base_url, &token, account, query, limit).await
301 }
302
303 pub async fn search_folder(
305 &self,
306 account: &str,
307 folder: &str,
308 query: &str,
309 limit: usize,
310 ) -> Result<InboxPage, ClientError> {
311 let token = self.auth.get_valid_token(account).await?;
312 search_folder_messages(
313 &self.http,
314 &self.base_url,
315 &token,
316 account,
317 folder,
318 query,
319 limit,
320 )
321 .await
322 }
323
324 pub async fn mark_unread(&self, account: &str, message_id: &str) -> Result<(), ClientError> {
326 let token = self.auth.get_valid_token(account).await?;
327 mail::mark_unread(&self.http, &self.base_url, &token, message_id).await
328 }
329
330 pub async fn set_flag(
332 &self,
333 account: &str,
334 message_id: &str,
335 flagged: bool,
336 ) -> Result<(), ClientError> {
337 let token = self.auth.get_valid_token(account).await?;
338 mail::set_flag(&self.http, &self.base_url, &token, message_id, flagged).await
339 }
340
341 pub async fn get_categories(
343 &self,
344 account: &str,
345 message_id: &str,
346 ) -> Result<Vec<String>, ClientError> {
347 let token = self.auth.get_valid_token(account).await?;
348 mail::get_categories(&self.http, &self.base_url, &token, message_id).await
349 }
350
351 pub async fn set_categories(
353 &self,
354 account: &str,
355 message_id: &str,
356 categories: &[String],
357 ) -> Result<(), ClientError> {
358 let token = self.auth.get_valid_token(account).await?;
359 mail::set_categories(&self.http, &self.base_url, &token, message_id, categories).await
360 }
361
362 pub async fn move_message(
364 &self,
365 account: &str,
366 message_id: &str,
367 destination: &str,
368 ) -> Result<(), ClientError> {
369 let token = self.auth.get_valid_token(account).await?;
370 mail::move_message(&self.http, &self.base_url, &token, message_id, destination).await
371 }
372
373 pub async fn list_mail_folders(&self, account: &str) -> Result<Vec<MailFolder>, ClientError> {
375 let token = self.auth.get_valid_token(account).await?;
376 mail::list_mail_folders(&self.http, &self.base_url, &token).await
377 }
378
379 pub async fn create_mail_folder(
381 &self,
382 account: &str,
383 display_name: &str,
384 ) -> Result<MailFolder, ClientError> {
385 let token = self.auth.get_valid_token(account).await?;
386 mail::create_mail_folder(&self.http, &self.base_url, &token, display_name).await
387 }
388
389 pub async fn list_child_folders(
391 &self,
392 account: &str,
393 parent_id: &str,
394 ) -> Result<Vec<MailFolder>, ClientError> {
395 let token = self.auth.get_valid_token(account).await?;
396 mail::list_child_folders(&self.http, &self.base_url, &token, parent_id).await
397 }
398
399 pub async fn create_child_folder(
401 &self,
402 account: &str,
403 parent_id: &str,
404 display_name: &str,
405 ) -> Result<MailFolder, ClientError> {
406 let token = self.auth.get_valid_token(account).await?;
407 mail::create_child_folder(&self.http, &self.base_url, &token, parent_id, display_name).await
408 }
409
410 pub async fn delete_mail_folder(
413 &self,
414 account: &str,
415 folder_id: &str,
416 ) -> Result<(), ClientError> {
417 let token = self.auth.get_valid_token(account).await?;
418 mail::delete_mail_folder(&self.http, &self.base_url, &token, folder_id).await
419 }
420
421 pub async fn send_mail(&self, account: &str, message: &Outgoing) -> Result<(), ClientError> {
423 let token = self.auth.get_valid_token(account).await?;
424 mail::send_mail(&self.http, &self.base_url, &token, message).await
425 }
426
427 pub async fn reply_message(
429 &self,
430 account: &str,
431 message_id: &str,
432 comment: &str,
433 ) -> Result<(), ClientError> {
434 let token = self.auth.get_valid_token(account).await?;
435 mail::reply_message(&self.http, &self.base_url, &token, message_id, comment).await
436 }
437
438 pub async fn reply_all_message(
440 &self,
441 account: &str,
442 message_id: &str,
443 comment: &str,
444 ) -> Result<(), ClientError> {
445 let token = self.auth.get_valid_token(account).await?;
446 mail::reply_all_message(&self.http, &self.base_url, &token, message_id, comment).await
447 }
448
449 pub async fn forward_message(
451 &self,
452 account: &str,
453 message_id: &str,
454 to: &[String],
455 comment: &str,
456 ) -> Result<(), ClientError> {
457 let token = self.auth.get_valid_token(account).await?;
458 mail::forward_message(&self.http, &self.base_url, &token, message_id, to, comment).await
459 }
460
461 pub async fn list_drafts(
463 &self,
464 account: &str,
465 limit: usize,
466 skip: usize,
467 ) -> Result<InboxPage, ClientError> {
468 let token = self.auth.get_valid_token(account).await?;
469 mail::list_drafts(&self.http, &self.base_url, &token, account, limit, skip).await
470 }
471
472 pub async fn create_draft(
474 &self,
475 account: &str,
476 message: &Outgoing,
477 ) -> Result<String, ClientError> {
478 let token = self.auth.get_valid_token(account).await?;
479 mail::create_draft(&self.http, &self.base_url, &token, message).await
480 }
481
482 pub async fn create_reply_draft(
484 &self,
485 account: &str,
486 message_id: &str,
487 comment: &str,
488 ) -> Result<String, ClientError> {
489 let token = self.auth.get_valid_token(account).await?;
490 mail::create_reply_draft(&self.http, &self.base_url, &token, message_id, comment).await
491 }
492
493 pub async fn create_reply_all_draft(
495 &self,
496 account: &str,
497 message_id: &str,
498 comment: &str,
499 ) -> Result<String, ClientError> {
500 let token = self.auth.get_valid_token(account).await?;
501 mail::create_reply_all_draft(&self.http, &self.base_url, &token, message_id, comment).await
502 }
503
504 pub async fn create_forward_draft(
506 &self,
507 account: &str,
508 message_id: &str,
509 to: &[String],
510 comment: &str,
511 ) -> Result<String, ClientError> {
512 let token = self.auth.get_valid_token(account).await?;
513 mail::create_forward_draft(&self.http, &self.base_url, &token, message_id, to, comment)
514 .await
515 }
516
517 pub async fn send_draft(&self, account: &str, message_id: &str) -> Result<(), ClientError> {
519 let token = self.auth.get_valid_token(account).await?;
520 mail::send_draft(&self.http, &self.base_url, &token, message_id).await
521 }
522
523 pub async fn update_draft(
525 &self,
526 account: &str,
527 message_id: &str,
528 message: &Outgoing,
529 ) -> Result<(), ClientError> {
530 let token = self.auth.get_valid_token(account).await?;
531 mail::update_draft(&self.http, &self.base_url, &token, message_id, message).await
532 }
533
534 pub async fn update_draft_recipients(
536 &self,
537 account: &str,
538 message_id: &str,
539 to: Option<&[String]>,
540 cc: Option<&[String]>,
541 bcc: Option<&[String]>,
542 ) -> Result<(), ClientError> {
543 let token = self.auth.get_valid_token(account).await?;
544 mail::update_draft_recipients(&self.http, &self.base_url, &token, message_id, to, cc, bcc)
545 .await
546 }
547
548 pub async fn delete_message(&self, account: &str, message_id: &str) -> Result<(), ClientError> {
551 let token = self.auth.get_valid_token(account).await?;
552 mail::delete_message(&self.http, &self.base_url, &token, message_id).await
553 }
554
555 pub async fn add_attachment(
557 &self,
558 account: &str,
559 message_id: &str,
560 name: &str,
561 content_type: &str,
562 bytes: &[u8],
563 ) -> Result<String, ClientError> {
564 let token = self.auth.get_valid_token(account).await?;
565 mail::add_attachment(
566 &self.http,
567 &self.base_url,
568 &token,
569 message_id,
570 name,
571 content_type,
572 bytes,
573 )
574 .await
575 }
576
577 pub async fn delete_attachment(
579 &self,
580 account: &str,
581 message_id: &str,
582 attachment_id: &str,
583 ) -> Result<(), ClientError> {
584 let token = self.auth.get_valid_token(account).await?;
585 mail::delete_attachment(
586 &self.http,
587 &self.base_url,
588 &token,
589 message_id,
590 attachment_id,
591 )
592 .await
593 }
594
595 pub async fn get_message(
597 &self,
598 account: &str,
599 message_id: &str,
600 ) -> Result<pidge_core::FullMessage, ClientError> {
601 let token = self.auth.get_valid_token(account).await?;
602 mail::get_message(&self.http, &self.base_url, &token, account, message_id).await
603 }
604
605 pub async fn fetch_message_headers(
607 &self,
608 account: &str,
609 message_id: &str,
610 ) -> Result<Vec<(String, String)>, ClientError> {
611 let token = self.auth.get_valid_token(account).await?;
612 mail::fetch_message_headers(&self.http, &self.base_url, &token, message_id).await
613 }
614
615 pub async fn list_attachments(
617 &self,
618 account: &str,
619 message_id: &str,
620 ) -> Result<Vec<pidge_core::Attachment>, ClientError> {
621 let token = self.auth.get_valid_token(account).await?;
622 mail::list_attachments(&self.http, &self.base_url, &token, message_id).await
623 }
624
625 pub async fn unsubscribe_one_click(&self, url: &str) -> Result<(), ClientError> {
632 mail::post_one_click(url, self.one_click).await
633 }
634
635 pub async fn list_people(
638 &self,
639 account: &str,
640 top: usize,
641 ) -> Result<Vec<people::Person>, ClientError> {
642 let token = self.auth.get_valid_token(account).await?;
643 people::list_people(&self.http, &self.base_url, &token, top).await
644 }
645
646 pub async fn get_attachment_bytes(
648 &self,
649 account: &str,
650 message_id: &str,
651 attachment_id: &str,
652 ) -> Result<Vec<u8>, ClientError> {
653 let token = self.auth.get_valid_token(account).await?;
654 mail::get_attachment_bytes(
655 &self.http,
656 &self.base_url,
657 &token,
658 message_id,
659 attachment_id,
660 )
661 .await
662 }
663
664 pub async fn mark_read(&self, account: &str, message_id: &str) -> Result<(), ClientError> {
666 let token = self.auth.get_valid_token(account).await?;
667 mail::mark_read(&self.http, &self.base_url, &token, message_id).await
668 }
669
670 pub async fn list_calendars(
674 &self,
675 account: &str,
676 ) -> Result<Vec<pidge_core::Calendar>, ClientError> {
677 let token = self.auth.get_valid_token(account).await?;
678 calendars::list_calendars(&self.http, &self.base_url, &token, account).await
679 }
680
681 pub async fn list_calendar_view(
683 &self,
684 account: &str,
685 calendar_id: Option<&str>,
686 start: chrono::DateTime<chrono::Utc>,
687 end: chrono::DateTime<chrono::Utc>,
688 limit: usize,
689 ) -> Result<events::EventsPage, ClientError> {
690 let token = self.auth.get_valid_token(account).await?;
691 events::list_calendar_view(
692 &self.http,
693 &self.base_url,
694 &token,
695 account,
696 calendar_id,
697 start,
698 end,
699 limit,
700 )
701 .await
702 }
703
704 pub async fn list_events_at(
706 &self,
707 account: &str,
708 url: &str,
709 ) -> Result<EventsPage, ClientError> {
710 let token = self.auth.get_valid_token(account).await?;
711 list_events_at(&self.http, &self.base_url, &token, account, url).await
712 }
713
714 pub async fn get_event(
716 &self,
717 account: &str,
718 event_id: &str,
719 ) -> Result<pidge_core::Event, ClientError> {
720 let token = self.auth.get_valid_token(account).await?;
721 events::get_event(&self.http, &self.base_url, &token, account, event_id).await
722 }
723
724 pub async fn create_event(
726 &self,
727 account: &str,
728 calendar_id: Option<&str>,
729 new_event: &events::NewEvent,
730 ) -> Result<String, ClientError> {
731 let token = self.auth.get_valid_token(account).await?;
732 events::create_event(&self.http, &self.base_url, &token, calendar_id, new_event).await
733 }
734
735 pub async fn update_event(
737 &self,
738 account: &str,
739 event_id: &str,
740 new_event: &events::NewEvent,
741 ) -> Result<(), ClientError> {
742 let token = self.auth.get_valid_token(account).await?;
743 events::update_event(&self.http, &self.base_url, &token, event_id, new_event).await
744 }
745
746 pub async fn move_time(
748 &self,
749 account: &str,
750 event_id: &str,
751 start: chrono::DateTime<chrono::Utc>,
752 end: chrono::DateTime<chrono::Utc>,
753 tz: &str,
754 ) -> Result<(), ClientError> {
755 let token = self.auth.get_valid_token(account).await?;
756 events::move_time(&self.http, &self.base_url, &token, event_id, start, end, tz).await
757 }
758
759 pub async fn delete_event(&self, account: &str, event_id: &str) -> Result<(), ClientError> {
761 let token = self.auth.get_valid_token(account).await?;
762 events::delete_event(&self.http, &self.base_url, &token, event_id).await
763 }
764
765 pub async fn cancel_event(
767 &self,
768 account: &str,
769 event_id: &str,
770 comment: &str,
771 ) -> Result<(), ClientError> {
772 let token = self.auth.get_valid_token(account).await?;
773 events::cancel_event(&self.http, &self.base_url, &token, event_id, comment).await
774 }
775
776 #[allow(clippy::too_many_arguments)]
781 pub async fn rsvp_event(
782 &self,
783 account: &str,
784 event_id: &str,
785 kind: events::RsvpKind,
786 comment: &str,
787 send_response: bool,
788 proposed: Option<&events::ProposedTime>,
789 ) -> Result<(), ClientError> {
790 let token = self.auth.get_valid_token(account).await?;
791 events::rsvp_event(
792 &self.http,
793 &self.base_url,
794 &token,
795 event_id,
796 kind,
797 comment,
798 send_response,
799 proposed,
800 )
801 .await
802 }
803
804 pub async fn move_event_to_calendar(
806 &self,
807 account: &str,
808 event_id: &str,
809 destination_calendar_id: &str,
810 ) -> Result<(), ClientError> {
811 let token = self.auth.get_valid_token(account).await?;
812 events::move_event_to_calendar(
813 &self.http,
814 &self.base_url,
815 &token,
816 event_id,
817 destination_calendar_id,
818 )
819 .await
820 }
821}
822
823#[cfg(test)]
824mod retry_tests {
825 use super::*;
826 use wiremock::matchers::{method, path};
827 use wiremock::{Mock, MockServer, Request, Respond, ResponseTemplate};
828
829 struct FlakyResponder {
831 failures: std::sync::atomic::AtomicU32,
832 }
833
834 impl Respond for FlakyResponder {
835 fn respond(&self, _req: &Request) -> ResponseTemplate {
836 let n = self
837 .failures
838 .fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
839 if n > 0 {
840 ResponseTemplate::new(429).insert_header("Retry-After", "0")
841 } else {
842 ResponseTemplate::new(200).set_body_json(serde_json::json!({"ok": true}))
843 }
844 }
845 }
846
847 #[tokio::test]
848 async fn retries_transient_429_until_success() {
849 let server = MockServer::start().await;
850 Mock::given(method("GET"))
851 .and(path("/thing"))
852 .respond_with(FlakyResponder {
853 failures: std::sync::atomic::AtomicU32::new(2),
854 })
855 .expect(3)
856 .mount(&server)
857 .await;
858
859 let http = reqwest::Client::new();
860 let resp = send_with_retry(http.get(format!("{}/thing", server.uri())))
861 .await
862 .unwrap();
863 assert_eq!(resp.status(), 200);
864 }
865
866 #[tokio::test]
867 async fn persistent_429_becomes_throttled_after_max_attempts() {
868 let server = MockServer::start().await;
869 Mock::given(method("GET"))
870 .and(path("/thing"))
871 .respond_with(ResponseTemplate::new(429).insert_header("Retry-After", "0"))
872 .expect(4)
873 .mount(&server)
874 .await;
875
876 let http = reqwest::Client::new();
877 let err = send_with_retry(http.get(format!("{}/thing", server.uri())))
878 .await
879 .unwrap_err();
880 match err {
881 ClientError::Throttled { retry_after } => assert_eq!(retry_after, Some(0)),
882 other => panic!("expected Throttled, got {other:?}"),
883 }
884 }
885
886 #[tokio::test]
887 async fn non_transient_errors_pass_through_without_retry() {
888 let server = MockServer::start().await;
889 Mock::given(method("GET"))
890 .and(path("/thing"))
891 .respond_with(ResponseTemplate::new(400).set_body_string("bad"))
892 .expect(1)
893 .mount(&server)
894 .await;
895
896 let http = reqwest::Client::new();
897 let resp = send_with_retry(http.get(format!("{}/thing", server.uri())))
898 .await
899 .unwrap();
900 assert_eq!(resp.status(), 400);
901 }
902}
903
904#[cfg(test)]
905mod continuation_tests {
906 use super::*;
907 use wiremock::matchers::method;
908 use wiremock::{Mock, MockServer, ResponseTemplate};
909
910 const REFUSAL: &str = "refusing to follow a continuation link off graph.microsoft.com";
911
912 fn refused(r: Result<(), ClientError>) -> bool {
913 matches!(r, Err(ClientError::Graph { status: 400, ref message }) if message == REFUSAL)
914 }
915
916 #[test]
917 fn graph_links_are_followed_and_others_refused_in_production() {
918 let base = config::GRAPH_BASE;
919 assert!(
920 check_continuation(
921 "https://graph.microsoft.com/v1.0/me/messages?$skiptoken=x",
922 base
923 )
924 .is_ok()
925 );
926 for url in [
927 "https://evil.example.com/v1.0/me/messages",
928 "http://graph.microsoft.com/v1.0/me/messages",
929 "https://graph.microsoft.com.evil.example.com/v1.0",
930 "https://graph.microsoft.com:8443/v1.0",
931 "not a url",
932 ] {
933 assert!(refused(check_continuation(url, base)), "{url}");
934 }
935 }
936
937 #[test]
938 fn a_test_base_url_allows_its_own_origin_only() {
939 let base = "http://127.0.0.1:4000/v1.0";
940 assert!(check_continuation("http://127.0.0.1:4000/v1.0/page-2", base).is_ok());
941 assert!(refused(check_continuation(
942 "http://127.0.0.1:4001/v1.0/page-2",
943 base
944 )));
945 }
946
947 #[tokio::test]
950 async fn off_graph_next_links_are_refused_without_a_request() {
951 let graph = MockServer::start().await;
952 let other = MockServer::start().await;
953 Mock::given(method("GET"))
954 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
955 "value": []
956 })))
957 .expect(0)
958 .mount(&other)
959 .await;
960 let http = reqwest::Client::new();
961 let base = format!("{}/v1.0", graph.uri());
962 let link = format!("{}/v1.0/page-2", other.uri());
963
964 let Err(err) = events::list_events_at(&http, &base, "tok", "a@b.se", &link).await else {
965 panic!("list_events_at followed an off-Graph link");
966 };
967 assert!(refused(Err(err)));
968 let Err(err) = mail::list_messages_at(&http, &base, "tok", "a@b.se", &link).await else {
969 panic!("list_messages_at followed an off-Graph link");
970 };
971 assert!(refused(Err(err)));
972 assert!(other.received_requests().await.unwrap().is_empty());
973 }
974}