1use crate::types::JmapSetError;
8use rusmes_storage::MessageStore;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct Identity {
16 pub id: String,
18 pub name: String,
20 pub email: String,
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub reply_to: Option<Vec<crate::types::EmailAddress>>,
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub bcc: Option<Vec<crate::types::EmailAddress>>,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub text_signature: Option<String>,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub html_signature: Option<String>,
34 pub may_delete: bool,
36}
37
38#[derive(Debug, Clone, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct IdentityGetRequest {
42 pub account_id: String,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub ids: Option<Vec<String>>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub properties: Option<Vec<String>>,
47}
48
49#[derive(Debug, Clone, Serialize)]
51#[serde(rename_all = "camelCase")]
52pub struct IdentityGetResponse {
53 pub account_id: String,
54 pub state: String,
55 pub list: Vec<Identity>,
56 pub not_found: Vec<String>,
57}
58
59#[derive(Debug, Clone, Deserialize)]
61#[serde(rename_all = "camelCase")]
62pub struct IdentitySetRequest {
63 pub account_id: String,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub if_in_state: Option<String>,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub create: Option<HashMap<String, IdentityObject>>,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub update: Option<HashMap<String, serde_json::Value>>,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub destroy: Option<Vec<String>>,
72}
73
74#[derive(Debug, Clone, Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct IdentityObject {
78 pub name: String,
79 pub email: String,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub reply_to: Option<Vec<crate::types::EmailAddress>>,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub bcc: Option<Vec<crate::types::EmailAddress>>,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub text_signature: Option<String>,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub html_signature: Option<String>,
88}
89
90#[derive(Debug, Clone, Serialize)]
92#[serde(rename_all = "camelCase")]
93pub struct IdentitySetResponse {
94 pub account_id: String,
95 pub old_state: String,
96 pub new_state: String,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub created: Option<HashMap<String, Identity>>,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub updated: Option<HashMap<String, Option<Identity>>>,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub destroyed: Option<Vec<String>>,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub not_created: Option<HashMap<String, JmapSetError>>,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub not_updated: Option<HashMap<String, JmapSetError>>,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub not_destroyed: Option<HashMap<String, JmapSetError>>,
109}
110
111#[derive(Debug, Clone, Deserialize)]
113#[serde(rename_all = "camelCase")]
114pub struct IdentityChangesRequest {
115 pub account_id: String,
116 pub since_state: String,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub max_changes: Option<u64>,
119}
120
121#[derive(Debug, Clone, Serialize)]
123#[serde(rename_all = "camelCase")]
124pub struct IdentityChangesResponse {
125 pub account_id: String,
126 pub old_state: String,
127 pub new_state: String,
128 pub has_more_changes: bool,
129 pub created: Vec<String>,
130 pub updated: Vec<String>,
131 pub destroyed: Vec<String>,
132}
133
134pub async fn identity_get(
136 request: IdentityGetRequest,
137 _message_store: &dyn MessageStore,
138) -> anyhow::Result<IdentityGetResponse> {
139 let mut list = Vec::new();
140 let mut not_found = Vec::new();
141
142 let ids = request.ids.unwrap_or_else(|| vec!["default".to_string()]);
144
145 for id in ids {
146 if id == "default" {
147 list.push(Identity {
149 id: "default".to_string(),
150 name: "Default User".to_string(),
151 email: "user@example.com".to_string(),
152 reply_to: None,
153 bcc: None,
154 text_signature: None,
155 html_signature: None,
156 may_delete: false,
157 });
158 } else {
159 not_found.push(id);
160 }
161 }
162
163 Ok(IdentityGetResponse {
164 account_id: request.account_id,
165 state: "1".to_string(),
166 list,
167 not_found,
168 })
169}
170
171#[allow(clippy::too_many_arguments)]
173pub async fn identity_set(
174 request: IdentitySetRequest,
175 _message_store: &dyn MessageStore,
176) -> anyhow::Result<IdentitySetResponse> {
177 let created = HashMap::new();
178 let updated = HashMap::new();
179 let destroyed = Vec::new();
180 let mut not_created = HashMap::new();
181 let mut not_updated = HashMap::new();
182 let mut not_destroyed = HashMap::new();
183
184 if let Some(create_map) = request.create {
186 for (creation_id, _identity_obj) in create_map {
187 not_created.insert(
188 creation_id,
189 JmapSetError {
190 error_type: "notImplemented".to_string(),
191 description: Some("Identity creation not yet implemented".to_string()),
192 },
193 );
194 }
195 }
196
197 if let Some(update_map) = request.update {
199 for (id, _patch) in update_map {
200 not_updated.insert(
201 id,
202 JmapSetError {
203 error_type: "notImplemented".to_string(),
204 description: Some("Identity update not yet implemented".to_string()),
205 },
206 );
207 }
208 }
209
210 if let Some(destroy_ids) = request.destroy {
212 for id in destroy_ids {
213 if id == "default" {
214 not_destroyed.insert(
215 id,
216 JmapSetError {
217 error_type: "forbidden".to_string(),
218 description: Some("Cannot delete default identity".to_string()),
219 },
220 );
221 } else {
222 not_destroyed.insert(
223 id,
224 JmapSetError {
225 error_type: "notImplemented".to_string(),
226 description: Some("Identity deletion not yet implemented".to_string()),
227 },
228 );
229 }
230 }
231 }
232
233 Ok(IdentitySetResponse {
234 account_id: request.account_id,
235 old_state: "1".to_string(),
236 new_state: "2".to_string(),
237 created: if created.is_empty() {
238 None
239 } else {
240 Some(created)
241 },
242 updated: if updated.is_empty() {
243 None
244 } else {
245 Some(updated)
246 },
247 destroyed: if destroyed.is_empty() {
248 None
249 } else {
250 Some(destroyed)
251 },
252 not_created: if not_created.is_empty() {
253 None
254 } else {
255 Some(not_created)
256 },
257 not_updated: if not_updated.is_empty() {
258 None
259 } else {
260 Some(not_updated)
261 },
262 not_destroyed: if not_destroyed.is_empty() {
263 None
264 } else {
265 Some(not_destroyed)
266 },
267 })
268}
269
270pub async fn identity_changes(
272 request: IdentityChangesRequest,
273 _message_store: &dyn MessageStore,
274) -> anyhow::Result<IdentityChangesResponse> {
275 let since_state: u64 = request.since_state.parse().unwrap_or(0);
276 let new_state = (since_state + 1).to_string();
277
278 Ok(IdentityChangesResponse {
279 account_id: request.account_id,
280 old_state: request.since_state,
281 new_state,
282 has_more_changes: false,
283 created: Vec::new(),
284 updated: Vec::new(),
285 destroyed: Vec::new(),
286 })
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use rusmes_storage::backends::filesystem::FilesystemBackend;
293 use rusmes_storage::StorageBackend;
294 use std::path::PathBuf;
295
296 fn create_test_store() -> std::sync::Arc<dyn MessageStore> {
297 let backend = FilesystemBackend::new(PathBuf::from("/tmp/rusmes-test-storage"));
298 backend.message_store()
299 }
300
301 #[tokio::test]
302 async fn test_identity_get() {
303 let store = create_test_store();
304 let request = IdentityGetRequest {
305 account_id: "acc1".to_string(),
306 ids: Some(vec!["default".to_string()]),
307 properties: None,
308 };
309
310 let response = identity_get(request, store.as_ref()).await.unwrap();
311 assert_eq!(response.list.len(), 1);
312 assert_eq!(response.list[0].id, "default");
313 }
314
315 #[tokio::test]
316 async fn test_identity_set_create() {
317 let store = create_test_store();
318 let mut create_map = HashMap::new();
319 create_map.insert(
320 "new1".to_string(),
321 IdentityObject {
322 name: "John Doe".to_string(),
323 email: "john@example.com".to_string(),
324 reply_to: None,
325 bcc: None,
326 text_signature: Some("Best regards,\nJohn".to_string()),
327 html_signature: None,
328 },
329 );
330
331 let request = IdentitySetRequest {
332 account_id: "acc1".to_string(),
333 if_in_state: None,
334 create: Some(create_map),
335 update: None,
336 destroy: None,
337 };
338
339 let response = identity_set(request, store.as_ref()).await.unwrap();
340 assert!(response.not_created.is_some());
341 }
342
343 #[tokio::test]
344 async fn test_identity_changes() {
345 let store = create_test_store();
346 let request = IdentityChangesRequest {
347 account_id: "acc1".to_string(),
348 since_state: "1".to_string(),
349 max_changes: Some(50),
350 };
351
352 let response = identity_changes(request, store.as_ref()).await.unwrap();
353 assert_eq!(response.old_state, "1");
354 assert_eq!(response.new_state, "2");
355 }
356
357 #[tokio::test]
358 async fn test_identity_set_destroy_default() {
359 let store = create_test_store();
360 let request = IdentitySetRequest {
361 account_id: "acc1".to_string(),
362 if_in_state: None,
363 create: None,
364 update: None,
365 destroy: Some(vec!["default".to_string()]),
366 };
367
368 let response = identity_set(request, store.as_ref()).await.unwrap();
369 assert!(response.not_destroyed.is_some());
370 let errors = response.not_destroyed.unwrap();
371 assert_eq!(errors.get("default").unwrap().error_type, "forbidden");
372 }
373
374 #[tokio::test]
375 async fn test_identity_with_signature() {
376 let store = create_test_store();
377 let mut create_map = HashMap::new();
378 create_map.insert(
379 "sig1".to_string(),
380 IdentityObject {
381 name: "Test User".to_string(),
382 email: "test@example.com".to_string(),
383 reply_to: None,
384 bcc: None,
385 text_signature: Some("--\nBest regards".to_string()),
386 html_signature: Some("<p>Best regards</p>".to_string()),
387 },
388 );
389
390 let request = IdentitySetRequest {
391 account_id: "acc1".to_string(),
392 if_in_state: None,
393 create: Some(create_map),
394 update: None,
395 destroy: None,
396 };
397
398 let response = identity_set(request, store.as_ref()).await.unwrap();
399 assert!(response.not_created.is_some());
400 }
401
402 #[tokio::test]
403 async fn test_identity_with_bcc() {
404 let store = create_test_store();
405 let mut create_map = HashMap::new();
406 let bcc = vec![crate::types::EmailAddress::new(
407 "archive@example.com".to_string(),
408 )];
409
410 create_map.insert(
411 "bcc1".to_string(),
412 IdentityObject {
413 name: "Test User".to_string(),
414 email: "test@example.com".to_string(),
415 reply_to: None,
416 bcc: Some(bcc),
417 text_signature: None,
418 html_signature: None,
419 },
420 );
421
422 let request = IdentitySetRequest {
423 account_id: "acc1".to_string(),
424 if_in_state: None,
425 create: Some(create_map),
426 update: None,
427 destroy: None,
428 };
429
430 let response = identity_set(request, store.as_ref()).await.unwrap();
431 assert!(response.not_created.is_some());
432 }
433
434 #[tokio::test]
435 async fn test_identity_get_not_found() {
436 let store = create_test_store();
437 let request = IdentityGetRequest {
438 account_id: "acc1".to_string(),
439 ids: Some(vec!["nonexistent".to_string()]),
440 properties: None,
441 };
442
443 let response = identity_get(request, store.as_ref()).await.unwrap();
444 assert_eq!(response.not_found.len(), 1);
445 }
446
447 #[tokio::test]
448 async fn test_identity_get_all() {
449 let store = create_test_store();
450 let request = IdentityGetRequest {
451 account_id: "acc1".to_string(),
452 ids: None,
453 properties: None,
454 };
455
456 let response = identity_get(request, store.as_ref()).await.unwrap();
457 assert_eq!(response.list.len(), 1);
458 }
459
460 #[tokio::test]
461 async fn test_identity_set_update() {
462 let store = create_test_store();
463 let mut update_map = HashMap::new();
464 update_map.insert(
465 "default".to_string(),
466 serde_json::json!({"name": "New Name"}),
467 );
468
469 let request = IdentitySetRequest {
470 account_id: "acc1".to_string(),
471 if_in_state: None,
472 create: None,
473 update: Some(update_map),
474 destroy: None,
475 };
476
477 let response = identity_set(request, store.as_ref()).await.unwrap();
478 assert!(response.not_updated.is_some());
479 }
480
481 #[tokio::test]
482 async fn test_identity_changes_state_progression() {
483 let store = create_test_store();
484
485 let request1 = IdentityChangesRequest {
486 account_id: "acc1".to_string(),
487 since_state: "5".to_string(),
488 max_changes: None,
489 };
490 let response1 = identity_changes(request1, store.as_ref()).await.unwrap();
491
492 let request2 = IdentityChangesRequest {
493 account_id: "acc1".to_string(),
494 since_state: response1.new_state.clone(),
495 max_changes: None,
496 };
497 let response2 = identity_changes(request2, store.as_ref()).await.unwrap();
498
499 assert!(response1.new_state < response2.new_state);
500 }
501
502 #[tokio::test]
503 async fn test_identity_default_may_not_delete() {
504 let store = create_test_store();
505 let request = IdentityGetRequest {
506 account_id: "acc1".to_string(),
507 ids: Some(vec!["default".to_string()]),
508 properties: None,
509 };
510
511 let response = identity_get(request, store.as_ref()).await.unwrap();
512 assert!(!response.list[0].may_delete);
513 }
514
515 #[tokio::test]
516 async fn test_identity_with_reply_to() {
517 let store = create_test_store();
518 let mut create_map = HashMap::new();
519 let reply_to = vec![crate::types::EmailAddress::new(
520 "support@example.com".to_string(),
521 )];
522
523 create_map.insert(
524 "replyto1".to_string(),
525 IdentityObject {
526 name: "Support".to_string(),
527 email: "noreply@example.com".to_string(),
528 reply_to: Some(reply_to),
529 bcc: None,
530 text_signature: None,
531 html_signature: None,
532 },
533 );
534
535 let request = IdentitySetRequest {
536 account_id: "acc1".to_string(),
537 if_in_state: None,
538 create: Some(create_map),
539 update: None,
540 destroy: None,
541 };
542
543 let response = identity_set(request, store.as_ref()).await.unwrap();
544 assert!(response.not_created.is_some());
545 }
546}