1use core::{convert::Infallible, mem, time::Duration};
10
11use alloc::{string::String, vec::Vec};
12
13use io_http::rfc6750::bearer::HttpAuthBearer;
14use log::{debug, trace};
15use thiserror::Error;
16
17use crate::{
18 coroutine::*,
19 v1::rest::history::{
20 GmailHistoryLabel,
21 list::{GmailHistoryList, GmailHistoryListParams},
22 },
23 v1::rest::messages::{GmailMessage, GmailMessageFormat, GmailMessageId, get::GmailMessageGet},
24 v1::rest::users::get_profile::GmailProfileGet,
25 v1::send::GmailSendError,
26};
27
28const POLL_SECONDS: u64 = 30;
29
30#[derive(Debug, Error)]
32pub enum GmailHistoryPollError {
33 #[error(transparent)]
35 Send(#[from] GmailSendError),
36}
37
38#[derive(Clone, Debug, Default)]
43pub struct GmailHistoryDiff {
44 pub history_id: String,
46 pub added: Vec<GmailMessage>,
48 pub removed: Vec<GmailMessageId>,
50 pub labels_added: Vec<GmailHistoryLabel>,
52 pub labels_removed: Vec<GmailHistoryLabel>,
54}
55
56#[derive(Debug)]
58pub enum GmailHistoryPollYield {
59 WantsRead,
61 WantsWrite(Vec<u8>),
63 WantsSleep(Duration),
65 Diff(GmailHistoryDiff),
67}
68
69pub struct GmailHistoryPoll {
75 state: State,
76 auth: HttpAuthBearer,
77 user_id: String,
78 mailbox: String,
79 history_id: Option<String>,
80}
81
82impl GmailHistoryPoll {
83 pub fn new(
86 auth: &HttpAuthBearer,
87 user_id: &str,
88 mailbox: &str,
89 ) -> Result<Self, GmailHistoryPollError> {
90 debug!("prepare gmail poll history");
91 trace!("user_id: {user_id:?}");
92 trace!("mailbox: {mailbox:?}");
93
94 let profile = GmailProfileGet::new(auth, user_id)?;
95 Ok(Self {
96 state: State::Baseline(profile),
97 auth: auth.clone(),
98 user_id: user_id.into(),
99 mailbox: mailbox.into(),
100 history_id: None,
101 })
102 }
103
104 fn list_history(&self, page_token: Option<&str>) -> Result<GmailHistoryList, GmailSendError> {
105 let params = GmailHistoryListParams {
106 start_history_id: self.history_id.as_deref().unwrap_or_default(),
107 label_id: Some(&self.mailbox),
108 history_types: &[],
109 max_results: None,
110 page_token,
111 };
112 GmailHistoryList::new(&self.auth, &self.user_id, ¶ms)
113 }
114
115 fn get_message(&self, id: &str) -> Result<GmailMessageGet, GmailSendError> {
116 GmailMessageGet::new(
117 &self.auth,
118 &self.user_id,
119 id,
120 GmailMessageFormat::Metadata,
121 &[],
122 )
123 }
124
125 fn finalize(&mut self, cycle: Cycle) -> GmailHistoryDiff {
126 let history_id = cycle
127 .new_history_id
128 .or_else(|| self.history_id.clone())
129 .unwrap_or_default();
130 self.history_id = Some(history_id.clone());
131 self.state = State::Sleeping;
132 GmailHistoryDiff {
133 history_id,
134 added: cycle.added,
135 removed: cycle.removed,
136 labels_added: cycle.labels_added,
137 labels_removed: cycle.labels_removed,
138 }
139 }
140}
141
142impl GmailCoroutine for GmailHistoryPoll {
143 type Yield = GmailHistoryPollYield;
144 type Return = Result<Infallible, GmailHistoryPollError>;
145
146 fn resume(&mut self, bytes: Option<&[u8]>) -> GmailCoroutineState<Self::Yield, Self::Return> {
147 let mut bytes = bytes;
148 loop {
149 match mem::replace(&mut self.state, State::Done) {
150 State::Baseline(mut profile) => match profile.resume(bytes.take()) {
151 GmailCoroutineState::Yielded(GmailYield::WantsRead) => {
152 self.state = State::Baseline(profile);
153 return GmailCoroutineState::Yielded(GmailHistoryPollYield::WantsRead);
154 }
155 GmailCoroutineState::Yielded(GmailYield::WantsWrite(out)) => {
156 self.state = State::Baseline(profile);
157 return GmailCoroutineState::Yielded(GmailHistoryPollYield::WantsWrite(
158 out,
159 ));
160 }
161 GmailCoroutineState::Complete(Err(err)) => {
162 return GmailCoroutineState::Complete(Err(err.into()));
163 }
164 GmailCoroutineState::Complete(Ok(out)) => {
165 self.history_id = out.response.history_id;
166 self.state = State::Sleeping;
167 }
168 },
169 State::Sleeping => {
170 let list = match self.list_history(None) {
171 Ok(list) => list,
172 Err(err) => return GmailCoroutineState::Complete(Err(err.into())),
173 };
174 self.state = State::Listing {
175 list,
176 cycle: Cycle::default(),
177 };
178 return GmailCoroutineState::Yielded(GmailHistoryPollYield::WantsSleep(
179 Duration::from_secs(POLL_SECONDS),
180 ));
181 }
182 State::Listing {
183 mut list,
184 mut cycle,
185 } => match list.resume(bytes.take()) {
186 GmailCoroutineState::Yielded(GmailYield::WantsRead) => {
187 self.state = State::Listing { list, cycle };
188 return GmailCoroutineState::Yielded(GmailHistoryPollYield::WantsRead);
189 }
190 GmailCoroutineState::Yielded(GmailYield::WantsWrite(out)) => {
191 self.state = State::Listing { list, cycle };
192 return GmailCoroutineState::Yielded(GmailHistoryPollYield::WantsWrite(
193 out,
194 ));
195 }
196 GmailCoroutineState::Complete(Err(err)) => {
197 if err.status() == Some(404) {
198 debug!("history cursor expired, re-baselining");
199 let profile = match GmailProfileGet::new(&self.auth, &self.user_id) {
200 Ok(profile) => profile,
201 Err(err) => {
202 return GmailCoroutineState::Complete(Err(err.into()));
203 }
204 };
205 self.history_id = None;
206 self.state = State::Baseline(profile);
207 continue;
208 }
209 return GmailCoroutineState::Complete(Err(err.into()));
210 }
211 GmailCoroutineState::Complete(Ok(out)) => {
212 let response = out.response;
213
214 for record in &response.history {
215 for message in &record.messages_added {
216 cycle.added_ids.push(message.message.id.clone());
217 }
218 for message in &record.messages_deleted {
219 cycle.removed.push(GmailMessageId {
220 id: message.message.id.clone(),
221 thread_id: message.message.thread_id.clone(),
222 });
223 }
224 for label in &record.labels_added {
225 cycle.labels_added.push(label.clone());
226 }
227 for label in &record.labels_removed {
228 cycle.labels_removed.push(label.clone());
229 }
230 }
231
232 if let Some(token) = response.next_page_token {
233 let list = match self.list_history(Some(&token)) {
234 Ok(list) => list,
235 Err(err) => {
236 return GmailCoroutineState::Complete(Err(err.into()));
237 }
238 };
239 self.state = State::Listing { list, cycle };
240 continue;
241 }
242
243 cycle.new_history_id = response.history_id;
244
245 if cycle.added_ids.is_empty() {
246 let diff = self.finalize(cycle);
247 return GmailCoroutineState::Yielded(GmailHistoryPollYield::Diff(diff));
248 }
249
250 let ids = mem::take(&mut cycle.added_ids);
251 let current = match self.get_message(&ids[0]) {
252 Ok(get) => get,
253 Err(err) => return GmailCoroutineState::Complete(Err(err.into())),
254 };
255 self.state = State::Fetching {
256 ids,
257 index: 0,
258 current,
259 cycle,
260 };
261 }
262 },
263 State::Fetching {
264 ids,
265 index,
266 mut current,
267 mut cycle,
268 } => match current.resume(bytes.take()) {
269 GmailCoroutineState::Yielded(GmailYield::WantsRead) => {
270 self.state = State::Fetching {
271 ids,
272 index,
273 current,
274 cycle,
275 };
276 return GmailCoroutineState::Yielded(GmailHistoryPollYield::WantsRead);
277 }
278 GmailCoroutineState::Yielded(GmailYield::WantsWrite(out)) => {
279 self.state = State::Fetching {
280 ids,
281 index,
282 current,
283 cycle,
284 };
285 return GmailCoroutineState::Yielded(GmailHistoryPollYield::WantsWrite(
286 out,
287 ));
288 }
289 GmailCoroutineState::Complete(result) => {
290 match result {
291 Ok(out) => cycle.added.push(out.response),
292 Err(err) => trace!("skipping message get: {err}"),
296 }
297
298 let index = index + 1;
299 if index < ids.len() {
300 let current = match self.get_message(&ids[index]) {
301 Ok(get) => get,
302 Err(err) => {
303 return GmailCoroutineState::Complete(Err(err.into()));
304 }
305 };
306 self.state = State::Fetching {
307 ids,
308 index,
309 current,
310 cycle,
311 };
312 } else {
313 let diff = self.finalize(cycle);
314 return GmailCoroutineState::Yielded(GmailHistoryPollYield::Diff(diff));
315 }
316 }
317 },
318 State::Done => unreachable!("gmail watch resumed in terminal state"),
321 }
322 }
323 }
324}
325
326#[derive(Default)]
327struct Cycle {
328 added_ids: Vec<String>,
329 added: Vec<GmailMessage>,
330 removed: Vec<GmailMessageId>,
331 labels_added: Vec<GmailHistoryLabel>,
332 labels_removed: Vec<GmailHistoryLabel>,
333 new_history_id: Option<String>,
334}
335
336enum State {
337 Baseline(GmailProfileGet),
338 Sleeping,
339 Listing {
340 list: GmailHistoryList,
341 cycle: Cycle,
342 },
343 Fetching {
344 ids: Vec<String>,
345 index: usize,
346 current: GmailMessageGet,
347 cycle: Cycle,
348 },
349 Done,
350}