postcrate_core/http/routes/
emails.rs1use axum::body::Body;
2use axum::extract::{Path, Query, State};
3use axum::http::{header, HeaderValue, StatusCode};
4use axum::response::{IntoResponse, Response};
5use axum::routing::{get, post};
6use axum::{Json, Router};
7
8use crate::db::emails::{EmailDetail, EmailSummary};
9use crate::error::{Error, Result};
10use crate::http::dto::{ListMessagesQuery, SearchBody};
11use crate::service::ServiceHandle;
12
13pub fn router() -> Router<ServiceHandle> {
14 Router::new()
15 .route("/messages", get(list))
16 .route("/messages/{id}", get(get_one).delete(delete_one))
17 .route("/messages/{id}/raw", get(get_raw))
18 .route(
19 "/messages/{id}/attachments/{aid}",
20 get(get_attachment),
21 )
22 .route("/messages/search", post(search))
23 .route("/messages/{id}/read", post(mark_read))
24 .route("/messages/{id}/pin", post(set_pin))
25 .route("/messages/{id}/star", post(set_star))
26 .route("/messages/{id}/note", post(set_note))
27 .route("/messages/{id}/tag", post(set_tag))
28 .route("/messages/{id}/release", post(release))
29}
30
31async fn list(
32 State(h): State<ServiceHandle>,
33 Query(q): Query<ListMessagesQuery>,
34) -> Result<Json<Vec<EmailSummary>>> {
35 let mb = q
36 .mailbox_id
37 .ok_or_else(|| Error::Invalid("mailboxId query param required".into()))?;
38 let limit = q.limit.unwrap_or(100).min(1000);
39 let offset = q.offset.unwrap_or(0);
40 Ok(Json(h.as_service().list_emails(&mb, limit, offset).await?))
41}
42
43async fn get_one(
44 State(h): State<ServiceHandle>,
45 Path(id): Path<String>,
46) -> Result<Json<EmailDetail>> {
47 Ok(Json(h.as_service().get_email(&id).await?))
48}
49
50async fn get_raw(
51 State(h): State<ServiceHandle>,
52 Path(id): Path<String>,
53) -> Result<Response> {
54 let bytes = h.as_service().get_email_raw(&id).await?;
55 let mut resp = (StatusCode::OK, bytes).into_response();
56 resp.headers_mut().insert(
57 header::CONTENT_TYPE,
58 HeaderValue::from_static("message/rfc822"),
59 );
60 Ok(resp)
61}
62
63async fn get_attachment(
64 State(h): State<ServiceHandle>,
65 Path((email_id, attachment_id)): Path<(String, String)>,
66) -> Result<Response> {
67 let detail = h.as_service().get_email(&email_id).await?;
70 if !detail.attachments.iter().any(|a| a.id == attachment_id) {
71 return Err(Error::AttachmentNotFound(attachment_id));
72 }
73 let (bytes, name, ct) = h.as_service().get_attachment_blob(&attachment_id).await?;
74 let mut resp = Response::builder().status(StatusCode::OK).body(Body::from(bytes))?;
75 if let Some(ct) = ct {
76 if let Ok(v) = HeaderValue::from_str(&ct) {
77 resp.headers_mut().insert(header::CONTENT_TYPE, v);
78 }
79 } else {
80 resp.headers_mut().insert(
81 header::CONTENT_TYPE,
82 HeaderValue::from_static("application/octet-stream"),
83 );
84 }
85 if let Some(name) = name {
86 let disposition = format!("attachment; filename=\"{}\"", sanitize(&name));
87 if let Ok(v) = HeaderValue::from_str(&disposition) {
88 resp.headers_mut().insert(header::CONTENT_DISPOSITION, v);
89 }
90 }
91 Ok(resp)
92}
93
94async fn delete_one(
95 State(h): State<ServiceHandle>,
96 Path(id): Path<String>,
97) -> Result<Json<serde_json::Value>> {
98 h.as_service().delete_email(&id).await?;
99 Ok(Json(serde_json::json!({"deleted": true})))
100}
101
102async fn search(
103 State(h): State<ServiceHandle>,
104 Json(body): Json<SearchBody>,
105) -> Result<Json<Vec<EmailSummary>>> {
106 let limit = body.limit.unwrap_or(50).min(500);
107 Ok(Json(
108 h.as_service()
109 .search_emails(&body.q, body.mailbox_id.as_deref(), limit)
110 .await?,
111 ))
112}
113
114#[derive(serde::Deserialize)]
115struct ReadBody {
116 read: bool,
117}
118
119async fn mark_read(
120 State(h): State<ServiceHandle>,
121 Path(id): Path<String>,
122 Json(body): Json<ReadBody>,
123) -> Result<Json<serde_json::Value>> {
124 h.as_service().mark_read(&id, body.read).await?;
125 Ok(Json(serde_json::json!({"read": body.read})))
126}
127
128#[derive(serde::Deserialize)]
129struct PinBody {
130 pinned: bool,
131}
132
133async fn set_pin(
134 State(h): State<ServiceHandle>,
135 Path(id): Path<String>,
136 Json(body): Json<PinBody>,
137) -> Result<Json<serde_json::Value>> {
138 h.as_service().set_pinned(&id, body.pinned).await?;
139 Ok(Json(serde_json::json!({"pinned": body.pinned})))
140}
141
142#[derive(serde::Deserialize)]
143struct StarBody {
144 starred: bool,
145}
146
147async fn set_star(
148 State(h): State<ServiceHandle>,
149 Path(id): Path<String>,
150 Json(body): Json<StarBody>,
151) -> Result<Json<serde_json::Value>> {
152 h.as_service().set_starred(&id, body.starred).await?;
153 Ok(Json(serde_json::json!({"starred": body.starred})))
154}
155
156#[derive(serde::Deserialize)]
157struct NoteBody {
158 note: Option<String>,
160}
161
162async fn set_note(
163 State(h): State<ServiceHandle>,
164 Path(id): Path<String>,
165 Json(body): Json<NoteBody>,
166) -> Result<Json<serde_json::Value>> {
167 h.as_service().set_note(&id, body.note.as_deref()).await?;
168 Ok(Json(serde_json::json!({"note": body.note})))
169}
170
171#[derive(serde::Deserialize)]
172struct TagBody {
173 tag: Option<String>,
175}
176
177async fn set_tag(
178 State(h): State<ServiceHandle>,
179 Path(id): Path<String>,
180 Json(body): Json<TagBody>,
181) -> Result<Json<serde_json::Value>> {
182 h.as_service().set_tag(&id, body.tag.as_deref()).await?;
183 Ok(Json(serde_json::json!({"tag": body.tag})))
184}
185
186#[derive(serde::Deserialize)]
187#[serde(rename_all = "camelCase")]
188struct ReleaseBody {
189 to: String,
190 relay: crate::RelayConfig,
191}
192
193async fn release(
194 State(h): State<ServiceHandle>,
195 Path(id): Path<String>,
196 Json(body): Json<ReleaseBody>,
197) -> Result<Json<serde_json::Value>> {
198 h.as_service().release_email(&id, &body.to, &body.relay).await?;
199 Ok(Json(serde_json::json!({"released": true, "to": body.to})))
200}
201
202impl From<axum::http::Error> for Error {
203 fn from(e: axum::http::Error) -> Self {
204 Error::Internal(e.to_string())
205 }
206}
207
208fn sanitize(s: &str) -> String {
209 s.chars()
210 .map(|c| if c == '"' || c == '\\' || c.is_control() { '_' } else { c })
211 .collect()
212}