Skip to main content

rusmes_jmap/methods/
identity.rs

1//! Identity method implementations for JMAP
2//!
3//! Implements:
4//! - Identity/get, Identity/set - sender identities
5//! - Identity/changes - identity tracking
6
7use crate::methods::ensure_account_ownership;
8use crate::types::{JmapSetError, Principal};
9use async_trait::async_trait;
10use rusmes_storage::MessageStore;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::PathBuf;
14
15/// Persisted state for one account's identities
16#[derive(Debug, Clone, Serialize, Deserialize)]
17struct IdentityAccountState {
18    /// Map of identity id → Identity
19    identities: HashMap<String, Identity>,
20    /// Monotonic version counter; incremented on every mutation
21    state_version: u64,
22}
23
24impl IdentityAccountState {
25    fn new_with_default(account_id: &str, username: &str) -> Self {
26        let default_email = if username.contains('@') {
27            username.to_string()
28        } else {
29            format!("{}@localhost", account_id)
30        };
31
32        let mut identities = HashMap::new();
33        identities.insert(
34            "default".to_string(),
35            Identity {
36                id: "default".to_string(),
37                name: "Default User".to_string(),
38                email: default_email,
39                reply_to: None,
40                bcc: None,
41                text_signature: None,
42                html_signature: None,
43                may_delete: false,
44            },
45        );
46
47        Self {
48            identities,
49            state_version: 1,
50        }
51    }
52}
53
54/// Trait for identity persistence
55#[async_trait]
56pub trait IdentityStore: Send + Sync {
57    /// Return all identities for an account (pre-populates default if absent)
58    async fn list_identities(
59        &self,
60        account_id: &str,
61        username: &str,
62    ) -> anyhow::Result<Vec<Identity>>;
63
64    /// Return one identity by id, or None
65    async fn get_identity(
66        &self,
67        account_id: &str,
68        username: &str,
69        id: &str,
70    ) -> anyhow::Result<Option<Identity>>;
71
72    /// Create a new identity, returning the stored object
73    async fn create_identity(
74        &self,
75        account_id: &str,
76        username: &str,
77        identity: Identity,
78    ) -> anyhow::Result<Identity>;
79
80    /// Update an identity via a flat JSON patch (top-level keys only), returning the updated object
81    async fn update_identity(
82        &self,
83        account_id: &str,
84        username: &str,
85        id: &str,
86        patch: &serde_json::Value,
87    ) -> anyhow::Result<Identity>;
88
89    /// Delete an identity by id; caller must reject "default" before calling
90    async fn delete_identity(
91        &self,
92        account_id: &str,
93        username: &str,
94        id: &str,
95    ) -> anyhow::Result<()>;
96
97    /// Return the current state token for an account
98    async fn state_token(&self, account_id: &str, username: &str) -> anyhow::Result<String>;
99}
100
101/// Filesystem-backed identity store.
102///
103/// Each account's identities are persisted to
104/// `{base_dir}/identities/{account_id}.json`.
105pub struct FileIdentityStore {
106    base_dir: PathBuf,
107}
108
109impl FileIdentityStore {
110    /// Create a new store rooted at `base_dir`.
111    pub fn new(base_dir: impl Into<PathBuf>) -> Self {
112        Self {
113            base_dir: base_dir.into(),
114        }
115    }
116
117    fn account_path(&self, account_id: &str) -> PathBuf {
118        self.base_dir
119            .join("identities")
120            .join(format!("{}.json", account_id))
121    }
122
123    async fn load(&self, account_id: &str, username: &str) -> anyhow::Result<IdentityAccountState> {
124        let path = self.account_path(account_id);
125        if !path.exists() {
126            return Ok(IdentityAccountState::new_with_default(account_id, username));
127        }
128        let bytes = tokio::fs::read(&path).await?;
129        let state: IdentityAccountState = serde_json::from_slice(&bytes)?;
130        Ok(state)
131    }
132
133    async fn save(&self, account_id: &str, state: &IdentityAccountState) -> anyhow::Result<()> {
134        let path = self.account_path(account_id);
135        if let Some(parent) = path.parent() {
136            tokio::fs::create_dir_all(parent).await?;
137        }
138        let bytes = serde_json::to_vec_pretty(state)?;
139        tokio::fs::write(&path, bytes).await?;
140        Ok(())
141    }
142}
143
144#[async_trait]
145impl IdentityStore for FileIdentityStore {
146    async fn list_identities(
147        &self,
148        account_id: &str,
149        username: &str,
150    ) -> anyhow::Result<Vec<Identity>> {
151        let state = self.load(account_id, username).await?;
152        Ok(state.identities.into_values().collect())
153    }
154
155    async fn get_identity(
156        &self,
157        account_id: &str,
158        username: &str,
159        id: &str,
160    ) -> anyhow::Result<Option<Identity>> {
161        let state = self.load(account_id, username).await?;
162        Ok(state.identities.get(id).cloned())
163    }
164
165    async fn create_identity(
166        &self,
167        account_id: &str,
168        username: &str,
169        identity: Identity,
170    ) -> anyhow::Result<Identity> {
171        let mut state = self.load(account_id, username).await?;
172        state
173            .identities
174            .insert(identity.id.clone(), identity.clone());
175        state.state_version += 1;
176        self.save(account_id, &state).await?;
177        Ok(identity)
178    }
179
180    async fn update_identity(
181        &self,
182        account_id: &str,
183        username: &str,
184        id: &str,
185        patch: &serde_json::Value,
186    ) -> anyhow::Result<Identity> {
187        let mut state = self.load(account_id, username).await?;
188        let existing = state
189            .identities
190            .get(id)
191            .cloned()
192            .ok_or_else(|| anyhow::anyhow!("Identity '{}' not found", id))?;
193
194        // Serialize current identity to a mutable JSON value, apply the JMAP
195        // patch (top-level "/fieldName" paths), then deserialize back.
196        let mut current_json = serde_json::to_value(&existing)?;
197        if let (Some(obj), Some(patch_obj)) = (current_json.as_object_mut(), patch.as_object()) {
198            for (path_key, value) in patch_obj {
199                // JMAP patch keys are "/fieldName"; strip leading '/'
200                let field = path_key.trim_start_matches('/');
201                obj.insert(field.to_string(), value.clone());
202            }
203        }
204        let mut updated: Identity = serde_json::from_value(current_json)?;
205
206        // Preserve immutable fields
207        updated.id = existing.id.clone();
208        if id == "default" {
209            updated.may_delete = false;
210        }
211
212        state.identities.insert(id.to_string(), updated.clone());
213        state.state_version += 1;
214        self.save(account_id, &state).await?;
215        Ok(updated)
216    }
217
218    async fn delete_identity(
219        &self,
220        account_id: &str,
221        username: &str,
222        id: &str,
223    ) -> anyhow::Result<()> {
224        let mut state = self.load(account_id, username).await?;
225        if state.identities.remove(id).is_none() {
226            return Err(anyhow::anyhow!("Identity '{}' not found", id));
227        }
228        state.state_version += 1;
229        self.save(account_id, &state).await?;
230        Ok(())
231    }
232
233    async fn state_token(&self, account_id: &str, username: &str) -> anyhow::Result<String> {
234        let state = self.load(account_id, username).await?;
235        Ok(state.state_version.to_string())
236    }
237}
238
239// ─── JMAP types ──────────────────────────────────────────────────────────────
240
241/// Identity object
242#[derive(Debug, Clone, Serialize, Deserialize)]
243#[serde(rename_all = "camelCase")]
244pub struct Identity {
245    /// Unique identifier
246    pub id: String,
247    /// Display name
248    pub name: String,
249    /// Email address
250    pub email: String,
251    /// Reply-to address
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub reply_to: Option<Vec<crate::types::EmailAddress>>,
254    /// Bcc address (auto-bcc on sends)
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub bcc: Option<Vec<crate::types::EmailAddress>>,
257    /// Text signature
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub text_signature: Option<String>,
260    /// HTML signature
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub html_signature: Option<String>,
263    /// May delete
264    pub may_delete: bool,
265}
266
267/// Identity/get request
268#[derive(Debug, Clone, Deserialize)]
269#[serde(rename_all = "camelCase")]
270pub struct IdentityGetRequest {
271    pub account_id: String,
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub ids: Option<Vec<String>>,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub properties: Option<Vec<String>>,
276}
277
278/// Identity/get response
279#[derive(Debug, Clone, Serialize)]
280#[serde(rename_all = "camelCase")]
281pub struct IdentityGetResponse {
282    pub account_id: String,
283    pub state: String,
284    pub list: Vec<Identity>,
285    pub not_found: Vec<String>,
286}
287
288/// Identity/set request
289#[derive(Debug, Clone, Deserialize)]
290#[serde(rename_all = "camelCase")]
291pub struct IdentitySetRequest {
292    pub account_id: String,
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub if_in_state: Option<String>,
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub create: Option<HashMap<String, IdentityObject>>,
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub update: Option<HashMap<String, serde_json::Value>>,
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub destroy: Option<Vec<String>>,
301}
302
303/// Identity object for creation
304#[derive(Debug, Clone, Deserialize)]
305#[serde(rename_all = "camelCase")]
306pub struct IdentityObject {
307    pub name: String,
308    pub email: String,
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub reply_to: Option<Vec<crate::types::EmailAddress>>,
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub bcc: Option<Vec<crate::types::EmailAddress>>,
313    #[serde(skip_serializing_if = "Option::is_none")]
314    pub text_signature: Option<String>,
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub html_signature: Option<String>,
317}
318
319/// Identity/set response
320#[derive(Debug, Clone, Serialize)]
321#[serde(rename_all = "camelCase")]
322pub struct IdentitySetResponse {
323    pub account_id: String,
324    pub old_state: String,
325    pub new_state: String,
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub created: Option<HashMap<String, Identity>>,
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub updated: Option<HashMap<String, Option<Identity>>>,
330    #[serde(skip_serializing_if = "Option::is_none")]
331    pub destroyed: Option<Vec<String>>,
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub not_created: Option<HashMap<String, JmapSetError>>,
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub not_updated: Option<HashMap<String, JmapSetError>>,
336    #[serde(skip_serializing_if = "Option::is_none")]
337    pub not_destroyed: Option<HashMap<String, JmapSetError>>,
338}
339
340/// Identity/changes request
341#[derive(Debug, Clone, Deserialize)]
342#[serde(rename_all = "camelCase")]
343pub struct IdentityChangesRequest {
344    pub account_id: String,
345    pub since_state: String,
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub max_changes: Option<u64>,
348}
349
350/// Identity/changes response
351#[derive(Debug, Clone, Serialize)]
352#[serde(rename_all = "camelCase")]
353pub struct IdentityChangesResponse {
354    pub account_id: String,
355    pub old_state: String,
356    pub new_state: String,
357    pub has_more_changes: bool,
358    pub created: Vec<String>,
359    pub updated: Vec<String>,
360    pub destroyed: Vec<String>,
361}
362
363// ─── Helpers ─────────────────────────────────────────────────────────────────
364
365/// Minimal email format validation: must contain exactly one '@' with non-empty
366/// local-part and domain.
367fn is_valid_email(email: &str) -> bool {
368    let at_count = email.chars().filter(|&c| c == '@').count();
369    if at_count != 1 {
370        return false;
371    }
372    let mut parts = email.splitn(2, '@');
373    let local = parts.next().unwrap_or("");
374    let domain = parts.next().unwrap_or("");
375    !local.is_empty() && !domain.is_empty()
376}
377
378// ─── Method handlers ─────────────────────────────────────────────────────────
379
380/// Handle Identity/get method
381pub async fn identity_get(
382    request: IdentityGetRequest,
383    _message_store: &dyn MessageStore,
384    identity_store: &dyn IdentityStore,
385    principal: &Principal,
386) -> anyhow::Result<IdentityGetResponse> {
387    ensure_account_ownership(&request.account_id, principal)?;
388
389    let state = identity_store
390        .state_token(&request.account_id, &principal.username)
391        .await?;
392
393    let mut list = Vec::new();
394    let mut not_found = Vec::new();
395
396    match request.ids {
397        None => {
398            let all = identity_store
399                .list_identities(&request.account_id, &principal.username)
400                .await?;
401            list.extend(all);
402        }
403        Some(ids) => {
404            for id in ids {
405                match identity_store
406                    .get_identity(&request.account_id, &principal.username, &id)
407                    .await?
408                {
409                    Some(identity) => list.push(identity),
410                    None => not_found.push(id),
411                }
412            }
413        }
414    }
415
416    Ok(IdentityGetResponse {
417        account_id: request.account_id,
418        state,
419        list,
420        not_found,
421    })
422}
423
424/// Handle Identity/set method
425pub async fn identity_set(
426    request: IdentitySetRequest,
427    _message_store: &dyn MessageStore,
428    identity_store: &dyn IdentityStore,
429    principal: &Principal,
430) -> anyhow::Result<IdentitySetResponse> {
431    ensure_account_ownership(&request.account_id, principal)?;
432
433    let old_state = identity_store
434        .state_token(&request.account_id, &principal.username)
435        .await?;
436
437    // Per RFC 8620 §5.3, if_in_state mismatch aborts the entire call
438    if let Some(ref expected) = request.if_in_state {
439        if *expected != old_state {
440            return Err(anyhow::anyhow!(
441                "stateMismatch: expected state '{}', current state is '{}'",
442                expected,
443                old_state
444            ));
445        }
446    }
447
448    let mut created: HashMap<String, Identity> = HashMap::new();
449    let mut updated: HashMap<String, Option<Identity>> = HashMap::new();
450    let mut destroyed: Vec<String> = Vec::new();
451    let mut not_created: HashMap<String, JmapSetError> = HashMap::new();
452    let mut not_updated: HashMap<String, JmapSetError> = HashMap::new();
453    let mut not_destroyed: HashMap<String, JmapSetError> = HashMap::new();
454
455    // ── Creates ──────────────────────────────────────────────────────────────
456    if let Some(create_map) = request.create {
457        for (creation_id, identity_obj) in create_map {
458            if !is_valid_email(&identity_obj.email) {
459                not_created.insert(
460                    creation_id,
461                    JmapSetError {
462                        error_type: "invalidProperties".to_string(),
463                        description: Some(format!(
464                            "Invalid email address: '{}'",
465                            identity_obj.email
466                        )),
467                    },
468                );
469                continue;
470            }
471            let new_id = uuid::Uuid::new_v4().to_string();
472            let new_identity = Identity {
473                id: new_id,
474                name: identity_obj.name,
475                email: identity_obj.email,
476                reply_to: identity_obj.reply_to,
477                bcc: identity_obj.bcc,
478                text_signature: identity_obj.text_signature,
479                html_signature: identity_obj.html_signature,
480                may_delete: true,
481            };
482            match identity_store
483                .create_identity(&request.account_id, &principal.username, new_identity)
484                .await
485            {
486                Ok(stored) => {
487                    created.insert(creation_id, stored);
488                }
489                Err(e) => {
490                    not_created.insert(
491                        creation_id,
492                        JmapSetError {
493                            error_type: "serverFail".to_string(),
494                            description: Some(format!("Failed to create identity: {}", e)),
495                        },
496                    );
497                }
498            }
499        }
500    }
501
502    // ── Updates ──────────────────────────────────────────────────────────────
503    if let Some(update_map) = request.update {
504        for (id, patch) in update_map {
505            match identity_store
506                .update_identity(&request.account_id, &principal.username, &id, &patch)
507                .await
508            {
509                Ok(stored) => {
510                    updated.insert(id, Some(stored));
511                }
512                Err(e) => {
513                    let err_msg = e.to_string();
514                    let error_type = if err_msg.contains("not found") {
515                        "notFound"
516                    } else {
517                        "serverFail"
518                    };
519                    not_updated.insert(
520                        id,
521                        JmapSetError {
522                            error_type: error_type.to_string(),
523                            description: Some(err_msg),
524                        },
525                    );
526                }
527            }
528        }
529    }
530
531    // ── Destroys ─────────────────────────────────────────────────────────────
532    if let Some(destroy_ids) = request.destroy {
533        for id in destroy_ids {
534            if id == "default" {
535                not_destroyed.insert(
536                    id,
537                    JmapSetError {
538                        error_type: "forbidden".to_string(),
539                        description: Some("Cannot delete default identity".to_string()),
540                    },
541                );
542                continue;
543            }
544            match identity_store
545                .delete_identity(&request.account_id, &principal.username, &id)
546                .await
547            {
548                Ok(()) => destroyed.push(id),
549                Err(e) => {
550                    let err_msg = e.to_string();
551                    let error_type = if err_msg.contains("not found") {
552                        "notFound"
553                    } else {
554                        "serverFail"
555                    };
556                    not_destroyed.insert(
557                        id,
558                        JmapSetError {
559                            error_type: error_type.to_string(),
560                            description: Some(err_msg),
561                        },
562                    );
563                }
564            }
565        }
566    }
567
568    let new_state = identity_store
569        .state_token(&request.account_id, &principal.username)
570        .await?;
571
572    Ok(IdentitySetResponse {
573        account_id: request.account_id,
574        old_state,
575        new_state,
576        created: if created.is_empty() {
577            None
578        } else {
579            Some(created)
580        },
581        updated: if updated.is_empty() {
582            None
583        } else {
584            Some(updated)
585        },
586        destroyed: if destroyed.is_empty() {
587            None
588        } else {
589            Some(destroyed)
590        },
591        not_created: if not_created.is_empty() {
592            None
593        } else {
594            Some(not_created)
595        },
596        not_updated: if not_updated.is_empty() {
597            None
598        } else {
599            Some(not_updated)
600        },
601        not_destroyed: if not_destroyed.is_empty() {
602            None
603        } else {
604            Some(not_destroyed)
605        },
606    })
607}
608
609/// Handle Identity/changes method
610pub async fn identity_changes(
611    request: IdentityChangesRequest,
612    _message_store: &dyn MessageStore,
613    identity_store: &dyn IdentityStore,
614    principal: &Principal,
615) -> anyhow::Result<IdentityChangesResponse> {
616    ensure_account_ownership(&request.account_id, principal)?;
617
618    let new_state = identity_store
619        .state_token(&request.account_id, &principal.username)
620        .await?;
621    let old_state = request.since_state;
622
623    // Slice A: we report the current state token but do not track per-object
624    // change history. Callers should re-fetch all identities when the state
625    // token differs from what they last saw.
626    Ok(IdentityChangesResponse {
627        account_id: request.account_id,
628        old_state,
629        new_state,
630        has_more_changes: false,
631        created: Vec::new(),
632        updated: Vec::new(),
633        destroyed: Vec::new(),
634    })
635}
636
637#[cfg(test)]
638mod tests {
639    use super::*;
640    use rusmes_storage::backends::filesystem::FilesystemBackend;
641    use rusmes_storage::StorageBackend;
642    use std::path::PathBuf;
643
644    fn test_principal() -> crate::types::Principal {
645        crate::types::Principal {
646            username: "alice@example.com".to_string(),
647            account_id: "acc1".to_string(),
648            scopes: vec![crate::types::SCOPE_ADMIN.to_string()],
649        }
650    }
651
652    fn create_test_store() -> std::sync::Arc<dyn MessageStore> {
653        let backend = FilesystemBackend::new(PathBuf::from("/tmp/rusmes-test-storage"));
654        backend.message_store()
655    }
656
657    fn create_identity_store(sub: &str) -> FileIdentityStore {
658        let mut dir = std::env::temp_dir();
659        dir.push(format!("rusmes-identity-test-{}", sub));
660        FileIdentityStore::new(dir)
661    }
662
663    // ── Required new tests ────────────────────────────────────────────────────
664
665    #[tokio::test]
666    async fn test_identity_create_and_get() {
667        let msg_store = create_test_store();
668        let id_store = create_identity_store("create_and_get");
669        let principal = test_principal();
670
671        let mut create_map = HashMap::new();
672        create_map.insert(
673            "c1".to_string(),
674            IdentityObject {
675                name: "Alice".to_string(),
676                email: "alice@example.com".to_string(),
677                reply_to: None,
678                bcc: None,
679                text_signature: None,
680                html_signature: None,
681            },
682        );
683        let set_resp = identity_set(
684            IdentitySetRequest {
685                account_id: "acc1".to_string(),
686                if_in_state: None,
687                create: Some(create_map),
688                update: None,
689                destroy: None,
690            },
691            msg_store.as_ref(),
692            &id_store,
693            &principal,
694        )
695        .await
696        .unwrap();
697
698        assert!(set_resp.not_created.is_none(), "create should succeed");
699        let created = set_resp.created.unwrap();
700        assert_eq!(created.len(), 1);
701        let stored = created.get("c1").unwrap();
702        assert_eq!(stored.name, "Alice");
703        assert_eq!(stored.email, "alice@example.com");
704        assert!(stored.may_delete);
705
706        // Fetch it back by id
707        let get_resp = identity_get(
708            IdentityGetRequest {
709                account_id: "acc1".to_string(),
710                ids: Some(vec![stored.id.clone()]),
711                properties: None,
712            },
713            msg_store.as_ref(),
714            &id_store,
715            &principal,
716        )
717        .await
718        .unwrap();
719        assert_eq!(get_resp.list.len(), 1);
720        assert_eq!(get_resp.list[0].email, "alice@example.com");
721    }
722
723    #[tokio::test]
724    async fn test_identity_update_name() {
725        let msg_store = create_test_store();
726        let id_store = create_identity_store("update_name");
727        let principal = test_principal();
728
729        // Create first
730        let mut create_map = HashMap::new();
731        create_map.insert(
732            "c1".to_string(),
733            IdentityObject {
734                name: "Original".to_string(),
735                email: "orig@example.com".to_string(),
736                reply_to: None,
737                bcc: None,
738                text_signature: None,
739                html_signature: None,
740            },
741        );
742        let set_resp = identity_set(
743            IdentitySetRequest {
744                account_id: "acc1".to_string(),
745                if_in_state: None,
746                create: Some(create_map),
747                update: None,
748                destroy: None,
749            },
750            msg_store.as_ref(),
751            &id_store,
752            &principal,
753        )
754        .await
755        .unwrap();
756        let new_id = set_resp.created.unwrap().get("c1").unwrap().id.clone();
757
758        // Update the name
759        let mut update_map = HashMap::new();
760        update_map.insert(new_id.clone(), serde_json::json!({"/name": "Updated Name"}));
761        let upd_resp = identity_set(
762            IdentitySetRequest {
763                account_id: "acc1".to_string(),
764                if_in_state: None,
765                create: None,
766                update: Some(update_map),
767                destroy: None,
768            },
769            msg_store.as_ref(),
770            &id_store,
771            &principal,
772        )
773        .await
774        .unwrap();
775
776        assert!(upd_resp.not_updated.is_none(), "update should succeed");
777        let upd = upd_resp.updated.unwrap();
778        let id_obj = upd.get(&new_id).unwrap().as_ref().unwrap();
779        assert_eq!(id_obj.name, "Updated Name");
780    }
781
782    #[tokio::test]
783    async fn test_identity_destroy_custom() {
784        let msg_store = create_test_store();
785        let id_store = create_identity_store("destroy_custom");
786        let principal = test_principal();
787
788        // Create an identity to destroy
789        let mut create_map = HashMap::new();
790        create_map.insert(
791            "c1".to_string(),
792            IdentityObject {
793                name: "To Be Deleted".to_string(),
794                email: "delete@example.com".to_string(),
795                reply_to: None,
796                bcc: None,
797                text_signature: None,
798                html_signature: None,
799            },
800        );
801        let set_resp = identity_set(
802            IdentitySetRequest {
803                account_id: "acc1".to_string(),
804                if_in_state: None,
805                create: Some(create_map),
806                update: None,
807                destroy: None,
808            },
809            msg_store.as_ref(),
810            &id_store,
811            &principal,
812        )
813        .await
814        .unwrap();
815        let new_id = set_resp.created.unwrap().get("c1").unwrap().id.clone();
816
817        // Destroy it
818        let del_resp = identity_set(
819            IdentitySetRequest {
820                account_id: "acc1".to_string(),
821                if_in_state: None,
822                create: None,
823                update: None,
824                destroy: Some(vec![new_id.clone()]),
825            },
826            msg_store.as_ref(),
827            &id_store,
828            &principal,
829        )
830        .await
831        .unwrap();
832
833        assert!(del_resp.not_destroyed.is_none(), "destroy should succeed");
834        let destroyed = del_resp.destroyed.unwrap();
835        assert!(destroyed.contains(&new_id));
836
837        // Verify gone
838        let get_resp = identity_get(
839            IdentityGetRequest {
840                account_id: "acc1".to_string(),
841                ids: Some(vec![new_id.clone()]),
842                properties: None,
843            },
844            msg_store.as_ref(),
845            &id_store,
846            &principal,
847        )
848        .await
849        .unwrap();
850        assert_eq!(get_resp.not_found, vec![new_id]);
851    }
852
853    #[tokio::test]
854    async fn test_identity_destroy_default_rejected() {
855        let msg_store = create_test_store();
856        let id_store = create_identity_store("destroy_default_rejected");
857        let principal = test_principal();
858
859        let resp = identity_set(
860            IdentitySetRequest {
861                account_id: "acc1".to_string(),
862                if_in_state: None,
863                create: None,
864                update: None,
865                destroy: Some(vec!["default".to_string()]),
866            },
867            msg_store.as_ref(),
868            &id_store,
869            &principal,
870        )
871        .await
872        .unwrap();
873
874        assert!(resp.not_destroyed.is_some());
875        let errors = resp.not_destroyed.unwrap();
876        assert_eq!(errors.get("default").unwrap().error_type, "forbidden");
877    }
878
879    #[tokio::test]
880    async fn test_identity_state_mismatch() {
881        let msg_store = create_test_store();
882        let id_store = create_identity_store("state_mismatch");
883        let principal = test_principal();
884
885        // Trigger a create to advance state past "1"
886        let mut create_map = HashMap::new();
887        create_map.insert(
888            "c1".to_string(),
889            IdentityObject {
890                name: "Trigger".to_string(),
891                email: "trigger@example.com".to_string(),
892                reply_to: None,
893                bcc: None,
894                text_signature: None,
895                html_signature: None,
896            },
897        );
898        identity_set(
899            IdentitySetRequest {
900                account_id: "acc1".to_string(),
901                if_in_state: None,
902                create: Some(create_map),
903                update: None,
904                destroy: None,
905            },
906            msg_store.as_ref(),
907            &id_store,
908            &principal,
909        )
910        .await
911        .unwrap();
912
913        // Submit with wrong state
914        let result = identity_set(
915            IdentitySetRequest {
916                account_id: "acc1".to_string(),
917                if_in_state: Some("999".to_string()),
918                create: None,
919                update: None,
920                destroy: None,
921            },
922            msg_store.as_ref(),
923            &id_store,
924            &principal,
925        )
926        .await;
927
928        assert!(result.is_err(), "wrong state should return Err");
929        let err_msg = result.unwrap_err().to_string();
930        assert!(
931            err_msg.contains("stateMismatch"),
932            "error should mention stateMismatch: {}",
933            err_msg
934        );
935    }
936
937    #[tokio::test]
938    async fn test_identity_full_roundtrip() {
939        let msg_store = create_test_store();
940        let id_store = create_identity_store("full_roundtrip");
941        let principal = test_principal();
942
943        // 1. Initial get — only default
944        let get1 = identity_get(
945            IdentityGetRequest {
946                account_id: "acc1".to_string(),
947                ids: None,
948                properties: None,
949            },
950            msg_store.as_ref(),
951            &id_store,
952            &principal,
953        )
954        .await
955        .unwrap();
956        assert_eq!(get1.list.len(), 1);
957        assert_eq!(get1.list[0].id, "default");
958        assert!(!get1.list[0].may_delete);
959        let state_after_default = get1.state.clone();
960
961        // 2. Create a new identity
962        let mut create_map = HashMap::new();
963        create_map.insert(
964            "newone".to_string(),
965            IdentityObject {
966                name: "Work".to_string(),
967                email: "work@company.com".to_string(),
968                reply_to: None,
969                bcc: None,
970                text_signature: Some("Regards,\nAlice".to_string()),
971                html_signature: None,
972            },
973        );
974        let set1 = identity_set(
975            IdentitySetRequest {
976                account_id: "acc1".to_string(),
977                if_in_state: Some(state_after_default.clone()),
978                create: Some(create_map),
979                update: None,
980                destroy: None,
981            },
982            msg_store.as_ref(),
983            &id_store,
984            &principal,
985        )
986        .await
987        .unwrap();
988        assert!(set1.not_created.is_none());
989        let work_id = set1.created.unwrap().get("newone").unwrap().id.clone();
990        let state_after_create = set1.new_state.clone();
991        assert_ne!(state_after_default, state_after_create);
992
993        // 3. Update it
994        let mut upd_map = HashMap::new();
995        upd_map.insert(
996            work_id.clone(),
997            serde_json::json!({"/name": "Work (updated)"}),
998        );
999        let set2 = identity_set(
1000            IdentitySetRequest {
1001                account_id: "acc1".to_string(),
1002                if_in_state: Some(state_after_create.clone()),
1003                create: None,
1004                update: Some(upd_map),
1005                destroy: None,
1006            },
1007            msg_store.as_ref(),
1008            &id_store,
1009            &principal,
1010        )
1011        .await
1012        .unwrap();
1013        assert!(set2.not_updated.is_none());
1014        let upd_identity = set2
1015            .updated
1016            .unwrap()
1017            .get(&work_id)
1018            .unwrap()
1019            .as_ref()
1020            .unwrap()
1021            .clone();
1022        assert_eq!(upd_identity.name, "Work (updated)");
1023
1024        // 4. Destroy it
1025        let set3 = identity_set(
1026            IdentitySetRequest {
1027                account_id: "acc1".to_string(),
1028                if_in_state: None,
1029                create: None,
1030                update: None,
1031                destroy: Some(vec![work_id.clone()]),
1032            },
1033            msg_store.as_ref(),
1034            &id_store,
1035            &principal,
1036        )
1037        .await
1038        .unwrap();
1039        assert!(set3.not_destroyed.is_none());
1040        assert_eq!(set3.destroyed.unwrap(), vec![work_id.clone()]);
1041
1042        // 5. Verify only default remains
1043        let get_final = identity_get(
1044            IdentityGetRequest {
1045                account_id: "acc1".to_string(),
1046                ids: None,
1047                properties: None,
1048            },
1049            msg_store.as_ref(),
1050            &id_store,
1051            &principal,
1052        )
1053        .await
1054        .unwrap();
1055        assert_eq!(get_final.list.len(), 1);
1056        assert_eq!(get_final.list[0].id, "default");
1057    }
1058
1059    // ── Retained legacy tests (updated for new signatures) ───────────────────
1060
1061    #[tokio::test]
1062    async fn test_identity_get() {
1063        let msg_store = create_test_store();
1064        let id_store = create_identity_store("get_default");
1065        let principal = test_principal();
1066        let request = IdentityGetRequest {
1067            account_id: "acc1".to_string(),
1068            ids: Some(vec!["default".to_string()]),
1069            properties: None,
1070        };
1071
1072        let response = identity_get(request, msg_store.as_ref(), &id_store, &principal)
1073            .await
1074            .unwrap();
1075        assert_eq!(response.list.len(), 1);
1076        assert_eq!(response.list[0].id, "default");
1077    }
1078
1079    #[tokio::test]
1080    async fn test_identity_set_create() {
1081        let msg_store = create_test_store();
1082        let id_store = create_identity_store("set_create");
1083        let principal = test_principal();
1084
1085        let mut create_map = HashMap::new();
1086        create_map.insert(
1087            "new1".to_string(),
1088            IdentityObject {
1089                name: "John Doe".to_string(),
1090                email: "john@example.com".to_string(),
1091                reply_to: None,
1092                bcc: None,
1093                text_signature: Some("Best regards,\nJohn".to_string()),
1094                html_signature: None,
1095            },
1096        );
1097
1098        let request = IdentitySetRequest {
1099            account_id: "acc1".to_string(),
1100            if_in_state: None,
1101            create: Some(create_map),
1102            update: None,
1103            destroy: None,
1104        };
1105
1106        let response = identity_set(request, msg_store.as_ref(), &id_store, &principal)
1107            .await
1108            .unwrap();
1109        assert!(response.created.is_some());
1110        assert_eq!(response.created.as_ref().unwrap().len(), 1);
1111    }
1112
1113    #[tokio::test]
1114    async fn test_identity_changes() {
1115        let msg_store = create_test_store();
1116        let id_store = create_identity_store("changes");
1117        let principal = test_principal();
1118        let request = IdentityChangesRequest {
1119            account_id: "acc1".to_string(),
1120            since_state: "1".to_string(),
1121            max_changes: Some(50),
1122        };
1123
1124        let response = identity_changes(request, msg_store.as_ref(), &id_store, &principal)
1125            .await
1126            .unwrap();
1127        assert_eq!(response.old_state, "1");
1128        assert!(!response.new_state.is_empty());
1129    }
1130
1131    #[tokio::test]
1132    async fn test_identity_set_destroy_default() {
1133        let msg_store = create_test_store();
1134        let id_store = create_identity_store("destroy_default");
1135        let principal = test_principal();
1136        let request = IdentitySetRequest {
1137            account_id: "acc1".to_string(),
1138            if_in_state: None,
1139            create: None,
1140            update: None,
1141            destroy: Some(vec!["default".to_string()]),
1142        };
1143
1144        let response = identity_set(request, msg_store.as_ref(), &id_store, &principal)
1145            .await
1146            .unwrap();
1147        assert!(response.not_destroyed.is_some());
1148        let errors = response.not_destroyed.unwrap();
1149        assert_eq!(errors.get("default").unwrap().error_type, "forbidden");
1150    }
1151
1152    #[tokio::test]
1153    async fn test_identity_with_signature() {
1154        let msg_store = create_test_store();
1155        let id_store = create_identity_store("with_signature");
1156        let principal = test_principal();
1157
1158        let mut create_map = HashMap::new();
1159        create_map.insert(
1160            "sig1".to_string(),
1161            IdentityObject {
1162                name: "Test User".to_string(),
1163                email: "test@example.com".to_string(),
1164                reply_to: None,
1165                bcc: None,
1166                text_signature: Some("--\nBest regards".to_string()),
1167                html_signature: Some("<p>Best regards</p>".to_string()),
1168            },
1169        );
1170
1171        let request = IdentitySetRequest {
1172            account_id: "acc1".to_string(),
1173            if_in_state: None,
1174            create: Some(create_map),
1175            update: None,
1176            destroy: None,
1177        };
1178
1179        let response = identity_set(request, msg_store.as_ref(), &id_store, &principal)
1180            .await
1181            .unwrap();
1182        assert!(response.created.is_some());
1183        let stored = response.created.as_ref().unwrap().get("sig1").unwrap();
1184        assert_eq!(stored.text_signature.as_deref(), Some("--\nBest regards"));
1185    }
1186
1187    #[tokio::test]
1188    async fn test_identity_with_bcc() {
1189        let msg_store = create_test_store();
1190        let id_store = create_identity_store("with_bcc");
1191        let principal = test_principal();
1192
1193        let mut create_map = HashMap::new();
1194        let bcc = vec![crate::types::EmailAddress::new(
1195            "archive@example.com".to_string(),
1196        )];
1197
1198        create_map.insert(
1199            "bcc1".to_string(),
1200            IdentityObject {
1201                name: "Test User".to_string(),
1202                email: "test@example.com".to_string(),
1203                reply_to: None,
1204                bcc: Some(bcc),
1205                text_signature: None,
1206                html_signature: None,
1207            },
1208        );
1209
1210        let request = IdentitySetRequest {
1211            account_id: "acc1".to_string(),
1212            if_in_state: None,
1213            create: Some(create_map),
1214            update: None,
1215            destroy: None,
1216        };
1217
1218        let response = identity_set(request, msg_store.as_ref(), &id_store, &principal)
1219            .await
1220            .unwrap();
1221        assert!(response.created.is_some());
1222        let stored = response.created.as_ref().unwrap().get("bcc1").unwrap();
1223        assert!(stored.bcc.is_some());
1224    }
1225
1226    #[tokio::test]
1227    async fn test_identity_get_not_found() {
1228        let msg_store = create_test_store();
1229        let id_store = create_identity_store("get_not_found");
1230        let principal = test_principal();
1231        let request = IdentityGetRequest {
1232            account_id: "acc1".to_string(),
1233            ids: Some(vec!["nonexistent".to_string()]),
1234            properties: None,
1235        };
1236
1237        let response = identity_get(request, msg_store.as_ref(), &id_store, &principal)
1238            .await
1239            .unwrap();
1240        assert_eq!(response.not_found.len(), 1);
1241    }
1242
1243    #[tokio::test]
1244    async fn test_identity_get_all() {
1245        let msg_store = create_test_store();
1246        let id_store = create_identity_store("get_all");
1247        let principal = test_principal();
1248        let request = IdentityGetRequest {
1249            account_id: "acc1".to_string(),
1250            ids: None,
1251            properties: None,
1252        };
1253
1254        let response = identity_get(request, msg_store.as_ref(), &id_store, &principal)
1255            .await
1256            .unwrap();
1257        assert!(!response.list.is_empty());
1258    }
1259
1260    #[tokio::test]
1261    async fn test_identity_set_update() {
1262        let msg_store = create_test_store();
1263        let id_store = create_identity_store("set_update_default");
1264        let principal = test_principal();
1265
1266        let mut update_map = HashMap::new();
1267        update_map.insert(
1268            "default".to_string(),
1269            serde_json::json!({"/name": "New Name"}),
1270        );
1271
1272        let request = IdentitySetRequest {
1273            account_id: "acc1".to_string(),
1274            if_in_state: None,
1275            create: None,
1276            update: Some(update_map),
1277            destroy: None,
1278        };
1279
1280        let response = identity_set(request, msg_store.as_ref(), &id_store, &principal)
1281            .await
1282            .unwrap();
1283        // Default can be updated (only destroy is forbidden)
1284        assert!(response.not_updated.is_none());
1285        let upd = response.updated.unwrap();
1286        let id_obj = upd.get("default").unwrap().as_ref().unwrap();
1287        assert_eq!(id_obj.name, "New Name");
1288    }
1289
1290    #[tokio::test]
1291    async fn test_identity_changes_state_progression() {
1292        let msg_store = create_test_store();
1293        let id_store = create_identity_store("changes_state_progression");
1294        let principal = test_principal();
1295
1296        // Create one identity to advance the version
1297        let mut create_map = HashMap::new();
1298        create_map.insert(
1299            "c1".to_string(),
1300            IdentityObject {
1301                name: "Test".to_string(),
1302                email: "test@example.com".to_string(),
1303                reply_to: None,
1304                bcc: None,
1305                text_signature: None,
1306                html_signature: None,
1307            },
1308        );
1309        identity_set(
1310            IdentitySetRequest {
1311                account_id: "acc1".to_string(),
1312                if_in_state: None,
1313                create: Some(create_map),
1314                update: None,
1315                destroy: None,
1316            },
1317            msg_store.as_ref(),
1318            &id_store,
1319            &principal,
1320        )
1321        .await
1322        .unwrap();
1323
1324        let request1 = IdentityChangesRequest {
1325            account_id: "acc1".to_string(),
1326            since_state: "1".to_string(),
1327            max_changes: None,
1328        };
1329        let response1 = identity_changes(request1, msg_store.as_ref(), &id_store, &principal)
1330            .await
1331            .unwrap();
1332
1333        let new_state_num: u64 = response1.new_state.parse().unwrap();
1334        assert!(new_state_num > 1, "state should have advanced beyond 1");
1335
1336        // Calling changes again with the returned new_state yields same state
1337        let request2 = IdentityChangesRequest {
1338            account_id: "acc1".to_string(),
1339            since_state: response1.new_state.clone(),
1340            max_changes: None,
1341        };
1342        let response2 = identity_changes(request2, msg_store.as_ref(), &id_store, &principal)
1343            .await
1344            .unwrap();
1345        assert_eq!(response1.new_state, response2.new_state);
1346    }
1347
1348    #[tokio::test]
1349    async fn test_identity_default_may_not_delete() {
1350        let msg_store = create_test_store();
1351        let id_store = create_identity_store("default_may_not_delete");
1352        let principal = test_principal();
1353        let request = IdentityGetRequest {
1354            account_id: "acc1".to_string(),
1355            ids: Some(vec!["default".to_string()]),
1356            properties: None,
1357        };
1358
1359        let response = identity_get(request, msg_store.as_ref(), &id_store, &principal)
1360            .await
1361            .unwrap();
1362        assert!(!response.list[0].may_delete);
1363    }
1364
1365    #[tokio::test]
1366    async fn test_identity_with_reply_to() {
1367        let msg_store = create_test_store();
1368        let id_store = create_identity_store("with_reply_to");
1369        let principal = test_principal();
1370
1371        let mut create_map = HashMap::new();
1372        let reply_to = vec![crate::types::EmailAddress::new(
1373            "support@example.com".to_string(),
1374        )];
1375
1376        create_map.insert(
1377            "replyto1".to_string(),
1378            IdentityObject {
1379                name: "Support".to_string(),
1380                email: "noreply@example.com".to_string(),
1381                reply_to: Some(reply_to),
1382                bcc: None,
1383                text_signature: None,
1384                html_signature: None,
1385            },
1386        );
1387
1388        let request = IdentitySetRequest {
1389            account_id: "acc1".to_string(),
1390            if_in_state: None,
1391            create: Some(create_map),
1392            update: None,
1393            destroy: None,
1394        };
1395
1396        let response = identity_set(request, msg_store.as_ref(), &id_store, &principal)
1397            .await
1398            .unwrap();
1399        assert!(response.created.is_some());
1400        let stored = response.created.as_ref().unwrap().get("replyto1").unwrap();
1401        assert!(stored.reply_to.is_some());
1402    }
1403
1404    #[tokio::test]
1405    async fn test_identity_invalid_email_rejected() {
1406        let msg_store = create_test_store();
1407        let id_store = create_identity_store("invalid_email");
1408        let principal = test_principal();
1409
1410        let mut create_map = HashMap::new();
1411        create_map.insert(
1412            "bad1".to_string(),
1413            IdentityObject {
1414                name: "Bad".to_string(),
1415                email: "not-an-email".to_string(),
1416                reply_to: None,
1417                bcc: None,
1418                text_signature: None,
1419                html_signature: None,
1420            },
1421        );
1422
1423        let response = identity_set(
1424            IdentitySetRequest {
1425                account_id: "acc1".to_string(),
1426                if_in_state: None,
1427                create: Some(create_map),
1428                update: None,
1429                destroy: None,
1430            },
1431            msg_store.as_ref(),
1432            &id_store,
1433            &principal,
1434        )
1435        .await
1436        .unwrap();
1437        assert!(response.not_created.is_some());
1438        let err = response.not_created.unwrap();
1439        assert_eq!(err.get("bad1").unwrap().error_type, "invalidProperties");
1440    }
1441}