1use bitwarden_api_api::ResponseContent;
2use bitwarden_core::ApiError;
3use bitwarden_error::bitwarden_error;
4use bitwarden_state::{
5 Setting, register_setting_key,
6 repository::{Repository, RepositoryError},
7};
8use thiserror::Error;
9#[cfg(feature = "wasm")]
10use wasm_bindgen::prelude::*;
11
12use crate::{Send, SendId, send_client::SendClient};
13
14register_setting_key!(const PENDING_SEND_DELETIONS: Vec<SendId> = "pending_send_deletions");
16
17#[allow(missing_docs)]
18#[bitwarden_error(flat)]
19#[derive(Debug, Error)]
20pub enum DeleteSendError {
21 #[error(transparent)]
22 Api(#[from] ApiError),
23 #[error(transparent)]
24 Repository(#[from] RepositoryError),
25 #[error(transparent)]
26 State(#[from] bitwarden_state::SettingsError),
27}
28
29async fn delete_send<R: Repository<Send> + ?Sized>(
30 api_client: &bitwarden_api_api::apis::ApiClient,
31 repository: &R,
32 pending_deletions: &Setting<Vec<SendId>>,
33 send_id: SendId,
34) -> Result<(), DeleteSendError> {
35 if let Err(e) = api_client.sends_api().delete(&send_id.to_string()).await {
36 queue_pending_deletion(pending_deletions, send_id).await;
38 return Err(e.into());
39 }
40
41 repository.remove(send_id).await?;
42
43 Ok(())
44}
45
46async fn queue_pending_deletion(pending_deletions: &Setting<Vec<SendId>>, send_id: SendId) {
49 let mut queue = match pending_deletions.get().await {
50 Ok(queue) => queue.unwrap_or_default(),
51 Err(e) => {
52 tracing::warn!("Failed to read pending send deletions: {e}");
53 return;
54 }
55 };
56
57 if queue.contains(&send_id) {
58 return;
59 }
60
61 queue.push(send_id);
62 if let Err(e) = pending_deletions.update(queue).await {
63 tracing::warn!("Failed to queue send for pending deletion: {e}");
64 }
65}
66
67async fn retry_pending_deletions<R: Repository<Send> + ?Sized>(
68 api_client: &bitwarden_api_api::apis::ApiClient,
69 repository: &R,
70 pending_deletions: &Setting<Vec<SendId>>,
71) -> Result<(), DeleteSendError> {
72 let queue = pending_deletions.get().await?.unwrap_or_default();
73 if queue.is_empty() {
74 return Ok(());
75 }
76
77 let mut resolved = Vec::new();
78 for send_id in queue {
79 match api_client.sends_api().delete(&send_id.to_string()).await {
80 Ok(())
82 | Err(ApiError::Response(ResponseContent {
83 status: reqwest::StatusCode::NOT_FOUND,
84 ..
85 })) => {
86 let _ = repository.remove(send_id).await;
88 resolved.push(send_id);
89 }
90 Err(_) => {}
91 }
92 }
93
94 if resolved.is_empty() {
95 return Ok(());
96 }
97
98 let mut current = pending_deletions.get().await?.unwrap_or_default();
100 current.retain(|id| !resolved.contains(id));
101 pending_deletions.update(current).await?;
102
103 Ok(())
104}
105
106#[cfg_attr(feature = "wasm", wasm_bindgen)]
107impl SendClient {
108 pub async fn delete(&self, send_id: SendId) -> Result<(), DeleteSendError> {
110 let config = self.client.internal.get_api_configurations();
111 let repository = self.get_repository()?;
112 let pending_deletions = self
113 .client
114 .platform()
115 .state()
116 .setting(PENDING_SEND_DELETIONS)?;
117
118 delete_send(
119 &config.api_client,
120 repository.as_ref(),
121 &pending_deletions,
122 send_id,
123 )
124 .await
125 }
126
127 pub async fn retry_pending_deletions(&self) -> Result<(), DeleteSendError> {
130 let config = self.client.internal.get_api_configurations();
131 let repository = self.get_repository()?;
132 let pending_deletions = self
133 .client
134 .platform()
135 .state()
136 .setting(PENDING_SEND_DELETIONS)?;
137
138 retry_pending_deletions(&config.api_client, repository.as_ref(), &pending_deletions).await
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use std::sync::Arc;
145
146 use bitwarden_api_api::apis::ApiClient;
147 use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
148 use bitwarden_crypto::{KeyStore, SymmetricKeyAlgorithm};
149 use bitwarden_state::SettingItem;
150 use bitwarden_test::MemoryRepository;
151 use uuid::uuid;
152
153 use super::*;
154 use crate::{AuthType, Send, SendId, SendTextView, SendType, SendView};
155
156 fn make_pending_setting() -> Setting<Vec<SendId>> {
157 let repository: Arc<dyn Repository<SettingItem>> =
158 Arc::new(MemoryRepository::<SettingItem>::default());
159 Setting::new(repository, PENDING_SEND_DELETIONS)
160 }
161
162 async fn make_store_with_send(
163 send_id: uuid::Uuid,
164 ) -> (KeyStore<KeySlotIds>, MemoryRepository<Send>) {
165 let store: KeyStore<KeySlotIds> = KeyStore::default();
166 {
167 let mut ctx = store.context_mut();
168 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
169 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
170 .unwrap();
171 }
172
173 let repository = MemoryRepository::<Send>::default();
174 let send_view = SendView {
175 id: None,
176 access_id: None,
177 name: "Test Send".to_string(),
178 notes: None,
179 key: None,
180 new_password: None,
181 has_password: false,
182 r#type: SendType::Text,
183 file: None,
184 text: Some(SendTextView {
185 text: Some("Secret text".to_string()),
186 hidden: false,
187 }),
188 data: None,
189 max_access_count: None,
190 access_count: 0,
191 disabled: false,
192 hide_email: false,
193 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
194 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
195 expiration_date: None,
196 emails: Vec::new(),
197 auth_type: AuthType::None,
198 };
199 let mut send = store.encrypt(send_view).unwrap();
200 send.id = Some(SendId::new(send_id));
201 repository.set(SendId::new(send_id), send).await.unwrap();
202
203 (store, repository)
204 }
205
206 #[tokio::test]
207 async fn test_delete_send() {
208 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
209 let (_store, repository) = make_store_with_send(send_id).await;
210 let pending = make_pending_setting();
211
212 let api_client = ApiClient::new_mocked(move |mock| {
213 mock.sends_api
214 .expect_delete()
215 .returning(move |_id| Ok(()))
216 .once();
217 });
218
219 let result = delete_send(&api_client, &repository, &pending, SendId::new(send_id)).await;
220
221 assert!(result.is_ok());
222 assert!(
223 repository
224 .get(SendId::new(send_id))
225 .await
226 .unwrap()
227 .is_none()
228 );
229 }
230
231 #[tokio::test]
232 async fn test_delete_send_http_error() {
233 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
234 let (_store, repository) = make_store_with_send(send_id).await;
235 let pending = make_pending_setting();
236
237 let api_client = ApiClient::new_mocked(move |mock| {
238 mock.sends_api
239 .expect_delete()
240 .returning(move |_id| {
241 Err(bitwarden_api_api::ApiError::Io(std::io::Error::other(
242 "Simulated error",
243 )))
244 })
245 .once();
246 });
247
248 let result = delete_send(&api_client, &repository, &pending, SendId::new(send_id)).await;
249
250 assert!(result.is_err());
251 assert!(matches!(result.unwrap_err(), DeleteSendError::Api(_)));
252 assert!(
254 repository
255 .get(SendId::new(send_id))
256 .await
257 .unwrap()
258 .is_some()
259 );
260 }
261
262 #[tokio::test]
263 async fn test_delete_send_failure_queues_pending_deletion() {
264 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
265 let (_store, repository) = make_store_with_send(send_id).await;
266 let pending = make_pending_setting();
267
268 let api_client = ApiClient::new_mocked(move |mock| {
269 mock.sends_api.expect_delete().returning(move |_id| {
270 Err(bitwarden_api_api::ApiError::Io(std::io::Error::other(
271 "Simulated error",
272 )))
273 });
274 });
275
276 let result = delete_send(&api_client, &repository, &pending, SendId::new(send_id)).await;
277 assert!(result.is_err());
278
279 let queue = pending.get().await.unwrap().unwrap_or_default();
280 assert_eq!(queue, vec![SendId::new(send_id)]);
281 }
282
283 #[tokio::test]
284 async fn test_delete_send_failure_does_not_duplicate_queue_entry() {
285 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
286 let (_store, repository) = make_store_with_send(send_id).await;
287 let pending = make_pending_setting();
288
289 let api_client = ApiClient::new_mocked(move |mock| {
290 mock.sends_api.expect_delete().returning(move |_id| {
291 Err(bitwarden_api_api::ApiError::Io(std::io::Error::other(
292 "Simulated error",
293 )))
294 });
295 });
296
297 assert!(
298 delete_send(&api_client, &repository, &pending, SendId::new(send_id))
299 .await
300 .is_err()
301 );
302 assert!(
303 delete_send(&api_client, &repository, &pending, SendId::new(send_id))
304 .await
305 .is_err()
306 );
307
308 let queue = pending.get().await.unwrap().unwrap_or_default();
309 assert_eq!(queue, vec![SendId::new(send_id)]);
310 }
311
312 #[tokio::test]
313 async fn test_retry_pending_deletions_success_removes_from_queue_and_repository() {
314 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
315 let (_store, repository) = make_store_with_send(send_id).await;
316 let pending = make_pending_setting();
317 pending.update(vec![SendId::new(send_id)]).await.unwrap();
318
319 let api_client = ApiClient::new_mocked(move |mock| {
320 mock.sends_api
321 .expect_delete()
322 .returning(move |_id| Ok(()))
323 .once();
324 });
325
326 retry_pending_deletions(&api_client, &repository, &pending)
327 .await
328 .unwrap();
329
330 let queue = pending.get().await.unwrap().unwrap_or_default();
331 assert!(queue.is_empty());
332 assert!(
333 repository
334 .get(SendId::new(send_id))
335 .await
336 .unwrap()
337 .is_none()
338 );
339 }
340
341 #[tokio::test]
342 async fn test_retry_pending_deletions_not_found_is_resolved() {
343 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
344 let (_store, repository) = make_store_with_send(send_id).await;
345 let pending = make_pending_setting();
346 pending.update(vec![SendId::new(send_id)]).await.unwrap();
347
348 let api_client = ApiClient::new_mocked(move |mock| {
349 mock.sends_api
350 .expect_delete()
351 .returning(move |_id| {
352 Err(bitwarden_api_api::ApiError::Response(ResponseContent {
353 status: reqwest::StatusCode::NOT_FOUND,
354 message: "not found".to_string(),
355 }))
356 })
357 .once();
358 });
359
360 retry_pending_deletions(&api_client, &repository, &pending)
361 .await
362 .unwrap();
363
364 let queue = pending.get().await.unwrap().unwrap_or_default();
365 assert!(queue.is_empty());
366 assert!(
367 repository
368 .get(SendId::new(send_id))
369 .await
370 .unwrap()
371 .is_none()
372 );
373 }
374
375 #[tokio::test]
376 async fn test_retry_pending_deletions_other_error_stays_queued() {
377 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
378 let (_store, repository) = make_store_with_send(send_id).await;
379 let pending = make_pending_setting();
380 pending.update(vec![SendId::new(send_id)]).await.unwrap();
381
382 let api_client = ApiClient::new_mocked(move |mock| {
383 mock.sends_api
384 .expect_delete()
385 .returning(move |_id| {
386 Err(bitwarden_api_api::ApiError::Io(std::io::Error::other(
387 "Simulated error",
388 )))
389 })
390 .once();
391 });
392
393 retry_pending_deletions(&api_client, &repository, &pending)
394 .await
395 .unwrap();
396
397 let queue = pending.get().await.unwrap().unwrap_or_default();
398 assert_eq!(queue, vec![SendId::new(send_id)]);
399 assert!(
401 repository
402 .get(SendId::new(send_id))
403 .await
404 .unwrap()
405 .is_some()
406 );
407 }
408
409 #[tokio::test]
410 async fn test_retry_pending_deletions_mixed_batch() {
411 let resolved_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
412 let failing_id = uuid!("3b8e0f2a-1c4d-4e6f-9a7b-2d5c8e1f0a3b");
413 let (store, repository) = make_store_with_send(resolved_id).await;
414 let mut send = store
416 .encrypt(SendView {
417 id: None,
418 access_id: None,
419 name: "Test Send".to_string(),
420 notes: None,
421 key: None,
422 new_password: None,
423 has_password: false,
424 r#type: SendType::Text,
425 file: None,
426 text: Some(SendTextView {
427 text: Some("Secret text".to_string()),
428 hidden: false,
429 }),
430 data: None,
431 max_access_count: None,
432 access_count: 0,
433 disabled: false,
434 hide_email: false,
435 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
436 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
437 expiration_date: None,
438 emails: Vec::new(),
439 auth_type: AuthType::None,
440 })
441 .unwrap();
442 send.id = Some(SendId::new(failing_id));
443 repository.set(SendId::new(failing_id), send).await.unwrap();
444
445 let pending = make_pending_setting();
446 pending
447 .update(vec![SendId::new(resolved_id), SendId::new(failing_id)])
448 .await
449 .unwrap();
450
451 let resolved_id_str = resolved_id.to_string();
452 let api_client = ApiClient::new_mocked(move |mock| {
453 mock.sends_api.expect_delete().returning(move |id| {
454 if id == resolved_id_str {
455 Ok(())
456 } else {
457 Err(bitwarden_api_api::ApiError::Io(std::io::Error::other(
458 "Simulated error",
459 )))
460 }
461 });
462 });
463
464 retry_pending_deletions(&api_client, &repository, &pending)
465 .await
466 .unwrap();
467
468 let queue = pending.get().await.unwrap().unwrap_or_default();
469 assert_eq!(queue, vec![SendId::new(failing_id)]);
470 assert!(
471 repository
472 .get(SendId::new(resolved_id))
473 .await
474 .unwrap()
475 .is_none()
476 );
477 assert!(
478 repository
479 .get(SendId::new(failing_id))
480 .await
481 .unwrap()
482 .is_some()
483 );
484 }
485
486 #[tokio::test]
487 async fn test_retry_pending_deletions_empty_queue_is_noop() {
488 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
489 let (_store, repository) = make_store_with_send(send_id).await;
490 let pending = make_pending_setting();
491
492 let api_client = ApiClient::new_mocked(|_mock| {});
494
495 retry_pending_deletions(&api_client, &repository, &pending)
496 .await
497 .unwrap();
498 }
499
500 #[tokio::test]
501 async fn test_delete_send_failure_then_retry_success_resolves_orphan() {
502 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
503 let (_store, repository) = make_store_with_send(send_id).await;
504 let pending = make_pending_setting();
505
506 let attempts = Arc::new(std::sync::atomic::AtomicUsize::new(0));
507 let api_client = ApiClient::new_mocked(move |mock| {
508 let attempts = attempts.clone();
509 mock.sends_api.expect_delete().returning(move |_id| {
510 if attempts.fetch_add(1, std::sync::atomic::Ordering::SeqCst) == 0 {
511 Err(bitwarden_api_api::ApiError::Io(std::io::Error::other(
512 "offline",
513 )))
514 } else {
515 Ok(())
516 }
517 });
518 });
519
520 let result = delete_send(&api_client, &repository, &pending, SendId::new(send_id)).await;
522 assert!(result.is_err());
523 assert!(
524 repository
525 .get(SendId::new(send_id))
526 .await
527 .unwrap()
528 .is_some(),
529 "orphaned Send stays visible until a retry resolves it"
530 );
531
532 retry_pending_deletions(&api_client, &repository, &pending)
534 .await
535 .unwrap();
536
537 assert!(
538 repository
539 .get(SendId::new(send_id))
540 .await
541 .unwrap()
542 .is_none()
543 );
544 assert!(pending.get().await.unwrap().unwrap_or_default().is_empty());
545 }
546}