Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::changes::state::StateManager;
|
||||
use common::{Server, storage::index::ObjectIndexBuilder};
|
||||
use email::identity::{ArchivedEmailAddress, Identity};
|
||||
use jmap_proto::{
|
||||
method::get::{GetRequest, GetResponse},
|
||||
object::identity::{self, IdentityProperty, IdentityValue},
|
||||
};
|
||||
use jmap_tools::{Map, Value};
|
||||
use std::{collections::BTreeSet, future::Future};
|
||||
use store::{
|
||||
SerializeInfallible, ValueKey,
|
||||
rkyv::{option::ArchivedOption, vec::ArchivedVec},
|
||||
roaring::RoaringBitmap,
|
||||
write::{AlignedBytes, Archive, BatchBuilder, assert::AssertValue},
|
||||
xxhash_rust::xxh3::Xxh3,
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
collection::{Collection, SyncCollection},
|
||||
field::{Field, IdentityField, PrincipalField},
|
||||
};
|
||||
|
||||
pub trait IdentityGet: Sync + Send {
|
||||
fn identity_get(
|
||||
&self,
|
||||
request: GetRequest<identity::Identity>,
|
||||
) -> impl Future<Output = trc::Result<GetResponse<identity::Identity>>> + Send;
|
||||
|
||||
fn identity_get_or_create(
|
||||
&self,
|
||||
account_id: u32,
|
||||
) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send;
|
||||
}
|
||||
|
||||
impl IdentityGet for Server {
|
||||
async fn identity_get(
|
||||
&self,
|
||||
mut request: GetRequest<identity::Identity>,
|
||||
) -> trc::Result<GetResponse<identity::Identity>> {
|
||||
let (ids, not_found_ids) = request.unwrap_ids(self.core.jmap.get_max_objects)?;
|
||||
let properties = request.unwrap_properties(&[
|
||||
IdentityProperty::Id,
|
||||
IdentityProperty::Name,
|
||||
IdentityProperty::Email,
|
||||
IdentityProperty::ReplyTo,
|
||||
IdentityProperty::Bcc,
|
||||
IdentityProperty::TextSignature,
|
||||
IdentityProperty::HtmlSignature,
|
||||
IdentityProperty::MayDelete,
|
||||
]);
|
||||
let account_id = request.account_id.document_id();
|
||||
let identity_ids = self.identity_get_or_create(account_id).await?;
|
||||
let ids = if let Some(ids) = ids {
|
||||
ids
|
||||
} else {
|
||||
identity_ids
|
||||
.iter()
|
||||
.take(self.core.jmap.get_max_objects)
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let mut response = GetResponse {
|
||||
account_id: request.account_id.into(),
|
||||
state: self
|
||||
.get_state(account_id, SyncCollection::Identity)
|
||||
.await?
|
||||
.into(),
|
||||
list: Vec::with_capacity(ids.len()),
|
||||
not_found: not_found_ids,
|
||||
};
|
||||
|
||||
for id in ids {
|
||||
// Obtain the identity object
|
||||
let document_id = id.document_id();
|
||||
if !identity_ids.contains(document_id) {
|
||||
response.push_not_found(id);
|
||||
continue;
|
||||
}
|
||||
let _identity = if let Some(identity) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::Identity,
|
||||
document_id,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
identity
|
||||
} else {
|
||||
response.push_not_found(id);
|
||||
continue;
|
||||
};
|
||||
let identity = _identity
|
||||
.unarchive::<Identity>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut result = Map::with_capacity(properties.len());
|
||||
for property in &properties {
|
||||
match property {
|
||||
IdentityProperty::Id => {
|
||||
result.insert_unchecked(IdentityProperty::Id, IdentityValue::Id(id));
|
||||
}
|
||||
IdentityProperty::MayDelete => {
|
||||
result.insert_unchecked(IdentityProperty::MayDelete, Value::Bool(true));
|
||||
}
|
||||
IdentityProperty::Name => {
|
||||
result.insert_unchecked(IdentityProperty::Name, identity.name.to_string());
|
||||
}
|
||||
IdentityProperty::Email => {
|
||||
result
|
||||
.insert_unchecked(IdentityProperty::Email, identity.email.to_string());
|
||||
}
|
||||
IdentityProperty::TextSignature => {
|
||||
result.insert_unchecked(
|
||||
IdentityProperty::TextSignature,
|
||||
identity.text_signature.to_string(),
|
||||
);
|
||||
}
|
||||
IdentityProperty::HtmlSignature => {
|
||||
result.insert_unchecked(
|
||||
IdentityProperty::HtmlSignature,
|
||||
identity.html_signature.to_string(),
|
||||
);
|
||||
}
|
||||
IdentityProperty::Bcc => {
|
||||
result
|
||||
.insert_unchecked(IdentityProperty::Bcc, email_to_value(&identity.bcc));
|
||||
}
|
||||
IdentityProperty::ReplyTo => {
|
||||
result.insert_unchecked(
|
||||
IdentityProperty::ReplyTo,
|
||||
email_to_value(&identity.reply_to),
|
||||
);
|
||||
}
|
||||
property => {
|
||||
result.insert_unchecked(property.clone(), Value::Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
response.list.push(result.into());
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn identity_get_or_create(&self, account_id: u32) -> trc::Result<RoaringBitmap> {
|
||||
// Obtain account info
|
||||
let account_info = self
|
||||
.account_info(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let addresses = account_info
|
||||
.addresses()
|
||||
.iter()
|
||||
.map(|a| a.as_str())
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
let mut hasher = Xxh3::new();
|
||||
for address in &addresses {
|
||||
hasher.update(address.as_bytes());
|
||||
hasher.update(b"\n");
|
||||
}
|
||||
let addresses_hash = hasher.digest();
|
||||
let stored_hash = self
|
||||
.store()
|
||||
.get_value::<u64>(ValueKey::property(
|
||||
account_id,
|
||||
Collection::Principal,
|
||||
0,
|
||||
PrincipalField::IdentityAddresses,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
let mut identity_ids = self
|
||||
.document_ids(account_id, Collection::Identity, IdentityField::DocumentId)
|
||||
.await?;
|
||||
if stored_hash == Some(addresses_hash) {
|
||||
return Ok(identity_ids);
|
||||
}
|
||||
|
||||
// Determine which addresses are missing and which identities are no longer valid
|
||||
let member_of = &account_info.account().id_member_of;
|
||||
let mut missing_addresses = addresses.clone();
|
||||
let mut obsolete_ids = Vec::new();
|
||||
for document_id in &identity_ids {
|
||||
if let Some(identity) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::Identity,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
let email = identity
|
||||
.unarchive::<Identity>()
|
||||
.caused_by(trc::location!())?
|
||||
.email
|
||||
.as_str();
|
||||
|
||||
if addresses.contains(email) {
|
||||
missing_addresses.remove(email);
|
||||
} else if !self
|
||||
.account_id_from_email(email, true)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.is_some_and(|id| id == account_id || member_of.contains(&id))
|
||||
{
|
||||
obsolete_ids.push(document_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::Identity);
|
||||
|
||||
// Create identities for the new addresses
|
||||
if !missing_addresses.is_empty() {
|
||||
let name = account_info.description().unwrap_or(account_info.name());
|
||||
let mut next_document_id = self
|
||||
.store()
|
||||
.assign_document_ids(
|
||||
account_id,
|
||||
Collection::Identity,
|
||||
missing_addresses.len() as u64,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
for email in missing_addresses {
|
||||
let name = if name.is_empty() {
|
||||
email.to_string()
|
||||
} else {
|
||||
name.to_string()
|
||||
};
|
||||
let document_id = next_document_id;
|
||||
next_document_id -= 1;
|
||||
batch
|
||||
.with_document(document_id)
|
||||
.tag(IdentityField::DocumentId)
|
||||
.custom(ObjectIndexBuilder::<(), _>::new().with_changes(Identity {
|
||||
name,
|
||||
email: email.to_string(),
|
||||
..Default::default()
|
||||
}))
|
||||
.caused_by(trc::location!())?
|
||||
.commit_point();
|
||||
identity_ids.insert(document_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete identities whose address no longer belongs to this account
|
||||
for document_id in obsolete_ids {
|
||||
batch
|
||||
.with_document(document_id)
|
||||
.untag(IdentityField::DocumentId)
|
||||
.clear(Field::ARCHIVE)
|
||||
.log_item_delete(SyncCollection::Identity, None)
|
||||
.commit_point();
|
||||
identity_ids.remove(document_id);
|
||||
}
|
||||
|
||||
batch
|
||||
.with_collection(Collection::Principal)
|
||||
.with_document(0)
|
||||
.assert_value(
|
||||
PrincipalField::IdentityAddresses,
|
||||
stored_hash.map_or(AssertValue::None, AssertValue::U64),
|
||||
)
|
||||
.set(
|
||||
PrincipalField::IdentityAddresses,
|
||||
addresses_hash.serialize(),
|
||||
);
|
||||
|
||||
match self.commit_batch(batch).await {
|
||||
Ok(_) => Ok(identity_ids),
|
||||
Err(err) if err.is_assertion_failure() => self
|
||||
.document_ids(account_id, Collection::Identity, IdentityField::DocumentId)
|
||||
.await
|
||||
.caused_by(trc::location!()),
|
||||
Err(err) => Err(err.caused_by(trc::location!())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn email_to_value(
|
||||
email: &ArchivedOption<ArchivedVec<ArchivedEmailAddress>>,
|
||||
) -> Value<'static, IdentityProperty, IdentityValue> {
|
||||
if let ArchivedOption::Some(email) = email {
|
||||
Value::Array(
|
||||
email
|
||||
.iter()
|
||||
.map(|email| {
|
||||
Value::Object(
|
||||
Map::with_capacity(2)
|
||||
.with_key_value(IdentityProperty::Name, &email.name)
|
||||
.with_key_value(IdentityProperty::Email, &email.email),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
Value::Null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod get;
|
||||
pub mod set;
|
||||
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::{Server, storage::index::ObjectIndexBuilder};
|
||||
use email::identity::{EmailAddress, Identity};
|
||||
use jmap_proto::{
|
||||
error::set::{SetError, SetErrorType},
|
||||
method::set::{SetRequest, SetResponse},
|
||||
object::identity::{self, IdentityProperty, IdentityValue},
|
||||
references::resolve::ResolveCreatedReference,
|
||||
request::MaybeInvalid,
|
||||
types::state::State,
|
||||
};
|
||||
use jmap_tools::{Key, Value};
|
||||
use registry::schema::enums::StorageQuota;
|
||||
use std::future::Future;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive, BatchBuilder},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
collection::{Collection, SyncCollection},
|
||||
field::{Field, IdentityField},
|
||||
id::Id,
|
||||
};
|
||||
use utils::sanitize_email;
|
||||
|
||||
pub trait IdentitySet: Sync + Send {
|
||||
fn identity_set(
|
||||
&self,
|
||||
request: SetRequest<'_, identity::Identity>,
|
||||
) -> impl Future<Output = trc::Result<SetResponse<identity::Identity>>> + Send;
|
||||
}
|
||||
|
||||
impl IdentitySet for Server {
|
||||
async fn identity_set(
|
||||
&self,
|
||||
mut request: SetRequest<'_, identity::Identity>,
|
||||
) -> trc::Result<SetResponse<identity::Identity>> {
|
||||
let account_id = request.account_id.document_id();
|
||||
let identity_ids = self
|
||||
.document_ids(account_id, Collection::Identity, IdentityField::DocumentId)
|
||||
.await?;
|
||||
let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?;
|
||||
let will_destroy = response.collect_will_destroy(request.unwrap_destroy());
|
||||
let account_info = self
|
||||
.account_info(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Process creates
|
||||
let mut batch = BatchBuilder::new();
|
||||
'create: for (id, object) in request.unwrap_create() {
|
||||
let mut identity = Identity::default();
|
||||
|
||||
for (property, mut value) in object.into_expanded_object() {
|
||||
if let Err(err) = response
|
||||
.resolve_self_references(&mut value, 0, false)
|
||||
.and_then(|_| {
|
||||
validate_identity_value(None, &property, value, &mut identity, true)
|
||||
})
|
||||
{
|
||||
response.not_created.append(id, err);
|
||||
continue 'create;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate email address
|
||||
if !identity.email.is_empty() {
|
||||
if !account_info
|
||||
.addresses()
|
||||
.iter()
|
||||
.any(|e| e == &identity.email)
|
||||
{
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::invalid_properties()
|
||||
.with_property(IdentityProperty::Email)
|
||||
.with_description(
|
||||
"E-mail address not configured for this account.".to_string(),
|
||||
),
|
||||
);
|
||||
continue 'create;
|
||||
}
|
||||
} else {
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::invalid_properties()
|
||||
.with_property(IdentityProperty::Email)
|
||||
.with_description("Missing e-mail address."),
|
||||
);
|
||||
continue 'create;
|
||||
}
|
||||
|
||||
// Validate quota
|
||||
if identity_ids.len()
|
||||
>= self.object_quota(
|
||||
account_info.object_quotas(),
|
||||
StorageQuota::MaxEmailIdentities,
|
||||
) as u64
|
||||
{
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::new(SetErrorType::OverQuota).with_description(concat!(
|
||||
"There are too many identities, ",
|
||||
"please delete some before adding a new one."
|
||||
)),
|
||||
);
|
||||
continue 'create;
|
||||
}
|
||||
|
||||
// Insert record
|
||||
let document_id = self
|
||||
.store()
|
||||
.assign_document_ids(account_id, Collection::Identity, 1)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::Identity)
|
||||
.with_document(document_id)
|
||||
.tag(IdentityField::DocumentId)
|
||||
.custom(ObjectIndexBuilder::<(), _>::new().with_changes(identity))
|
||||
.caused_by(trc::location!())?
|
||||
.commit_point();
|
||||
response.created(id, document_id);
|
||||
}
|
||||
|
||||
// Process updates
|
||||
'update: for (id, object) in request.unwrap_update() {
|
||||
let id = match id {
|
||||
MaybeInvalid::Value(id) => id,
|
||||
invalid => {
|
||||
response.not_updated.append(invalid, SetError::not_found());
|
||||
continue 'update;
|
||||
}
|
||||
};
|
||||
// Make sure id won't be destroyed
|
||||
if will_destroy.contains(&id) {
|
||||
response.not_updated.append(id, SetError::will_destroy());
|
||||
continue 'update;
|
||||
}
|
||||
|
||||
// Obtain identity
|
||||
let document_id = id.document_id();
|
||||
let identity_ = if let Some(identity_) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::Identity,
|
||||
document_id,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
identity_
|
||||
} else {
|
||||
response.not_updated.append(id, SetError::not_found());
|
||||
continue 'update;
|
||||
};
|
||||
let identity = identity_
|
||||
.to_unarchived::<Identity>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut new_identity = identity
|
||||
.deserialize::<Identity>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
for (property, mut value) in object.into_expanded_object() {
|
||||
if let Err(err) = response
|
||||
.resolve_self_references(&mut value, 0, false)
|
||||
.and_then(|_| {
|
||||
validate_identity_value(
|
||||
Some(id),
|
||||
&property,
|
||||
value,
|
||||
&mut new_identity,
|
||||
false,
|
||||
)
|
||||
})
|
||||
{
|
||||
response.not_updated.append(id, err);
|
||||
continue 'update;
|
||||
}
|
||||
}
|
||||
|
||||
// Update record
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::Identity)
|
||||
.with_document(document_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::new()
|
||||
.with_current(identity)
|
||||
.with_changes(new_identity),
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.commit_point();
|
||||
response.updated.append(id, None);
|
||||
}
|
||||
|
||||
// Process deletions
|
||||
for id in will_destroy {
|
||||
let document_id = id.document_id();
|
||||
if identity_ids.contains(document_id) {
|
||||
// Update record
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::Identity)
|
||||
.with_document(document_id)
|
||||
.untag(IdentityField::DocumentId)
|
||||
.clear(Field::ARCHIVE)
|
||||
.log_item_delete(SyncCollection::Identity, None)
|
||||
.commit_point();
|
||||
response.destroyed.push(id);
|
||||
} else {
|
||||
response.not_destroyed.append(id, SetError::not_found());
|
||||
}
|
||||
}
|
||||
|
||||
// Write changes
|
||||
if !batch.is_empty() {
|
||||
let change_id = self
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.and_then(|ids| ids.last_change_id(account_id))
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
response.new_state = State::Exact(change_id).into();
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_identity_value(
|
||||
expected_id: Option<Id>,
|
||||
property: &Key<'_, IdentityProperty>,
|
||||
value: Value<'_, IdentityProperty, IdentityValue>,
|
||||
identity: &mut Identity,
|
||||
is_create: bool,
|
||||
) -> Result<(), SetError<IdentityProperty>> {
|
||||
let Key::Property(property) = property else {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(property.to_owned())
|
||||
.with_description("Invalid property."));
|
||||
};
|
||||
|
||||
match (property, value) {
|
||||
(IdentityProperty::Name, Value::Str(value)) if value.len() < 255 => {
|
||||
identity.name = value.into_owned();
|
||||
}
|
||||
(IdentityProperty::Email, Value::Str(value)) if is_create && value.len() < 255 => {
|
||||
identity.email = sanitize_email(&value).ok_or_else(|| {
|
||||
SetError::invalid_properties()
|
||||
.with_property(IdentityProperty::Email)
|
||||
.with_description("Invalid e-mail address.")
|
||||
})?;
|
||||
}
|
||||
(IdentityProperty::TextSignature, Value::Str(value)) if value.len() < 2048 => {
|
||||
identity.text_signature = value.into_owned();
|
||||
}
|
||||
(IdentityProperty::HtmlSignature, Value::Str(value)) if value.len() < 2048 => {
|
||||
identity.html_signature = value.into_owned();
|
||||
}
|
||||
(IdentityProperty::ReplyTo | IdentityProperty::Bcc, Value::Array(value)) => {
|
||||
let mut addresses = Vec::with_capacity(value.len());
|
||||
for addr in value {
|
||||
let mut address = EmailAddress {
|
||||
name: None,
|
||||
email: "".into(),
|
||||
};
|
||||
let mut is_valid = false;
|
||||
if let Value::Object(obj) = addr {
|
||||
for (key, value) in obj.into_vec() {
|
||||
match (key, value) {
|
||||
(Key::Property(IdentityProperty::Email), Value::Str(value))
|
||||
if value.len() < 255 =>
|
||||
{
|
||||
is_valid = true;
|
||||
address.email = value.into_owned();
|
||||
}
|
||||
(Key::Property(IdentityProperty::Name), Value::Str(value))
|
||||
if value.len() < 255 =>
|
||||
{
|
||||
address.name = Some(value.into_owned());
|
||||
}
|
||||
(Key::Property(IdentityProperty::Name), Value::Null) => (),
|
||||
_ => {
|
||||
is_valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if is_valid && !address.email.is_empty() {
|
||||
addresses.push(address);
|
||||
} else {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(property.clone())
|
||||
.with_description("Invalid e-mail address object."));
|
||||
}
|
||||
}
|
||||
|
||||
match property {
|
||||
IdentityProperty::ReplyTo => {
|
||||
identity.reply_to = Some(addresses);
|
||||
}
|
||||
IdentityProperty::Bcc => {
|
||||
identity.bcc = Some(addresses);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
(IdentityProperty::Name, Value::Null) => {
|
||||
identity.name.clear();
|
||||
}
|
||||
(IdentityProperty::TextSignature, Value::Null) => {
|
||||
identity.text_signature.clear();
|
||||
}
|
||||
(IdentityProperty::HtmlSignature, Value::Null) => {
|
||||
identity.html_signature.clear();
|
||||
}
|
||||
(IdentityProperty::ReplyTo, Value::Null) => identity.reply_to = None,
|
||||
(IdentityProperty::Bcc, Value::Null) => identity.bcc = None,
|
||||
(IdentityProperty::Id, value) => {
|
||||
if !expected_id.is_some_and(|expected| crate::matches_id(&value, expected)) {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(IdentityProperty::Id)
|
||||
.with_description("The id property is immutable."));
|
||||
}
|
||||
}
|
||||
(property, _) => {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_property(property.clone())
|
||||
.with_description("Field could not be set."));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user