1use crate::repository::MessageRepository;
2use hyper::http::HeaderValue;
3use hyper::{header, Body, Request, Response, Server, StatusCode};
4use log::info;
5use routerify::ext::RequestExt;
6use routerify::{Middleware, Router, RouterService};
7use serde::Serialize;
8use std::io;
9use std::net::TcpListener;
10use std::sync::{Arc, Mutex};
11
12async fn add_cors_headers(mut res: Response<Body>) -> Result<Response<Body>, io::Error> {
13 let headers = res.headers_mut();
14
15 headers.insert(
16 header::ACCESS_CONTROL_ALLOW_ORIGIN,
17 HeaderValue::from_static("*"),
18 );
19 headers.insert(
20 header::ACCESS_CONTROL_ALLOW_METHODS,
21 HeaderValue::from_static("*"),
22 );
23 headers.insert(
24 header::ACCESS_CONTROL_ALLOW_HEADERS,
25 HeaderValue::from_static("*"),
26 );
27 headers.insert(
28 header::ACCESS_CONTROL_EXPOSE_HEADERS,
29 HeaderValue::from_static("*"),
30 );
31
32 Ok(res)
33}
34
35struct State {
36 repository: Arc<Mutex<MessageRepository>>,
37}
38
39fn router(repository: Arc<Mutex<MessageRepository>>) -> Router<Body, io::Error> {
40 let state = State { repository };
41
42 Router::builder()
43 .middleware(Middleware::post(add_cors_headers))
44 .data(state)
45 .delete("/messages/:id", delete_message)
46 .get("/messages/:id.json", get_message_json)
47 .get("/messages/:id.source", get_message_source)
48 .get("/messages/:id.html", get_message_html)
49 .get("/messages/:id.eml", get_message_eml)
50 .get("/messages/:id.plain", get_message_plain)
51 .get("/messages/:id/parts/:cid", get_message_part)
52 .get("/messages", get_messages)
53 .delete("/messages", delete_messages)
54 .options("/*", options_handler)
55 .any(handler_404)
56 .build()
57 .unwrap()
58}
59
60pub async fn run_http_server(
61 tcp_listener: TcpListener,
62 repository: Arc<Mutex<MessageRepository>>,
63) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
64 info!(
65 "Starting HTTP server on {}",
66 tcp_listener.local_addr().unwrap()
67 );
68
69 let router = router(repository);
70 let service = RouterService::new(router).unwrap();
71
72 let server = Server::from_tcp(tcp_listener).unwrap().serve(service);
73
74 server.await?;
75
76 Ok(())
77}
78
79#[derive(Serialize)]
80struct GetMessagesListItem {
81 id: usize,
82 sender: Option<String>,
83 recipients: Vec<String>,
84 subject: Option<String>,
85 size: String,
86 created_at: String,
87}
88
89async fn get_messages(req: Request<Body>) -> Result<Response<Body>, io::Error> {
90 let repository = &req.data::<State>().unwrap().repository;
91
92 let mut messages = vec![];
93 for message in repository.lock().unwrap().find_all() {
94 messages.push(GetMessagesListItem {
95 id: message.id.unwrap(),
96 sender: message.sender.clone(),
97 recipients: message.recipients.clone(),
98 subject: message.subject.clone(),
99 size: message.size.to_string(),
100 created_at: message.created_at.to_rfc3339(),
101 })
102 }
103
104 Ok(Response::builder()
105 .header("Content-Type", "application/json")
106 .body(serde_json::to_string(&messages).unwrap().into())
107 .unwrap())
108}
109
110async fn delete_messages(req: Request<Body>) -> Result<Response<Body>, io::Error> {
111 let repository = &req.data::<State>().unwrap().repository;
112
113 repository.lock().unwrap().delete_all();
114
115 Ok(Response::builder()
116 .status(StatusCode::NO_CONTENT)
117 .body(Body::empty())
118 .unwrap())
119}
120
121#[derive(Serialize)]
122struct GetMessage {
123 id: usize,
124 sender: Option<String>,
125 recipients: Vec<String>,
126 subject: Option<String>,
127 size: String,
128
129 #[serde(rename = "type")]
130 ty: String,
131 created_at: String,
132 formats: Vec<String>,
133 attachments: Vec<GetMessageAttachment>,
134}
135
136#[derive(Serialize)]
137struct GetMessageAttachment {
138 pub cid: String,
139 #[serde(rename = "type")]
140 pub typ: String,
141 pub filename: String,
142 pub size: usize,
143 pub href: String,
144}
145
146async fn get_message_json(req: Request<Body>) -> Result<Response<Body>, io::Error> {
147 let id: usize = req.param("id").unwrap().parse().unwrap();
148 let repository = &req.data::<State>().unwrap().repository;
149
150 let message = repository.lock().unwrap().find(id).map(|message| {
151 let mut formats = vec!["source".to_string()];
152 if message.html().is_some() {
153 formats.push("html".to_string());
154 }
155
156 if message.plain().is_some() {
157 formats.push("plain".to_string());
158 }
159
160 GetMessage {
161 id,
162 sender: message.sender.clone(),
163 recipients: message.recipients.clone(),
164 subject: message.subject.clone(),
165 size: message.size.to_string(),
166 ty: message.typ.clone(),
167 created_at: message.created_at.to_rfc3339(),
168 formats,
169 attachments: message
170 .parts
171 .iter()
172 .filter(|p| p.is_attachment)
173 .map(|attachment| GetMessageAttachment {
174 cid: attachment.cid.clone(),
175 typ: attachment.typ.clone(),
176 filename: attachment.filename.clone(),
177 size: attachment.size,
178 href: format!("/messages/{}/parts/{}", id, attachment.cid),
179 })
180 .collect(),
181 }
182 });
183
184 if let Some(message) = message {
185 Ok(Response::builder()
186 .header("Content-Type", "application/json")
187 .body(serde_json::to_string(&message).unwrap().into())
188 .unwrap())
189 } else {
190 Ok(Response::builder()
191 .status(StatusCode::NOT_FOUND)
192 .body(Body::empty())
193 .unwrap())
194 }
195}
196
197async fn get_message_html(req: Request<Body>) -> Result<Response<Body>, io::Error> {
198 let id: usize = req.param("id").unwrap().parse().unwrap();
199 let repository = &req.data::<State>().unwrap().repository;
200
201 let repository = repository.lock().unwrap();
202 let html_part = repository.find(id).and_then(|message| message.html());
203
204 return if let Some(html_part) = html_part {
205 Ok(Response::builder()
206 .header(
207 "Content-Type",
208 format!("text/html; charset={}", html_part.charset),
209 )
210 .body(Body::from(html_part.body.clone()))
211 .unwrap())
212 } else {
213 Ok(Response::builder()
214 .status(StatusCode::NOT_FOUND)
215 .body(Body::empty())
216 .unwrap())
217 };
218}
219
220async fn get_message_plain(req: Request<Body>) -> Result<Response<Body>, io::Error> {
221 let id: usize = req.param("id").unwrap().parse().unwrap();
222 let repository = &req.data::<State>().unwrap().repository;
223
224 let repository = repository.lock().unwrap();
225 let html_part = repository.find(id).and_then(|message| message.plain());
226
227 return if let Some(html_part) = html_part {
228 Ok(Response::builder()
229 .header(
230 "Content-Type",
231 format!("text/plain; charset={}", html_part.charset),
232 )
233 .body(Body::from(html_part.body.clone()))
234 .unwrap())
235 } else {
236 Ok(Response::builder()
237 .status(StatusCode::NOT_FOUND)
238 .body(Body::empty())
239 .unwrap())
240 };
241}
242
243async fn get_message_source(req: Request<Body>) -> Result<Response<Body>, io::Error> {
244 let id: usize = req.param("id").unwrap().parse().unwrap();
245 let repository = &req.data::<State>().unwrap().repository;
246
247 let repository = repository.lock().unwrap();
248 let message = repository.find(id);
249
250 return if let Some(message) = message {
251 Ok(Response::builder()
252 .header(
253 "Content-Type",
254 format!("text/plain; charset={}", message.charset),
255 )
256 .body(Body::from(message.source.clone()))
257 .unwrap())
258 } else {
259 Ok(Response::builder()
260 .status(StatusCode::NOT_FOUND)
261 .body(Body::empty())
262 .unwrap())
263 };
264}
265
266async fn get_message_eml(req: Request<Body>) -> Result<Response<Body>, io::Error> {
267 let id: usize = req.param("id").unwrap().parse().unwrap();
268 let repository = &req.data::<State>().unwrap().repository;
269
270 let repository = repository.lock().unwrap();
271 let message = repository.find(id);
272
273 return if let Some(message) = message {
274 Ok(Response::builder()
275 .header(
276 "Content-Type",
277 format!("message/rfc822; charset={}", message.charset),
278 )
279 .body(Body::from(message.source.clone()))
280 .unwrap())
281 } else {
282 Ok(Response::builder()
283 .status(StatusCode::NOT_FOUND)
284 .body(Body::empty())
285 .unwrap())
286 };
287}
288
289async fn get_message_part(req: Request<Body>) -> Result<Response<Body>, io::Error> {
290 let id: usize = req.param("id").unwrap().parse().unwrap();
291 let cid = req.param("cid").unwrap();
292 let repository = &req.data::<State>().unwrap().repository;
293
294 let repository = repository.lock().unwrap();
295 let part = repository
296 .find(id)
297 .and_then(|message| message.parts.iter().find(|part| part.cid.as_str() == cid));
298
299 return if let Some(part) = part {
300 let mut response = Response::builder()
301 .header(
302 "Content-Type",
303 format!("{}; charset={}", part.typ, part.charset),
304 )
305 .body(Body::from(part.body.clone()))
306 .unwrap();
307
308 if part.is_attachment {
309 let content_disposition = HeaderValue::from_str(
310 format!("attachment; filename=\"{}\"", part.filename).as_str(),
311 )
312 .unwrap();
313 response
314 .headers_mut()
315 .insert("Content-Disposition", content_disposition);
316 }
317
318 Ok(response)
319 } else {
320 Ok(Response::builder()
321 .status(StatusCode::NOT_FOUND)
322 .body(Body::empty())
323 .unwrap())
324 };
325}
326
327async fn delete_message(req: Request<Body>) -> Result<Response<Body>, io::Error> {
328 let id: usize = req.param("id").unwrap().parse().unwrap();
329 let repository = &req.data::<State>().unwrap().repository;
330
331 let deleted_message = repository.lock().unwrap().delete(id);
332
333 if deleted_message.is_some() {
334 Ok(Response::builder()
335 .status(StatusCode::NO_CONTENT)
336 .body(Body::empty())
337 .unwrap())
338 } else {
339 Ok(Response::builder()
340 .status(StatusCode::NOT_FOUND)
341 .body(Body::empty())
342 .unwrap())
343 }
344}
345
346async fn handler_404(_: Request<Body>) -> Result<Response<Body>, io::Error> {
347 Ok(Response::builder()
348 .status(StatusCode::NOT_FOUND)
349 .body(Body::from("Page Not Found"))
350 .unwrap())
351}
352
353async fn options_handler(_req: Request<Body>) -> Result<Response<Body>, io::Error> {
354 Ok(Response::new(Body::empty()))
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360 use crate::repository::{Message, MessagePart, MessageRepository};
361 use chrono::{TimeZone, Utc};
362 use hyper::{body, Client, Request, StatusCode};
363 use hyper::{body::HttpBody, Server};
364 use routerify::{Router, RouterService};
365 use std::{
366 net::SocketAddr,
367 sync::{Arc, Mutex},
368 };
369 use tokio::sync::oneshot::{self, Sender};
370
371 #[allow(dead_code)]
372 pub struct Serve {
373 addr: SocketAddr,
374 tx: Sender<()>,
375 }
376
377 impl Serve {
378 pub fn addr(&self) -> SocketAddr {
379 self.addr
380 }
381 }
382
383 pub async fn serve<B, E>(router: Router<B, E>) -> Serve
384 where
385 B: HttpBody + Send + Sync + 'static,
386 E: Into<Box<dyn std::error::Error + Send + Sync>> + 'static,
387 <B as HttpBody>::Data: Send + Sync + 'static,
388 <B as HttpBody>::Error: Into<Box<dyn std::error::Error + Send + Sync>> + 'static,
389 {
390 let service = RouterService::new(router).unwrap();
391 let server = Server::bind(&([127, 0, 0, 1], 0).into()).serve(service);
392 let addr = server.local_addr();
393
394 let (tx, rx) = oneshot::channel::<()>();
395
396 let graceful_server = server.with_graceful_shutdown(async {
397 rx.await.unwrap();
398 });
399
400 tokio::spawn(async move {
401 graceful_server.await.unwrap();
402 });
403
404 Serve { addr, tx }
405 }
406
407 async fn body_to_string(body: Body) -> String {
408 return String::from_utf8(body::to_bytes(body).await.unwrap().to_vec()).unwrap();
409 }
410
411 async fn body_to_json(body: Body) -> serde_json::Value {
412 return serde_json::from_str(body_to_string(body).await.as_str()).unwrap();
413 }
414
415 fn create_test_message() -> Message {
416 Message {
417 id: Some(1),
418 size: 42,
419 subject: Some("This is the subject".to_string()),
420 sender: Some("sender@example.com".to_string()),
421 recipients: vec!["recipient@example.com".to_string()],
422 created_at: Utc.timestamp(1431648000, 0),
423 typ: "text/plain".to_string(),
424 parts: vec![],
425 charset: "UTF-8".to_string(),
426 source: b"Subject: This is the subject\r\n\r\nHello world!\r\n".to_vec(),
427 }
428 }
429
430 #[tokio::test]
431 async fn test_get_messages_returns_messages_in_repository() {
432 let repository = Arc::new(Mutex::new(MessageRepository::new()));
433 repository.lock().unwrap().persist(create_test_message());
434
435 let router = router(repository);
436
437 let serve = serve(router).await;
438 let res = Client::new()
439 .request(
440 Request::builder()
441 .method("GET")
442 .uri(format!("http://{}/{}", serve.addr(), "messages"))
443 .body(Body::empty())
444 .unwrap(),
445 )
446 .await
447 .unwrap();
448
449 let expected = serde_json::json!([
450 {
451 "created_at": "2015-05-15T00:00:00+00:00",
452 "id": 1,
453 "recipients": ["recipient@example.com"],
454 "sender": "sender@example.com",
455 "size": "42",
456 "subject": "This is the subject",
457 }
458 ]);
459
460 assert_eq!(StatusCode::OK, res.status());
461 assert_eq!(expected, body_to_json(res.into_body()).await);
462 }
463
464 #[tokio::test]
465 async fn test_delete_messages_clears_repository() {
466 let repository = Arc::new(Mutex::new(MessageRepository::new()));
467 repository.lock().unwrap().persist(create_test_message());
468
469 let router = router(Arc::clone(&repository));
470
471 let serve = serve(router).await;
472 let res = Client::new()
473 .request(
474 Request::builder()
475 .method("DELETE")
476 .uri(format!("http://{}/{}", serve.addr(), "messages"))
477 .body(Body::empty())
478 .unwrap(),
479 )
480 .await
481 .unwrap();
482
483 let repository = Arc::clone(&repository);
484 let handle = repository.lock().unwrap();
485 let repository_messages = handle.find_all();
486
487 let expected_messages: Vec<&Message> = vec![];
489 assert_eq!(StatusCode::NO_CONTENT, res.status());
490 assert_eq!(expected_messages, repository_messages);
491 }
492
493 #[tokio::test]
494 async fn test_get_message_json() {
495 let repository = Arc::new(Mutex::new(MessageRepository::new()));
496 repository.lock().unwrap().persist(create_test_message());
497
498 let router = router(repository);
499
500 let serve = serve(router).await;
501 let res = Client::new()
502 .request(
503 Request::builder()
504 .method("GET")
505 .uri(format!("http://{}/{}", serve.addr(), "messages/1.json"))
506 .body(Body::empty())
507 .unwrap(),
508 )
509 .await
510 .unwrap();
511
512 assert_eq!(StatusCode::OK, res.status());
513
514 let expected_message = serde_json::json!({
515 "created_at": "2015-05-15T00:00:00+00:00",
516 "id": 1,
517 "recipients": ["recipient@example.com"],
518 "sender": "sender@example.com",
519 "size": "42",
520 "subject": "This is the subject",
521 "attachments": [],
522 "formats": ["source"],
523 "type": "text/plain",
524 });
525 assert_eq!(expected_message, body_to_json(res.into_body()).await);
526 }
527
528 #[tokio::test]
529 async fn test_get_message_html() {
530 let repository = Arc::new(Mutex::new(MessageRepository::new()));
531 repository.lock().unwrap().persist(Message {
532 id: Some(1),
533 size: 42,
534 subject: Some("This is the subject".to_string()),
535 sender: Some("sender@example.com".to_string()),
536 recipients: vec!["recipient@example.com".to_string()],
537 created_at: Utc.timestamp(1431648000, 0),
538 typ: "text/plain".to_string(),
539 parts: vec![MessagePart {
540 cid: "some-id".to_string(),
541 typ: "text/html".to_string(),
542 filename: "some_html_abc123".to_string(),
543 size: 422,
544 charset: "UTF-8".to_string(),
545 body: b"<html>Hello world</html".to_vec(),
546 is_attachment: false,
547 }],
548 charset: "UTF-8".to_string(),
549 source: b"Subject: This is the subject\r\n\r\nHello world!\r\n".to_vec(),
550 });
551
552 let router = router(repository);
553
554 let serve = serve(router).await;
555 let res = Client::new()
556 .request(
557 Request::builder()
558 .method("GET")
559 .uri(format!("http://{}/{}", serve.addr(), "messages/1.html"))
560 .body(Body::empty())
561 .unwrap(),
562 )
563 .await
564 .unwrap();
565
566 assert_eq!(StatusCode::OK, res.status());
567 assert_eq!(
568 "<html>Hello world</html",
569 body_to_string(res.into_body()).await
570 );
571 }
572
573 #[tokio::test]
574 async fn test_get_message_plain() {
575 let repository = Arc::new(Mutex::new(MessageRepository::new()));
576 repository.lock().unwrap().persist(Message {
577 id: Some(1),
578 size: 42,
579 subject: Some("This is the subject".to_string()),
580 sender: Some("sender@example.com".to_string()),
581 recipients: vec!["recipient@example.com".to_string()],
582 created_at: Utc.timestamp(1431648000, 0),
583 typ: "multipart/mixed".to_string(),
584 parts: vec![MessagePart {
585 cid: "some-id".to_string(),
586 typ: "text/plain".to_string(),
587 filename: "some_html_abc123".to_string(),
588 size: 422,
589 charset: "UTF-8".to_string(),
590 body: b"This is some plaintext".to_vec(),
591 is_attachment: false,
592 }],
593 charset: "UTF-8".to_string(),
594 source: b"Subject: This is the subject\r\n\r\nHello world!\r\n".to_vec(),
595 });
596
597 let router = router(repository);
598
599 let serve = serve(router).await;
600 let res = Client::new()
601 .request(
602 Request::builder()
603 .method("GET")
604 .uri(format!("http://{}/{}", serve.addr(), "messages/1.plain"))
605 .body(Body::empty())
606 .unwrap(),
607 )
608 .await
609 .unwrap();
610
611 assert_eq!(StatusCode::OK, res.status());
612 assert_eq!(
613 "This is some plaintext",
614 body_to_string(res.into_body()).await
615 );
616 }
617
618 #[tokio::test]
619 async fn test_get_message_source() {
620 let repository = Arc::new(Mutex::new(MessageRepository::new()));
621 repository.lock().unwrap().persist(Message {
622 id: Some(1),
623 size: 42,
624 subject: Some("This is the subject".to_string()),
625 sender: Some("sender@example.com".to_string()),
626 recipients: vec!["recipient@example.com".to_string()],
627 created_at: Utc.timestamp(1431648000, 0),
628 typ: "multipart/mixed".to_string(),
629 parts: vec![MessagePart {
630 cid: "some-id".to_string(),
631 typ: "text/plain".to_string(),
632 filename: "some_html_abc123".to_string(),
633 size: 422,
634 charset: "UTF-8".to_string(),
635 body: b"This is some plaintext".to_vec(),
636 is_attachment: false,
637 }],
638 charset: "UTF-8".to_string(),
639 source: b"Subject: This is the subject\r\n\r\nHello world!\r\n".to_vec(),
640 });
641
642 let router = router(repository);
643
644 let serve = serve(router).await;
645 let res = Client::new()
646 .request(
647 Request::builder()
648 .method("GET")
649 .uri(format!("http://{}/{}", serve.addr(), "messages/1.source"))
650 .body(Body::empty())
651 .unwrap(),
652 )
653 .await
654 .unwrap();
655
656 assert_eq!(StatusCode::OK, res.status());
657 assert_eq!(
658 "Subject: This is the subject\r\n\r\nHello world!\r\n",
659 body_to_string(res.into_body()).await
660 );
661 }
662
663 #[tokio::test]
664 async fn test_get_message_eml() {
665 let repository = Arc::new(Mutex::new(MessageRepository::new()));
666 repository.lock().unwrap().persist(Message {
667 id: Some(1),
668 size: 42,
669 subject: Some("This is the subject".to_string()),
670 sender: Some("sender@example.com".to_string()),
671 recipients: vec!["recipient@example.com".to_string()],
672 created_at: Utc.timestamp(1431648000, 0),
673 typ: "multipart/mixed".to_string(),
674 parts: vec![MessagePart {
675 cid: "some-id".to_string(),
676 typ: "text/plain".to_string(),
677 filename: "some_html_abc123".to_string(),
678 size: 422,
679 charset: "UTF-8".to_string(),
680 body: b"This is some plaintext".to_vec(),
681 is_attachment: false,
682 }],
683 charset: "UTF-8".to_string(),
684 source: b"Subject: This is the subject\r\n\r\nHello world!\r\n".to_vec(),
685 });
686
687 let router = router(repository);
688
689 let serve = serve(router).await;
690 let res = Client::new()
691 .request(
692 Request::builder()
693 .method("GET")
694 .uri(format!("http://{}/{}", serve.addr(), "messages/1.eml"))
695 .body(Body::empty())
696 .unwrap(),
697 )
698 .await
699 .unwrap();
700
701 assert_eq!(StatusCode::OK, res.status());
702 assert_eq!(
703 "message/rfc822; charset=UTF-8",
704 res.headers().get("Content-Type").unwrap().to_str().unwrap()
705 );
706 assert_eq!(
707 "Subject: This is the subject\r\n\r\nHello world!\r\n",
708 body_to_string(res.into_body()).await
709 );
710 }
711
712 #[tokio::test]
713 async fn test_get_message_part() {
714 let repository = Arc::new(Mutex::new(MessageRepository::new()));
715 repository.lock().unwrap().persist(Message {
716 id: Some(1),
717 size: 42,
718 subject: Some("This is the subject".to_string()),
719 sender: Some("sender@example.com".to_string()),
720 recipients: vec!["recipient@example.com".to_string()],
721 created_at: Utc.timestamp(1431648000, 0),
722 typ: "multipart/mixed".to_string(),
723 parts: vec![MessagePart {
724 cid: "some-id".to_string(),
725 typ: "text/plain".to_string(),
726 filename: "some_textfile.txt".to_string(),
727 size: 422,
728 charset: "UTF-8".to_string(),
729 body: b"This is some plaintext as an attachment".to_vec(),
730 is_attachment: true,
731 }],
732 charset: "UTF-8".to_string(),
733 source: b"Subject: This is the subject\r\n\r\nHello world!\r\n".to_vec(),
734 });
735
736 let router = router(repository);
737
738 let serve = serve(router).await;
739 let res = Client::new()
740 .request(
741 Request::builder()
742 .method("GET")
743 .uri(format!(
744 "http://{}/{}",
745 serve.addr(),
746 "messages/1/parts/some-id"
747 ))
748 .body(Body::empty())
749 .unwrap(),
750 )
751 .await
752 .unwrap();
753
754 assert_eq!(StatusCode::OK, res.status());
755 assert_eq!(
756 "attachment; filename=\"some_textfile.txt\"",
757 res.headers()
758 .get("Content-Disposition")
759 .unwrap()
760 .to_str()
761 .unwrap()
762 );
763 assert_eq!(
764 "text/plain; charset=UTF-8",
765 res.headers().get("Content-Type").unwrap().to_str().unwrap()
766 );
767 assert_eq!(
768 "This is some plaintext as an attachment",
769 body_to_string(res.into_body()).await
770 );
771 }
772
773 #[tokio::test]
774 async fn test_delete_message() {
775 let repository = Arc::new(Mutex::new(MessageRepository::new()));
776 repository.lock().unwrap().persist(Message {
777 id: Some(1),
778 size: 42,
779 subject: Some("This is the subject".to_string()),
780 sender: Some("sender@example.com".to_string()),
781 recipients: vec!["recipient@example.com".to_string()],
782 created_at: Utc.timestamp(1431648000, 0),
783 typ: "multipart/mixed".to_string(),
784 parts: vec![],
785 charset: "UTF-8".to_string(),
786 source: b"Subject: This is the subject\r\n\r\nHello world!\r\n".to_vec(),
787 });
788
789 let router = router(Arc::clone(&repository));
790
791 let serve = serve(router).await;
792 let res = Client::new()
793 .request(
794 Request::builder()
795 .method("DELETE")
796 .uri(format!("http://{}/{}", serve.addr(), "messages/1"))
797 .body(Body::empty())
798 .unwrap(),
799 )
800 .await
801 .unwrap();
802
803 let repository = Arc::clone(&repository);
804 let handle = repository.lock().unwrap();
805
806 let message = handle.find(1);
807
808 assert_eq!(StatusCode::NO_CONTENT, res.status());
809 assert_eq!(None, message);
810 }
811}