tiny_mailcatcher/
http.rs

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 repository = &res.data().repository;
488        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}