|
|
|
@ -23,7 +23,7 @@
|
|
|
|
|
#![deny(unused_must_use)]
|
|
|
|
|
|
|
|
|
|
use std::{
|
|
|
|
|
collections::{BTreeSet, HashMap, HashSet},
|
|
|
|
|
collections::{HashMap, HashSet},
|
|
|
|
|
convert::TryFrom,
|
|
|
|
|
pin::Pin,
|
|
|
|
|
str::FromStr,
|
|
|
|
@ -39,7 +39,7 @@ use futures::{
|
|
|
|
|
Stream,
|
|
|
|
|
};
|
|
|
|
|
use indexmap::{IndexMap, IndexSet};
|
|
|
|
|
use isahc::{config::RedirectPolicy, AsyncReadResponseExt, HttpClient};
|
|
|
|
|
use isahc::AsyncReadResponseExt;
|
|
|
|
|
use serde_json::{json, Value};
|
|
|
|
|
use smallvec::SmallVec;
|
|
|
|
|
use url::Url;
|
|
|
|
@ -89,13 +89,15 @@ pub mod session;
|
|
|
|
|
use session::*;
|
|
|
|
|
|
|
|
|
|
pub mod rfc8620;
|
|
|
|
|
use rfc8620::*;
|
|
|
|
|
|
|
|
|
|
pub mod objects;
|
|
|
|
|
use objects::*;
|
|
|
|
|
use rfc8620::{
|
|
|
|
|
argument::Argument, capabilities::JmapCoreCapability, download_request_format, filters::Filter,
|
|
|
|
|
upload_request_format, BlobObject, Get, GetResponse, Id, MethodResponse, Object, Query,
|
|
|
|
|
QueryResponse, Set, SetResponse, State, UploadResponse,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
pub mod mailbox;
|
|
|
|
|
use mailbox::*;
|
|
|
|
|
pub mod objects;
|
|
|
|
|
use mailbox::JmapMailbox;
|
|
|
|
|
|
|
|
|
|
pub fn deserialize_from_str<'de, T: serde::de::Deserialize<'de>>(s: &'de str) -> Result<T> {
|
|
|
|
|
let jd = &mut serde_json::Deserializer::from_str(s);
|
|
|
|
@ -256,13 +258,13 @@ pub struct Store {
|
|
|
|
|
pub main_identity: String,
|
|
|
|
|
pub extra_identities: Vec<String>,
|
|
|
|
|
pub byte_cache: Arc<FutureMutex<HashMap<EnvelopeHash, EnvelopeCache>>>,
|
|
|
|
|
pub id_store: Arc<FutureMutex<HashMap<EnvelopeHash, Id<EmailObject>>>>,
|
|
|
|
|
pub reverse_id_store: Arc<FutureMutex<HashMap<Id<EmailObject>, EnvelopeHash>>>,
|
|
|
|
|
pub id_store: Arc<FutureMutex<HashMap<EnvelopeHash, Id<objects::email::EmailObject>>>>,
|
|
|
|
|
pub reverse_id_store: Arc<FutureMutex<HashMap<Id<objects::email::EmailObject>, EnvelopeHash>>>,
|
|
|
|
|
pub blob_id_store: Arc<FutureMutex<HashMap<EnvelopeHash, Id<BlobObject>>>>,
|
|
|
|
|
pub collection: Collection,
|
|
|
|
|
pub mailboxes: Arc<RwLock<HashMap<MailboxHash, JmapMailbox>>>,
|
|
|
|
|
pub mailboxes_index: Arc<RwLock<HashMap<MailboxHash, HashSet<EnvelopeHash>>>>,
|
|
|
|
|
pub mailbox_state: Arc<FutureMutex<State<MailboxObject>>>,
|
|
|
|
|
pub mailbox_state: Arc<FutureMutex<State<objects::mailbox::MailboxObject>>>,
|
|
|
|
|
pub online_status: OnlineStatus,
|
|
|
|
|
pub is_subscribed: Arc<IsSubscribedFn>,
|
|
|
|
|
pub core_capabilities: Arc<Mutex<IndexMap<String, CapabilitiesObject>>>,
|
|
|
|
@ -270,7 +272,7 @@ pub struct Store {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Store {
|
|
|
|
|
pub async fn add_envelope(&self, obj: EmailObject) -> Envelope {
|
|
|
|
|
pub async fn add_envelope(&self, obj: objects::email::EmailObject) -> Envelope {
|
|
|
|
|
let mut flags = Flag::default();
|
|
|
|
|
let mut labels: IndexSet<TagHash> = IndexSet::new();
|
|
|
|
|
let id;
|
|
|
|
@ -331,7 +333,7 @@ impl Store {
|
|
|
|
|
|
|
|
|
|
pub async fn remove_envelope(
|
|
|
|
|
&self,
|
|
|
|
|
obj_id: Id<EmailObject>,
|
|
|
|
|
obj_id: Id<objects::email::EmailObject>,
|
|
|
|
|
) -> Option<(EnvelopeHash, SmallVec<[MailboxHash; 8]>)> {
|
|
|
|
|
let env_hash = self.reverse_id_store.lock().await.remove(&obj_id)?;
|
|
|
|
|
self.id_store.lock().await.remove(&env_hash);
|
|
|
|
@ -513,7 +515,7 @@ impl MailBackend for JmapType {
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let mailbox_id: Id<MailboxObject> = {
|
|
|
|
|
let mailbox_id: Id<objects::mailbox::MailboxObject> = {
|
|
|
|
|
let mailboxes_lck = store.mailboxes.read().unwrap();
|
|
|
|
|
if let Some(mailbox) = mailboxes_lck.get(&mailbox_hash) {
|
|
|
|
|
mailbox.id.clone()
|
|
|
|
@ -534,18 +536,17 @@ impl MailBackend for JmapType {
|
|
|
|
|
Ok(s) => s,
|
|
|
|
|
};
|
|
|
|
|
let mut req = Request::new(conn.request_no.clone());
|
|
|
|
|
let creation_id: Id<EmailObject> = "1".to_string().into();
|
|
|
|
|
|
|
|
|
|
let import_call: EmailImport =
|
|
|
|
|
EmailImport::new()
|
|
|
|
|
.account_id(mail_account_id)
|
|
|
|
|
.emails(indexmap! {
|
|
|
|
|
creation_id.clone() => EmailImportObject::new()
|
|
|
|
|
.blob_id(upload_response.blob_id)
|
|
|
|
|
.mailbox_ids(indexmap! {
|
|
|
|
|
mailbox_id => true
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
let creation_id: Id<objects::email::EmailObject> = "1".to_string().into();
|
|
|
|
|
|
|
|
|
|
let import_call: objects::email::EmailImport = objects::email::EmailImport::new()
|
|
|
|
|
.account_id(mail_account_id)
|
|
|
|
|
.emails(indexmap! {
|
|
|
|
|
creation_id.clone() => objects::email::EmailImportObject::new()
|
|
|
|
|
.blob_id(upload_response.blob_id)
|
|
|
|
|
.mailbox_ids(indexmap! {
|
|
|
|
|
mailbox_id => true
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
req.add_call(&import_call).await;
|
|
|
|
|
let mut res = conn.post_async(None, serde_json::to_string(&req)?).await?;
|
|
|
|
@ -558,14 +559,16 @@ impl MailBackend for JmapType {
|
|
|
|
|
}
|
|
|
|
|
Ok(s) => s,
|
|
|
|
|
};
|
|
|
|
|
let m = EmailImportResponse::try_from(v.method_responses.remove(0)).map_err(|err| {
|
|
|
|
|
let ierr: Result<EmailImportError> = deserialize_from_str(&res_text);
|
|
|
|
|
if let Ok(err) = ierr {
|
|
|
|
|
Error::new(format!("Could not save message: {:?}", err))
|
|
|
|
|
} else {
|
|
|
|
|
err
|
|
|
|
|
}
|
|
|
|
|
})?;
|
|
|
|
|
let m = objects::email::EmailImportResponse::try_from(v.method_responses.remove(0))
|
|
|
|
|
.map_err(|err| {
|
|
|
|
|
let ierr: Result<objects::email::EmailImportError> =
|
|
|
|
|
deserialize_from_str(&res_text);
|
|
|
|
|
if let Ok(err) = ierr {
|
|
|
|
|
Error::new(format!("Could not save message: {:?}", err))
|
|
|
|
|
} else {
|
|
|
|
|
err
|
|
|
|
|
}
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
if let Some(err) = m.not_created.and_then(|m| m.get(&creation_id).cloned()) {
|
|
|
|
|
return Err(Error::new(format!("Could not save message: {:?}", err)));
|
|
|
|
@ -599,21 +602,23 @@ impl MailBackend for JmapType {
|
|
|
|
|
.clone();
|
|
|
|
|
|
|
|
|
|
let mut f = Filter::Condition(
|
|
|
|
|
EmailFilterCondition::new()
|
|
|
|
|
objects::email::EmailFilterCondition::new()
|
|
|
|
|
.in_mailbox(Some(mailbox_id))
|
|
|
|
|
.into(),
|
|
|
|
|
);
|
|
|
|
|
f &= Filter::<EmailFilterCondition, EmailObject>::from(q);
|
|
|
|
|
f &= Filter::<objects::email::EmailFilterCondition, objects::email::EmailObject>::from(
|
|
|
|
|
q,
|
|
|
|
|
);
|
|
|
|
|
f
|
|
|
|
|
} else {
|
|
|
|
|
Filter::<EmailFilterCondition, EmailObject>::from(q)
|
|
|
|
|
Filter::<objects::email::EmailFilterCondition, objects::email::EmailObject>::from(q)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(Box::pin(async move {
|
|
|
|
|
let mut conn = connection.lock().await;
|
|
|
|
|
conn.connect().await?;
|
|
|
|
|
let mail_account_id = conn.session_guard().await?.mail_account_id();
|
|
|
|
|
let email_call: EmailQuery = EmailQuery::new(
|
|
|
|
|
let email_call = objects::email::EmailQuery::new(
|
|
|
|
|
Query::new()
|
|
|
|
|
.account_id(mail_account_id)
|
|
|
|
|
.filter(Some(filter))
|
|
|
|
@ -635,8 +640,10 @@ impl MailBackend for JmapType {
|
|
|
|
|
Ok(s) => s,
|
|
|
|
|
};
|
|
|
|
|
store.online_status.update_timestamp(None).await;
|
|
|
|
|
let m = QueryResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
|
|
|
|
|
let QueryResponse::<EmailObject> { ids, .. } = m;
|
|
|
|
|
let m = QueryResponse::<objects::email::EmailObject>::try_from(
|
|
|
|
|
v.method_responses.remove(0),
|
|
|
|
|
)?;
|
|
|
|
|
let QueryResponse::<objects::email::EmailObject> { ids, .. } = m;
|
|
|
|
|
let ret = ids.into_iter().map(|id| id.into_hash()).collect();
|
|
|
|
|
Ok(ret)
|
|
|
|
|
}))
|
|
|
|
@ -661,16 +668,16 @@ impl MailBackend for JmapType {
|
|
|
|
|
Ok(Box::pin(async move {
|
|
|
|
|
let mut conn = connection.lock().await;
|
|
|
|
|
let mail_account_id = conn.session_guard().await?.mail_account_id();
|
|
|
|
|
let mailbox_set_call: MailboxSet = MailboxSet::new(
|
|
|
|
|
Set::<MailboxObject>::new()
|
|
|
|
|
let mailbox_set_call = objects::mailbox::MailboxSet::new(
|
|
|
|
|
Set::<objects::mailbox::MailboxObject>::new()
|
|
|
|
|
.account_id(mail_account_id)
|
|
|
|
|
.create(Some({
|
|
|
|
|
let id: Id<MailboxObject> = path.as_str().into();
|
|
|
|
|
let id: Id<objects::mailbox::MailboxObject> = path.as_str().into();
|
|
|
|
|
indexmap! {
|
|
|
|
|
id.clone().into() => MailboxObject {
|
|
|
|
|
id.clone().into() => objects::mailbox::MailboxObject {
|
|
|
|
|
id,
|
|
|
|
|
name: path.clone(),
|
|
|
|
|
..MailboxObject::default()
|
|
|
|
|
..objects::mailbox::MailboxObject::default()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})),
|
|
|
|
@ -758,7 +765,8 @@ impl MailBackend for JmapType {
|
|
|
|
|
mailboxes_lck[&destination_mailbox_hash].id.clone(),
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
let mut update_map: IndexMap<Argument<Id<EmailObject>>, Value> = IndexMap::default();
|
|
|
|
|
let mut update_map: IndexMap<Argument<Id<objects::email::EmailObject>>, Value> =
|
|
|
|
|
IndexMap::default();
|
|
|
|
|
let mut update_keywords: IndexMap<String, Value> = IndexMap::default();
|
|
|
|
|
update_keywords.insert(
|
|
|
|
|
format!("mailboxIds/{}", &destination_mailbox_id),
|
|
|
|
@ -785,8 +793,8 @@ impl MailBackend for JmapType {
|
|
|
|
|
let conn = connection.lock().await;
|
|
|
|
|
let mail_account_id = conn.session_guard().await?.mail_account_id();
|
|
|
|
|
|
|
|
|
|
let email_set_call: EmailSet = EmailSet::new(
|
|
|
|
|
Set::<EmailObject>::new()
|
|
|
|
|
let email_set_call = objects::email::EmailSet::new(
|
|
|
|
|
Set::<objects::email::EmailObject>::new()
|
|
|
|
|
.account_id(mail_account_id)
|
|
|
|
|
.update(Some(update_map)),
|
|
|
|
|
);
|
|
|
|
@ -806,7 +814,8 @@ impl MailBackend for JmapType {
|
|
|
|
|
Ok(s) => s,
|
|
|
|
|
};
|
|
|
|
|
store.online_status.update_timestamp(None).await;
|
|
|
|
|
let m = SetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
|
|
|
|
|
let m =
|
|
|
|
|
SetResponse::<objects::email::EmailObject>::try_from(v.method_responses.remove(0))?;
|
|
|
|
|
if let Some(ids) = m.not_updated {
|
|
|
|
|
if !ids.is_empty() {
|
|
|
|
|
return Err(Error::new(format!(
|
|
|
|
@ -831,9 +840,12 @@ impl MailBackend for JmapType {
|
|
|
|
|
let store = self.store.clone();
|
|
|
|
|
let connection = self.connection.clone();
|
|
|
|
|
Ok(Box::pin(async move {
|
|
|
|
|
let mut update_map: IndexMap<Argument<Id<EmailObject>>, Value> = IndexMap::default();
|
|
|
|
|
let mut ids: Vec<Id<EmailObject>> = Vec::with_capacity(env_hashes.rest.len() + 1);
|
|
|
|
|
let mut id_map: IndexMap<Id<EmailObject>, EnvelopeHash> = IndexMap::default();
|
|
|
|
|
let mut update_map: IndexMap<Argument<Id<objects::email::EmailObject>>, Value> =
|
|
|
|
|
IndexMap::default();
|
|
|
|
|
let mut ids: Vec<Id<objects::email::EmailObject>> =
|
|
|
|
|
Vec::with_capacity(env_hashes.rest.len() + 1);
|
|
|
|
|
let mut id_map: IndexMap<Id<objects::email::EmailObject>, EnvelopeHash> =
|
|
|
|
|
IndexMap::default();
|
|
|
|
|
let mut update_keywords: IndexMap<String, Value> = IndexMap::default();
|
|
|
|
|
for op in flags.iter() {
|
|
|
|
|
match op {
|
|
|
|
@ -894,15 +906,15 @@ impl MailBackend for JmapType {
|
|
|
|
|
let conn = connection.lock().await;
|
|
|
|
|
let mail_account_id = conn.session_guard().await?.mail_account_id();
|
|
|
|
|
|
|
|
|
|
let email_set_call: EmailSet = EmailSet::new(
|
|
|
|
|
Set::<EmailObject>::new()
|
|
|
|
|
let email_set_call = objects::email::EmailSet::new(
|
|
|
|
|
Set::<objects::email::EmailObject>::new()
|
|
|
|
|
.account_id(mail_account_id.clone())
|
|
|
|
|
.update(Some(update_map)),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let mut req = Request::new(conn.request_no.clone());
|
|
|
|
|
req.add_call(&email_set_call).await;
|
|
|
|
|
let email_call: EmailGet = EmailGet::new(
|
|
|
|
|
let email_call = objects::email::EmailGet::new(
|
|
|
|
|
Get::new()
|
|
|
|
|
.ids(Some(Argument::Value(ids)))
|
|
|
|
|
.account_id(mail_account_id)
|
|
|
|
@ -930,7 +942,8 @@ impl MailBackend for JmapType {
|
|
|
|
|
Ok(s) => s,
|
|
|
|
|
};
|
|
|
|
|
store.online_status.update_timestamp(None).await;
|
|
|
|
|
let m = SetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
|
|
|
|
|
let m =
|
|
|
|
|
SetResponse::<objects::email::EmailObject>::try_from(v.method_responses.remove(0))?;
|
|
|
|
|
if let Some(ids) = m.not_updated {
|
|
|
|
|
return Err(Error::new(
|
|
|
|
|
ids.into_iter()
|
|
|
|
@ -949,8 +962,10 @@ impl MailBackend for JmapType {
|
|
|
|
|
}
|
|
|
|
|
drop(tag_index_lck);
|
|
|
|
|
}
|
|
|
|
|
let e = GetResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
|
|
|
|
|
let GetResponse::<EmailObject> { list, state, .. } = e;
|
|
|
|
|
let e = GetResponse::<objects::email::EmailObject>::try_from(
|
|
|
|
|
v.method_responses.pop().unwrap(),
|
|
|
|
|
)?;
|
|
|
|
|
let GetResponse::<objects::email::EmailObject> { list, state, .. } = e;
|
|
|
|
|
{
|
|
|
|
|
let (is_empty, is_equal) = {
|
|
|
|
|
let mailboxes_lck = conn.store.mailboxes.read().unwrap();
|
|
|
|
@ -967,7 +982,11 @@ impl MailBackend for JmapType {
|
|
|
|
|
};
|
|
|
|
|
if is_empty {
|
|
|
|
|
let mut mailboxes_lck = conn.store.mailboxes.write().unwrap();
|
|
|
|
|
debug!("{:?}: inserting state {}", EmailObject::NAME, &state);
|
|
|
|
|
debug!(
|
|
|
|
|
"{:?}: inserting state {}",
|
|
|
|
|
objects::email::EmailObject::NAME,
|
|
|
|
|
&state
|
|
|
|
|
);
|
|
|
|
|
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
|
|
|
|
|
*mbox.email_state.lock().unwrap() = Some(state);
|
|
|
|
|
});
|
|
|
|
@ -1019,30 +1038,31 @@ impl MailBackend for JmapType {
|
|
|
|
|
// "$draft" flag and moves it from the Drafts folder to the Sent folder.
|
|
|
|
|
let (draft_mailbox_id, sent_mailbox_id) = {
|
|
|
|
|
let mailboxes_lck = store.mailboxes.read().unwrap();
|
|
|
|
|
let find_fn = |usage: SpecialUsageMailbox| -> Result<Id<MailboxObject>> {
|
|
|
|
|
if let Some(sent_folder) =
|
|
|
|
|
mailboxes_lck.values().find(|m| m.special_usage() == usage)
|
|
|
|
|
{
|
|
|
|
|
Ok(sent_folder.id.clone())
|
|
|
|
|
} else if let Some(sent_folder) = mailboxes_lck
|
|
|
|
|
.values()
|
|
|
|
|
.find(|m| m.special_usage() == SpecialUsageMailbox::Inbox)
|
|
|
|
|
{
|
|
|
|
|
Ok(sent_folder.id.clone())
|
|
|
|
|
} else {
|
|
|
|
|
Ok(mailboxes_lck
|
|
|
|
|
let find_fn =
|
|
|
|
|
|usage: SpecialUsageMailbox| -> Result<Id<objects::mailbox::MailboxObject>> {
|
|
|
|
|
if let Some(sent_folder) =
|
|
|
|
|
mailboxes_lck.values().find(|m| m.special_usage() == usage)
|
|
|
|
|
{
|
|
|
|
|
Ok(sent_folder.id.clone())
|
|
|
|
|
} else if let Some(sent_folder) = mailboxes_lck
|
|
|
|
|
.values()
|
|
|
|
|
.next()
|
|
|
|
|
.ok_or_else(|| {
|
|
|
|
|
Error::new(format!(
|
|
|
|
|
"Account `{}` has no mailboxes.",
|
|
|
|
|
store.account_name
|
|
|
|
|
))
|
|
|
|
|
})?
|
|
|
|
|
.id
|
|
|
|
|
.clone())
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
.find(|m| m.special_usage() == SpecialUsageMailbox::Inbox)
|
|
|
|
|
{
|
|
|
|
|
Ok(sent_folder.id.clone())
|
|
|
|
|
} else {
|
|
|
|
|
Ok(mailboxes_lck
|
|
|
|
|
.values()
|
|
|
|
|
.next()
|
|
|
|
|
.ok_or_else(|| {
|
|
|
|
|
Error::new(format!(
|
|
|
|
|
"Account `{}` has no mailboxes.",
|
|
|
|
|
store.account_name
|
|
|
|
|
))
|
|
|
|
|
})?
|
|
|
|
|
.id
|
|
|
|
|
.clone())
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
(find_fn(SpecialUsageMailbox::Drafts)?, {
|
|
|
|
|
if let Some(h) = mailbox_hash {
|
|
|
|
@ -1085,11 +1105,11 @@ impl MailBackend for JmapType {
|
|
|
|
|
};
|
|
|
|
|
{
|
|
|
|
|
let mut req = Request::new(conn.request_no.clone());
|
|
|
|
|
let creation_id: Id<EmailObject> = "newid".into();
|
|
|
|
|
let import_call: EmailImport = EmailImport::new()
|
|
|
|
|
let creation_id: Id<objects::email::EmailObject> = "newid".into();
|
|
|
|
|
let import_call: objects::email::EmailImport = objects::email::EmailImport::new()
|
|
|
|
|
.account_id(mail_account_id.clone())
|
|
|
|
|
.emails(indexmap! {
|
|
|
|
|
creation_id => EmailImportObject::new()
|
|
|
|
|
creation_id => objects::email::EmailImportObject::new()
|
|
|
|
|
.blob_id(upload_response.blob_id)
|
|
|
|
|
.keywords(indexmap! {
|
|
|
|
|
"$draft".to_string() => true,
|
|
|
|
@ -1125,11 +1145,11 @@ impl MailBackend for JmapType {
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let mut req = Request::new(conn.request_no.clone());
|
|
|
|
|
let subm_set_call: EmailSubmissionSet = EmailSubmissionSet::new(
|
|
|
|
|
Set::<EmailSubmissionObject>::new()
|
|
|
|
|
let subm_set_call = objects::submission::EmailSubmissionSet::new(
|
|
|
|
|
Set::<objects::submission::EmailSubmissionObject>::new()
|
|
|
|
|
.account_id(mail_account_id.clone())
|
|
|
|
|
.create(Some(indexmap! {
|
|
|
|
|
Argument::from(Id::from("k1490")) => EmailSubmissionObject::new(
|
|
|
|
|
Argument::from(Id::from("k1490")) => objects::submission::EmailSubmissionObject::new(
|
|
|
|
|
/* account_id: */ mail_account_id,
|
|
|
|
|
/* identity_id: */ identity_id,
|
|
|
|
|
/* email_id: */ email_id,
|
|
|
|
|