From fa6b42447e2649ca57114a3d1c1b8d1559ab2765 Mon Sep 17 00:00:00 2001 From: nig Date: Thu, 7 Nov 2024 10:43:04 +0100 Subject: [PATCH 01/32] [tutasdk] move id-related code to its own module --- tuta-sdk/rust/demo/src/main.rs | 2 +- .../sdk/src/blobs/blob_access_token_cache.rs | 2 +- .../sdk/src/blobs/blob_access_token_facade.rs | 6 +- tuta-sdk/rust/sdk/src/blobs/blob_facade.rs | 6 +- tuta-sdk/rust/sdk/src/crypto/crypto_facade.rs | 4 +- tuta-sdk/rust/sdk/src/crypto_entity_client.rs | 4 +- tuta-sdk/rust/sdk/src/element_value.rs | 8 ++- tuta-sdk/rust/sdk/src/entity_client.rs | 13 +--- tuta-sdk/rust/sdk/src/id.rs | 3 + tuta-sdk/rust/sdk/src/{ => id}/custom_id.rs | 2 +- .../rust/sdk/src/{ => id}/generated_id.rs | 2 +- tuta-sdk/rust/sdk/src/id/id_tuple.rs | 61 ++++++++++++++++++ tuta-sdk/rust/sdk/src/instance_mapper.rs | 10 ++- tuta-sdk/rust/sdk/src/json_serializer.rs | 4 +- tuta-sdk/rust/sdk/src/key_cache.rs | 2 +- tuta-sdk/rust/sdk/src/key_loader_facade.rs | 6 +- tuta-sdk/rust/sdk/src/lib.rs | 62 ++----------------- tuta-sdk/rust/sdk/src/login/credentials.rs | 2 +- tuta-sdk/rust/sdk/src/login/login_facade.rs | 9 +-- tuta-sdk/rust/sdk/src/mail_facade.rs | 7 ++- tuta-sdk/rust/sdk/src/typed_entity_client.rs | 4 +- tuta-sdk/rust/sdk/src/user_facade.rs | 2 +- .../rust/sdk/src/util/entity_test_utils.rs | 2 +- tuta-sdk/rust/sdk/src/util/test_utils.rs | 4 +- tuta-sdk/rust/sdk/tests/download_mail_test.rs | 2 +- 25 files changed, 119 insertions(+), 110 deletions(-) create mode 100644 tuta-sdk/rust/sdk/src/id.rs rename tuta-sdk/rust/sdk/src/{ => id}/custom_id.rs (97%) rename tuta-sdk/rust/sdk/src/{ => id}/generated_id.rs (98%) create mode 100644 tuta-sdk/rust/sdk/src/id/id_tuple.rs diff --git a/tuta-sdk/rust/demo/src/main.rs b/tuta-sdk/rust/demo/src/main.rs index 2df1c41e4cc..4f76743047e 100644 --- a/tuta-sdk/rust/demo/src/main.rs +++ b/tuta-sdk/rust/demo/src/main.rs @@ -5,11 +5,11 @@ use std::sync::Arc; use async_trait::async_trait; use tutasdk::folder_system::MailSetKind; -use tutasdk::generated_id::GeneratedId; use tutasdk::login::{CredentialType, Credentials}; use tutasdk::rest_client::{ HttpMethod, RestClient, RestClientError, RestClientOptions, RestResponse, }; +use tutasdk::GeneratedId; use tutasdk::Sdk; struct ReqwestHttpClient { diff --git a/tuta-sdk/rust/sdk/src/blobs/blob_access_token_cache.rs b/tuta-sdk/rust/sdk/src/blobs/blob_access_token_cache.rs index 272a11a3072..dae127d1905 100644 --- a/tuta-sdk/rust/sdk/src/blobs/blob_access_token_cache.rs +++ b/tuta-sdk/rust/sdk/src/blobs/blob_access_token_cache.rs @@ -1,7 +1,7 @@ use crate::date::DateProvider; use crate::entities::generated::storage::BlobServerAccessInfo; -use crate::generated_id::GeneratedId; use crate::tutanota_constants::ArchiveDataType; +use crate::GeneratedId; use std::collections::HashMap; use std::future::Future; use std::sync::{Arc, RwLock}; diff --git a/tuta-sdk/rust/sdk/src/blobs/blob_access_token_facade.rs b/tuta-sdk/rust/sdk/src/blobs/blob_access_token_facade.rs index 825b0f89e33..1e6526188f0 100644 --- a/tuta-sdk/rust/sdk/src/blobs/blob_access_token_facade.rs +++ b/tuta-sdk/rust/sdk/src/blobs/blob_access_token_facade.rs @@ -1,17 +1,17 @@ use crate::blobs::blob_access_token_cache::{BlobAccessTokenCache, BlobWriteTokenKey}; use crate::crypto::randomizer_facade::RandomizerFacade; -use crate::custom_id::CustomId; use crate::date::DateProvider; use crate::entities::generated::storage::{ BlobAccessTokenPostIn, BlobServerAccessInfo, BlobWriteData, }; -use crate::generated_id::GeneratedId; use crate::services::generated::storage::BlobAccessTokenService; #[cfg_attr(test, mockall_double::double)] use crate::services::service_executor::ResolvingServiceExecutor; use crate::services::ExtraServiceParams; use crate::tutanota_constants::ArchiveDataType; use crate::ApiCallError; +use crate::CustomId; +use crate::GeneratedId; use base64::prelude::BASE64_URL_SAFE_NO_PAD; use base64::Engine; use std::sync::Arc; @@ -83,13 +83,13 @@ impl BlobAccessTokenFacade { mod tests { use super::*; use crate::crypto::randomizer_facade::RandomizerFacade; - use crate::custom_id::CustomId; use crate::date::date_provider::stub::DateProviderStub; use crate::date::DateTime; use crate::entities::generated::storage::{BlobAccessTokenPostOut, BlobServerAccessInfo}; use crate::services::service_executor::MockResolvingServiceExecutor; use crate::tutanota_constants::ArchiveDataType; use crate::util::test_utils::create_test_entity; + use crate::CustomId; use crate::GeneratedId; use std::sync::Arc; diff --git a/tuta-sdk/rust/sdk/src/blobs/blob_facade.rs b/tuta-sdk/rust/sdk/src/blobs/blob_facade.rs index 88636728ddb..c110830435b 100644 --- a/tuta-sdk/rust/sdk/src/blobs/blob_facade.rs +++ b/tuta-sdk/rust/sdk/src/blobs/blob_facade.rs @@ -7,7 +7,6 @@ use crate::crypto::randomizer_facade::RandomizerFacade; use crate::entities::generated::storage::{BlobGetIn, BlobPostOut, BlobServerAccessInfo}; use crate::entities::generated::sys::BlobReferenceTokenWrapper; use crate::entities::Entity; -use crate::generated_id::GeneratedId; use crate::instance_mapper::InstanceMapper; use crate::json_element::RawEntity; use crate::json_serializer::JsonSerializer; @@ -17,6 +16,7 @@ use crate::rest_client::{RestClientOptions, RestResponse}; use crate::rest_error::HttpError; use crate::tutanota_constants::{ArchiveDataType, MAX_BLOB_SIZE_BYTES}; use crate::type_model_provider::init_type_model_provider; +use crate::GeneratedId; use crate::{crypto, ApiCallError, HeadersProvider}; use base64::Engine; use crypto::sha256; @@ -256,17 +256,17 @@ mod tests { use crate::blobs::blob_access_token_facade::MockBlobAccessTokenFacade; use crate::crypto::randomizer_facade::test_util::DeterministicRng; use crate::crypto::randomizer_facade::RandomizerFacade; - use crate::custom_id::CustomId; use crate::entities::generated::storage::BlobPostOut; use crate::entities::generated::storage::{BlobServerAccessInfo, BlobServerUrl}; use crate::entities::generated::sys::BlobReferenceTokenWrapper; - use crate::generated_id::GeneratedId; use crate::rest_client::MockRestClient; use crate::rest_client::RestClientOptions; use crate::rest_client::RestResponse; use crate::tutanota_constants::ArchiveDataType; use crate::type_model_provider::init_type_model_provider; use crate::util::test_utils::create_test_entity; + use crate::CustomId; + use crate::GeneratedId; use crate::GenericAesKey; use crate::HeadersProvider; use crate::InstanceMapper; diff --git a/tuta-sdk/rust/sdk/src/crypto/crypto_facade.rs b/tuta-sdk/rust/sdk/src/crypto/crypto_facade.rs index b748fd4d015..1f5c08956ca 100644 --- a/tuta-sdk/rust/sdk/src/crypto/crypto_facade.rs +++ b/tuta-sdk/rust/sdk/src/crypto/crypto_facade.rs @@ -7,12 +7,12 @@ use crate::crypto::tuta_crypt::{PQError, PQMessage}; use crate::crypto::Aes256Key; use crate::element_value::{ElementValue, ParsedEntity}; use crate::entities::generated::sys::BucketKey; -use crate::generated_id::GeneratedId; use crate::instance_mapper::InstanceMapper; #[cfg_attr(test, mockall_double::double)] use crate::key_loader_facade::KeyLoaderFacade; use crate::metamodel::TypeModel; use crate::util::ArrayCastingError; +use crate::GeneratedId; use crate::IdTupleGenerated; use base64::prelude::BASE64_URL_SAFE_NO_PAD; use base64::Engine; @@ -385,12 +385,12 @@ mod test { use crate::entities::generated::sys::{BucketKey, InstanceSessionKey}; use crate::entities::generated::tutanota::Mail; use crate::entities::Entity; - use crate::generated_id::GeneratedId; use crate::instance_mapper::InstanceMapper; use crate::key_loader_facade::{MockKeyLoaderFacade, VersionedAesKey}; use crate::metamodel::TypeModel; use crate::type_model_provider::init_type_model_provider; use crate::util::test_utils::{create_test_entity, typed_entity_to_parsed_entity}; + use crate::GeneratedId; use crate::IdTupleGenerated; #[tokio::test] diff --git a/tuta-sdk/rust/sdk/src/crypto_entity_client.rs b/tuta-sdk/rust/sdk/src/crypto_entity_client.rs index 8141f4d7956..1541e0e9020 100644 --- a/tuta-sdk/rust/sdk/src/crypto_entity_client.rs +++ b/tuta-sdk/rust/sdk/src/crypto_entity_client.rs @@ -5,10 +5,10 @@ use crate::entities::entity_facade::EntityFacade; use crate::entities::Entity; #[cfg_attr(test, mockall_double::double)] use crate::entity_client::EntityClient; -use crate::entity_client::IdType; -use crate::generated_id::GeneratedId; +use crate::id::id_tuple::IdType; use crate::instance_mapper::InstanceMapper; use crate::metamodel::TypeModel; +use crate::GeneratedId; use crate::{ApiCallError, ListLoadDirection}; use serde::Deserialize; use std::sync::Arc; diff --git a/tuta-sdk/rust/sdk/src/element_value.rs b/tuta-sdk/rust/sdk/src/element_value.rs index d45296c70cf..f2e19d07768 100644 --- a/tuta-sdk/rust/sdk/src/element_value.rs +++ b/tuta-sdk/rust/sdk/src/element_value.rs @@ -1,7 +1,9 @@ -use crate::custom_id::CustomId; +use crate::id::custom_id::CustomId; +use crate::id::id_tuple::IdTupleCustom; +use crate::id::id_tuple::IdTupleGenerated; +use crate::GeneratedId; + use crate::date::DateTime; -use crate::generated_id::GeneratedId; -use crate::{IdTupleCustom, IdTupleGenerated}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; diff --git a/tuta-sdk/rust/sdk/src/entity_client.rs b/tuta-sdk/rust/sdk/src/entity_client.rs index f4800a4405b..d8d161a6e65 100644 --- a/tuta-sdk/rust/sdk/src/entity_client.rs +++ b/tuta-sdk/rust/sdk/src/entity_client.rs @@ -1,23 +1,16 @@ -use std::fmt::Display; use std::sync::Arc; use crate::element_value::{ElementValue, ParsedEntity}; -use crate::generated_id::GeneratedId; +use crate::id::id_tuple::{BaseIdType, IdTupleType, IdType}; use crate::json_element::RawEntity; use crate::json_serializer::JsonSerializer; use crate::metamodel::{ElementType, TypeModel}; use crate::rest_client::{HttpMethod, RestClient, RestClientOptions}; use crate::rest_error::HttpError; use crate::type_model_provider::TypeModelProvider; +use crate::GeneratedId; use crate::{ApiCallError, HeadersProvider, ListLoadDirection, TypeRef}; -/// Denotes an ID that can be serialised into a string and used to access resources -pub trait IdType: Display + 'static {} - -/// Denotes a basic ID type such as GeneratedId or CustomID that can be serialised into a string and used to access resources -pub trait BaseIdType: Display + IdType + 'static {} -pub trait IdTupleType: Display + IdType + 'static {} - /// A high level interface to manipulate unencrypted entities/instances via the REST API pub struct EntityClient { rest_client: Arc, @@ -271,10 +264,10 @@ mod tests { use std::collections::HashMap; use super::*; - use crate::custom_id::CustomId; use crate::entities::Entity; use crate::metamodel::{Cardinality, ModelValue, ValueType}; use crate::rest_client::{MockRestClient, RestResponse}; + use crate::CustomId; use crate::{collection, str_map, IdTupleCustom, IdTupleGenerated}; use mockall::predicate::{always, eq}; use serde::{Deserialize, Serialize}; diff --git a/tuta-sdk/rust/sdk/src/id.rs b/tuta-sdk/rust/sdk/src/id.rs new file mode 100644 index 00000000000..b4c248be29e --- /dev/null +++ b/tuta-sdk/rust/sdk/src/id.rs @@ -0,0 +1,3 @@ +pub mod custom_id; +pub mod generated_id; +pub mod id_tuple; diff --git a/tuta-sdk/rust/sdk/src/custom_id.rs b/tuta-sdk/rust/sdk/src/id/custom_id.rs similarity index 97% rename from tuta-sdk/rust/sdk/src/custom_id.rs rename to tuta-sdk/rust/sdk/src/id/custom_id.rs index 2f4e3a16d85..94dd1f85442 100644 --- a/tuta-sdk/rust/sdk/src/custom_id.rs +++ b/tuta-sdk/rust/sdk/src/id/custom_id.rs @@ -1,4 +1,4 @@ -use crate::entity_client::{BaseIdType, IdType}; +use crate::id::id_tuple::{BaseIdType, IdType}; use base64::Engine; use serde::de::{Error, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; diff --git a/tuta-sdk/rust/sdk/src/generated_id.rs b/tuta-sdk/rust/sdk/src/id/generated_id.rs similarity index 98% rename from tuta-sdk/rust/sdk/src/generated_id.rs rename to tuta-sdk/rust/sdk/src/id/generated_id.rs index 2def179728b..d3b60089ca9 100644 --- a/tuta-sdk/rust/sdk/src/generated_id.rs +++ b/tuta-sdk/rust/sdk/src/id/generated_id.rs @@ -1,4 +1,4 @@ -use crate::entity_client::{BaseIdType, IdType}; +use crate::id::id_tuple::{BaseIdType, IdType}; use serde::de::{Error, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::borrow::ToOwned; diff --git a/tuta-sdk/rust/sdk/src/id/id_tuple.rs b/tuta-sdk/rust/sdk/src/id/id_tuple.rs new file mode 100644 index 00000000000..9d54a3d2ebc --- /dev/null +++ b/tuta-sdk/rust/sdk/src/id/id_tuple.rs @@ -0,0 +1,61 @@ +use crate::id::custom_id::CustomId; +use crate::GeneratedId; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; + +/// Denotes an ID that can be serialised into a string and used to access resources +pub trait IdType: Display + 'static {} + +/// Denotes a basic ID type such as GeneratedId or CustomID that can be serialised into a string and used to access resources +pub trait BaseIdType: Display + IdType + 'static {} +pub trait IdTupleType: Display + IdType + 'static {} + +/// A set of keys used to identify an element within a List Element Type +#[derive(uniffi::Record, Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +pub struct IdTupleGenerated { + pub list_id: GeneratedId, + pub element_id: GeneratedId, +} + +/// A set of keys used to identify an element within a List Element Type +#[derive(uniffi::Record, Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +pub struct IdTupleCustom { + pub list_id: GeneratedId, + pub element_id: CustomId, +} + +impl IdTupleGenerated { + #[must_use] + pub fn new(list_id: GeneratedId, element_id: GeneratedId) -> Self { + Self { + list_id, + element_id, + } + } +} + +impl IdTupleCustom { + #[must_use] + pub fn new(list_id: GeneratedId, element_id: CustomId) -> Self { + Self { + list_id, + element_id, + } + } +} +impl IdType for IdTupleGenerated {} +impl IdTupleType for IdTupleGenerated {} +impl IdType for IdTupleCustom {} +impl IdTupleType for IdTupleCustom {} + +impl Display for IdTupleGenerated { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + write!(f, "{}/{}", self.list_id, self.element_id) + } +} + +impl Display for IdTupleCustom { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + write!(f, "{}/{}", self.list_id, self.element_id) + } +} diff --git a/tuta-sdk/rust/sdk/src/instance_mapper.rs b/tuta-sdk/rust/sdk/src/instance_mapper.rs index f381e3f3f4a..52c89419730 100644 --- a/tuta-sdk/rust/sdk/src/instance_mapper.rs +++ b/tuta-sdk/rust/sdk/src/instance_mapper.rs @@ -8,12 +8,10 @@ use serde::ser::{Error, Impossible, SerializeMap, SerializeSeq, SerializeStruct} use serde::{de, ser, Deserialize, Deserializer, Serialize, Serializer}; use thiserror::Error; -use crate::custom_id::CustomId; use crate::date::DateTime; use crate::element_value::{ElementValue, ParsedEntity}; use crate::entities::Entity; -use crate::generated_id::GeneratedId; -use crate::{IdTupleCustom, IdTupleGenerated}; +use crate::{CustomId, GeneratedId, IdTupleCustom, IdTupleGenerated}; /// Converter between untyped representations of API Entities and generated structures pub struct InstanceMapper {} @@ -670,13 +668,13 @@ impl Serializer for ElementValueSerializer { T: ?Sized + Serialize, { match name { - crate::generated_id::GENERATED_ID_STRUCT_NAME => { + crate::id::generated_id::GENERATED_ID_STRUCT_NAME => { let Ok(ElementValue::String(id_string)) = value.serialize(self) else { unreachable!("should've serialized GeneratedId as a string"); }; Ok(ElementValue::IdGeneratedId(GeneratedId(id_string))) }, - crate::custom_id::CUSTOM_ID_STRUCT_NAME => { + crate::id::custom_id::CUSTOM_ID_STRUCT_NAME => { let Ok(ElementValue::String(id_string)) = value.serialize(self) else { unreachable!("should've serialized CustomId as a string"); }; @@ -1115,12 +1113,12 @@ mod tests { CalendarEventUidIndex, Mail, MailDetailsBlob, MailboxGroupRoot, OutOfOfficeNotification, OutOfOfficeNotificationRecipientList, }; - use crate::generated_id::GeneratedId; use crate::json_element::RawEntity; use crate::json_serializer::JsonSerializer; use crate::tutanota_constants::PublicKeyIdentifierType; use crate::type_model_provider::init_type_model_provider; use crate::util::test_utils::{create_test_entity, generate_random_group}; + use crate::GeneratedId; use crate::TypeRef; use std::sync::Arc; diff --git a/tuta-sdk/rust/sdk/src/json_serializer.rs b/tuta-sdk/rust/sdk/src/json_serializer.rs index e93dc6e14ce..72dab00457d 100644 --- a/tuta-sdk/rust/sdk/src/json_serializer.rs +++ b/tuta-sdk/rust/sdk/src/json_serializer.rs @@ -4,16 +4,16 @@ use std::sync::Arc; use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; use thiserror::Error; -use crate::custom_id::CustomId; use crate::date::DateTime; use crate::element_value::{ElementValue, ParsedEntity}; -use crate::generated_id::GeneratedId; use crate::json_element::{JsonElement, RawEntity}; use crate::json_serializer::InstanceMapperError::InvalidValue; use crate::metamodel::{ AssociationType, Cardinality, ElementType, ModelValue, TypeModel, ValueType, }; use crate::type_model_provider::TypeModelProvider; +use crate::CustomId; +use crate::GeneratedId; use crate::{IdTupleCustom, IdTupleGenerated, TypeRef}; impl From<&TypeModel> for TypeRef { diff --git a/tuta-sdk/rust/sdk/src/key_cache.rs b/tuta-sdk/rust/sdk/src/key_cache.rs index 1f7836c201f..0721325f3e1 100644 --- a/tuta-sdk/rust/sdk/src/key_cache.rs +++ b/tuta-sdk/rust/sdk/src/key_cache.rs @@ -1,7 +1,7 @@ use crate::crypto::Aes256Key; use crate::entities::generated::sys::User; -use crate::generated_id::GeneratedId; use crate::key_loader_facade::VersionedAesKey; +use crate::GeneratedId; use std::collections::HashMap; use std::sync::RwLock; diff --git a/tuta-sdk/rust/sdk/src/key_loader_facade.rs b/tuta-sdk/rust/sdk/src/key_loader_facade.rs index 7e663d349a0..3b4d4f4af3c 100644 --- a/tuta-sdk/rust/sdk/src/key_loader_facade.rs +++ b/tuta-sdk/rust/sdk/src/key_loader_facade.rs @@ -1,13 +1,13 @@ use crate::crypto::key::{AsymmetricKeyPair, GenericAesKey, KeyLoadError}; use crate::crypto::key_encryption::decrypt_key_pair; -use crate::custom_id::CustomId; use crate::entities::generated::sys::{Group, GroupKey}; -use crate::generated_id::GeneratedId; #[cfg_attr(test, mockall_double::double)] use crate::typed_entity_client::TypedEntityClient; #[cfg_attr(test, mockall_double::double)] use crate::user_facade::UserFacade; use crate::util::Versioned; +use crate::CustomId; +use crate::GeneratedId; use crate::ListLoadDirection; use futures::future::BoxFuture; use std::cmp::Ordering; @@ -276,13 +276,13 @@ mod tests { use crate::crypto::randomizer_facade::test_util::make_thread_rng_facade; use crate::crypto::randomizer_facade::RandomizerFacade; use crate::crypto::{aes::Iv, Aes256Key, PQKeyPairs}; - use crate::custom_id::CustomId; use crate::entities::generated::sys::{GroupKeysRef, GroupMembership, KeyPair}; use crate::key_cache::MockKeyCache; use crate::typed_entity_client::MockTypedEntityClient; use crate::user_facade::MockUserFacade; use crate::util::get_vec_reversed; use crate::util::test_utils::{generate_random_group, random_aes256_key}; + use crate::CustomId; use crate::{IdTupleCustom, IdTupleGenerated}; use mockall::predicate; use std::array::from_fn; diff --git a/tuta-sdk/rust/sdk/src/lib.rs b/tuta-sdk/rust/sdk/src/lib.rs index 50898e3a08a..a1a3b963685 100644 --- a/tuta-sdk/rust/sdk/src/lib.rs +++ b/tuta-sdk/rust/sdk/src/lib.rs @@ -7,7 +7,6 @@ use std::sync::Arc; use minicbor::encode::Write; use minicbor::{Encode, Encoder}; -use serde::{Deserialize, Serialize}; use thiserror::Error; #[cfg_attr(test, mockall_double::double)] @@ -21,7 +20,6 @@ use crate::crypto::randomizer_facade::RandomizerFacade; use crate::crypto::{aes::Iv, Aes256Key}; #[cfg_attr(test, mockall_double::double)] use crate::crypto_entity_client::CryptoEntityClient; -use crate::custom_id::CustomId; use crate::date::date_provider::SystemDateProvider; use crate::element_value::ElementValue; use crate::entities::entity_facade::EntityFacadeImpl; @@ -29,8 +27,6 @@ use crate::entities::generated::sys::{CreateSessionData, SaltData}; use crate::entities::generated::tutanota::Mail; #[cfg_attr(test, mockall_double::double)] use crate::entity_client::EntityClient; -use crate::entity_client::{IdTupleType, IdType}; -use crate::generated_id::GeneratedId; use crate::instance_mapper::InstanceMapper; use crate::json_serializer::{InstanceMapperError, JsonSerializer}; #[cfg_attr(test, mockall_double::double)] @@ -55,13 +51,11 @@ use rest_client::RestClientError; pub mod crypto; mod crypto_entity_client; -pub mod custom_id; pub mod date; mod element_value; pub mod entities; mod entity_client; pub mod folder_system; -pub mod generated_id; mod groups; mod instance_mapper; mod json_element; @@ -74,6 +68,7 @@ mod mail_facade; mod metamodel; mod blobs; +mod id; #[cfg(feature = "net")] pub mod net; pub mod rest_client; @@ -86,6 +81,11 @@ mod typed_entity_client; mod user_facade; mod util; +pub use id::custom_id::CustomId; +pub use id::generated_id::GeneratedId; +pub use id::id_tuple::IdTupleCustom; +pub use id::id_tuple::IdTupleGenerated; + pub static CLIENT_VERSION: &str = env!("CARGO_PKG_VERSION"); uniffi::setup_scaffolding!(); @@ -372,56 +372,6 @@ pub enum ListLoadDirection { DESC, } -/// A set of keys used to identify an element within a List Element Type -#[derive(uniffi::Record, Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] -pub struct IdTupleGenerated { - pub list_id: GeneratedId, - pub element_id: GeneratedId, -} - -/// A set of keys used to identify an element within a List Element Type -#[derive(uniffi::Record, Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] -pub struct IdTupleCustom { - pub list_id: GeneratedId, - pub element_id: CustomId, -} - -impl IdTupleGenerated { - #[must_use] - pub fn new(list_id: GeneratedId, element_id: GeneratedId) -> Self { - Self { - list_id, - element_id, - } - } -} - -impl IdTupleCustom { - #[must_use] - pub fn new(list_id: GeneratedId, element_id: CustomId) -> Self { - Self { - list_id, - element_id, - } - } -} -impl IdType for IdTupleGenerated {} -impl IdTupleType for IdTupleGenerated {} -impl IdType for IdTupleCustom {} -impl IdTupleType for IdTupleCustom {} - -impl Display for IdTupleGenerated { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - write!(f, "{}/{}", self.list_id, self.element_id) - } -} - -impl Display for IdTupleCustom { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - write!(f, "{}/{}", self.list_id, self.element_id) - } -} - /// Contains an error from the SDK to be handled by the consuming code over the FFI #[derive(Error, Debug, uniffi::Error, Eq, PartialEq, Clone)] pub enum ApiCallError { diff --git a/tuta-sdk/rust/sdk/src/login/credentials.rs b/tuta-sdk/rust/sdk/src/login/credentials.rs index c087f85252b..3b9d9a59541 100644 --- a/tuta-sdk/rust/sdk/src/login/credentials.rs +++ b/tuta-sdk/rust/sdk/src/login/credentials.rs @@ -1,4 +1,4 @@ -use crate::generated_id::GeneratedId; +use crate::GeneratedId; #[derive(uniffi::Record, Clone)] pub struct Credentials { diff --git a/tuta-sdk/rust/sdk/src/login/login_facade.rs b/tuta-sdk/rust/sdk/src/login/login_facade.rs index c04feb7290e..01a8933b40f 100644 --- a/tuta-sdk/rust/sdk/src/login/login_facade.rs +++ b/tuta-sdk/rust/sdk/src/login/login_facade.rs @@ -6,13 +6,12 @@ use thiserror::Error; use crate::crypto::key::GenericAesKey; use crate::crypto::{generate_key_from_passphrase, sha256, Aes256Key}; -use crate::custom_id::CustomId; use crate::element_value::ParsedEntity; use crate::entities::generated::sys::{Session, User}; use crate::entities::Entity; #[cfg_attr(test, mockall_double::double)] use crate::entity_client::EntityClient; -use crate::generated_id::{GeneratedId, GENERATED_ID_BYTES_LENGTH}; +use crate::id::generated_id::GENERATED_ID_BYTES_LENGTH; use crate::login::credentials::Credentials; #[cfg_attr(test, mockall_double::double)] use crate::typed_entity_client::TypedEntityClient; @@ -20,6 +19,8 @@ use crate::typed_entity_client::TypedEntityClient; use crate::user_facade::UserFacade; use crate::util::{array_cast_slice, BASE64_EXT}; use crate::ApiCallError::InternalSdkError; +use crate::CustomId; +use crate::GeneratedId; use crate::{ApiCallError, IdTupleCustom}; /// Error that may occur during login and session creation @@ -203,16 +204,16 @@ mod tests { use crate::crypto::key::GenericAesKey; use crate::crypto::randomizer_facade::RandomizerFacade; use crate::crypto::{aes::Iv, Aes128Key, Aes256Key}; - use crate::custom_id::CustomId; use crate::entities::generated::sys::{GroupMembership, Session, User, UserExternalAuthInfo}; use crate::entities::Entity; use crate::entity_client::MockEntityClient; - use crate::generated_id::GeneratedId; use crate::login::credentials::{CredentialType, Credentials}; use crate::login::login_facade::LoginFacade; use crate::typed_entity_client::MockTypedEntityClient; use crate::user_facade::MockUserFacade; use crate::util::test_utils::{create_test_entity, typed_entity_to_parsed_entity}; + use crate::CustomId; + use crate::GeneratedId; use crate::{IdTupleCustom, IdTupleGenerated}; #[tokio::test] diff --git a/tuta-sdk/rust/sdk/src/mail_facade.rs b/tuta-sdk/rust/sdk/src/mail_facade.rs index 6033a2f62f7..70c6a63b1f9 100644 --- a/tuta-sdk/rust/sdk/src/mail_facade.rs +++ b/tuta-sdk/rust/sdk/src/mail_facade.rs @@ -4,14 +4,15 @@ use crate::entities::generated::tutanota::{ Mail, MailBox, MailFolder, MailboxGroupRoot, SimpleMoveMailPostIn, UnreadMailStatePostIn, }; use crate::folder_system::{FolderSystem, MailSetKind}; -use crate::generated_id::GeneratedId; use crate::groups::GroupType; +use crate::id::id_tuple::IdTupleGenerated; use crate::services::generated::tutanota::{SimpleMoveMailService, UnreadMailStateService}; #[cfg_attr(test, mockall_double::double)] use crate::services::service_executor::ResolvingServiceExecutor; #[cfg_attr(test, mockall_double::double)] use crate::user_facade::UserFacade; -use crate::{ApiCallError, IdTupleGenerated, ListLoadDirection}; +use crate::GeneratedId; +use crate::{ApiCallError, ListLoadDirection}; use std::sync::Arc; /// Provides high level functions to manipulate mail entities via the REST API @@ -186,12 +187,12 @@ mod tests { use crate::crypto_entity_client::MockCryptoEntityClient; use crate::entities::generated::tutanota::SimpleMoveMailPostIn; use crate::folder_system::MailSetKind; - use crate::generated_id::GeneratedId; use crate::mail_facade::MailFacade; use crate::services::generated::tutanota::SimpleMoveMailService; use crate::services::generated::tutanota::UnreadMailStateService; use crate::services::service_executor::MockResolvingServiceExecutor; use crate::user_facade::MockUserFacade; + use crate::GeneratedId; use crate::IdTupleGenerated; use mockall::predicate::{always, eq}; use std::sync::Arc; diff --git a/tuta-sdk/rust/sdk/src/typed_entity_client.rs b/tuta-sdk/rust/sdk/src/typed_entity_client.rs index cec3268af42..ab2835f2a90 100644 --- a/tuta-sdk/rust/sdk/src/typed_entity_client.rs +++ b/tuta-sdk/rust/sdk/src/typed_entity_client.rs @@ -1,10 +1,10 @@ use crate::entities::Entity; #[cfg_attr(test, mockall_double::double)] use crate::entity_client::EntityClient; -use crate::entity_client::{BaseIdType, IdType}; -use crate::generated_id::GeneratedId; +use crate::id::id_tuple::{BaseIdType, IdType}; use crate::instance_mapper::InstanceMapper; use crate::metamodel::{ElementType, TypeModel}; +use crate::GeneratedId; use crate::{ApiCallError, ListLoadDirection}; use serde::Deserialize; use std::sync::Arc; diff --git a/tuta-sdk/rust/sdk/src/user_facade.rs b/tuta-sdk/rust/sdk/src/user_facade.rs index 8172eefee96..e8a51b874c3 100644 --- a/tuta-sdk/rust/sdk/src/user_facade.rs +++ b/tuta-sdk/rust/sdk/src/user_facade.rs @@ -3,13 +3,13 @@ use crate::crypto::key::GenericAesKey; use crate::crypto::sha256; use crate::crypto::{Aes256Key, AES_256_KEY_SIZE}; use crate::entities::generated::sys::{GroupMembership, User}; -use crate::generated_id::GeneratedId; use crate::groups::GroupType; #[cfg_attr(test, mockall_double::double)] use crate::key_cache::KeyCache; use crate::key_loader_facade::VersionedAesKey; use crate::util::Versioned; use crate::ApiCallError; +use crate::GeneratedId; use base64::prelude::BASE64_STANDARD; use base64::Engine; use std::borrow::ToOwned; diff --git a/tuta-sdk/rust/sdk/src/util/entity_test_utils.rs b/tuta-sdk/rust/sdk/src/util/entity_test_utils.rs index 15fa6f4a108..5bc80ae9403 100644 --- a/tuta-sdk/rust/sdk/src/util/entity_test_utils.rs +++ b/tuta-sdk/rust/sdk/src/util/entity_test_utils.rs @@ -4,9 +4,9 @@ use crate::date::DateTime; use crate::element_value::{ElementValue, ParsedEntity}; use crate::entities::generated::tutanota::{Mail, MailAddress}; use crate::entities::Entity; -use crate::generated_id::GeneratedId; use crate::type_model_provider::{init_type_model_provider, TypeModelProvider}; use crate::util::test_utils::{create_test_entity, typed_entity_to_parsed_entity}; +use crate::GeneratedId; use crate::{IdTupleGenerated, TypeRef}; /// Generates and returns an encrypted Mail ParsedEntity. It also returns the decrypted Mail for comparison diff --git a/tuta-sdk/rust/sdk/src/util/test_utils.rs b/tuta-sdk/rust/sdk/src/util/test_utils.rs index bb7d2eff55e..7f14f451b34 100644 --- a/tuta-sdk/rust/sdk/src/util/test_utils.rs +++ b/tuta-sdk/rust/sdk/src/util/test_utils.rs @@ -5,18 +5,18 @@ use rand::random; use crate::crypto::crypto_facade::CryptoProtocolVersion; use crate::crypto::randomizer_facade::test_util::make_thread_rng_facade; use crate::crypto::Aes256Key; -use crate::custom_id::CustomId; use crate::element_value::{ElementValue, ParsedEntity}; use crate::entities::generated::sys::{ ArchiveRef, ArchiveType, Group, GroupKeysRef, KeyPair, PubEncKeyData, TypeInfo, }; use crate::entities::Entity; -use crate::generated_id::GeneratedId; use crate::instance_mapper::InstanceMapper; use crate::metamodel::ElementType::Aggregated; use crate::metamodel::{AssociationType, Cardinality, ElementType, ValueType}; use crate::tutanota_constants::PublicKeyIdentifierType; use crate::type_model_provider::{init_type_model_provider, TypeModelProvider}; +use crate::CustomId; +use crate::GeneratedId; use crate::{IdTupleCustom, IdTupleGenerated}; /// Generates a URL-safe random string of length `Size`. diff --git a/tuta-sdk/rust/sdk/tests/download_mail_test.rs b/tuta-sdk/rust/sdk/tests/download_mail_test.rs index 0b259c86300..b8674ff6a68 100644 --- a/tuta-sdk/rust/sdk/tests/download_mail_test.rs +++ b/tuta-sdk/rust/sdk/tests/download_mail_test.rs @@ -5,9 +5,9 @@ mod tests { use base64::prelude::BASE64_STANDARD; use base64::Engine; use std::sync::Arc; - use tutasdk::generated_id::GeneratedId; use tutasdk::login::{CredentialType, Credentials}; use tutasdk::rest_client::{HttpMethod, RestClient}; + use tutasdk::GeneratedId; use tutasdk::{IdTupleGenerated, Sdk}; #[tokio::test] From 7dc10d87d5e8b827db09c976d966f395e2293534 Mon Sep 17 00:00:00 2001 From: nig Date: Thu, 7 Nov 2024 16:48:12 +0100 Subject: [PATCH 02/32] add tutanota model V77 --- src/common/api/entities/tutanota/ModelInfo.ts | 2 +- src/common/api/entities/tutanota/Services.ts | 11 + .../api/entities/tutanota/TypeModels.js | 694 +++++++++++++++--- src/common/api/entities/tutanota/TypeRefs.ts | 106 +++ .../sdk/src/entities/generated/tutanota.rs | 128 ++++ .../sdk/src/services/generated/tutanota.rs | 51 +- .../rust/sdk/src/type_models/tutanota.json | 694 +++++++++++++++--- 7 files changed, 1429 insertions(+), 257 deletions(-) diff --git a/src/common/api/entities/tutanota/ModelInfo.ts b/src/common/api/entities/tutanota/ModelInfo.ts index 32080d1675c..74c36142193 100644 --- a/src/common/api/entities/tutanota/ModelInfo.ts +++ b/src/common/api/entities/tutanota/ModelInfo.ts @@ -1,5 +1,5 @@ const modelInfo = { - version: 76, + version: 77, compatibleSince: 76, } diff --git a/src/common/api/entities/tutanota/Services.ts b/src/common/api/entities/tutanota/Services.ts index 47567f22817..75b41080e99 100644 --- a/src/common/api/entities/tutanota/Services.ts +++ b/src/common/api/entities/tutanota/Services.ts @@ -14,6 +14,8 @@ import {GroupInvitationPostDataTypeRef} from "./TypeRefs.js" import {GroupInvitationPostReturnTypeRef} from "./TypeRefs.js" import {GroupInvitationPutDataTypeRef} from "./TypeRefs.js" import {GroupInvitationDeleteDataTypeRef} from "./TypeRefs.js" +import {ImportMailPostInTypeRef} from "./TypeRefs.js" +import {ImportMailPostOutTypeRef} from "./TypeRefs.js" import {ListUnsubscribeDataTypeRef} from "./TypeRefs.js" import {CreateMailFolderDataTypeRef} from "./TypeRefs.js" import {CreateMailFolderReturnTypeRef} from "./TypeRefs.js" @@ -107,6 +109,15 @@ export const GroupInvitationService = Object.freeze({ delete: {data: GroupInvitationDeleteDataTypeRef, return: null}, } as const) +export const ImportMailService = Object.freeze({ + app: "tutanota", + name: "ImportMailService", + get: null, + post: {data: ImportMailPostInTypeRef, return: ImportMailPostOutTypeRef}, + put: null, + delete: null, +} as const) + export const ListUnsubscribeService = Object.freeze({ app: "tutanota", name: "ListUnsubscribeService", diff --git a/src/common/api/entities/tutanota/TypeModels.js b/src/common/api/entities/tutanota/TypeModels.js index 7a37b906134..4094f4e1e4c 100644 --- a/src/common/api/entities/tutanota/TypeModels.js +++ b/src/common/api/entities/tutanota/TypeModels.js @@ -56,7 +56,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "Birthday": { "name": "Birthday", @@ -106,7 +106,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "Body": { "name": "Body", @@ -147,7 +147,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "CalendarDeleteData": { "name": "CalendarDeleteData", @@ -181,7 +181,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CalendarEvent": { "name": "CalendarEvent", @@ -380,7 +380,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CalendarEventAttendee": { "name": "CalendarEventAttendee", @@ -423,7 +423,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CalendarEventIndexRef": { "name": "CalendarEventIndexRef", @@ -457,7 +457,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CalendarEventUidIndex": { "name": "CalendarEventUidIndex", @@ -528,7 +528,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CalendarEventUpdate": { "name": "CalendarEventUpdate", @@ -616,7 +616,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CalendarEventUpdateList": { "name": "CalendarEventUpdateList", @@ -650,7 +650,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CalendarGroupRoot": { "name": "CalendarGroupRoot", @@ -749,7 +749,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CalendarRepeatRule": { "name": "CalendarRepeatRule", @@ -828,7 +828,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "Contact": { "name": "Contact", @@ -1151,7 +1151,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactAddress": { "name": "ContactAddress", @@ -1201,7 +1201,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactCustomDate": { "name": "ContactCustomDate", @@ -1251,7 +1251,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactList": { "name": "ContactList", @@ -1340,7 +1340,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactListEntry": { "name": "ContactListEntry", @@ -1417,7 +1417,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactListGroupRoot": { "name": "ContactListGroupRoot", @@ -1496,7 +1496,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactMailAddress": { "name": "ContactMailAddress", @@ -1546,7 +1546,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactMessengerHandle": { "name": "ContactMessengerHandle", @@ -1596,7 +1596,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactPhoneNumber": { "name": "ContactPhoneNumber", @@ -1646,7 +1646,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactPronouns": { "name": "ContactPronouns", @@ -1687,7 +1687,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactRelationship": { "name": "ContactRelationship", @@ -1737,7 +1737,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactSocialId": { "name": "ContactSocialId", @@ -1787,7 +1787,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactWebsite": { "name": "ContactWebsite", @@ -1837,7 +1837,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ConversationEntry": { "name": "ConversationEntry", @@ -1926,7 +1926,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CreateExternalUserGroupData": { "name": "CreateExternalUserGroupData", @@ -1985,7 +1985,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "CreateGroupPostReturn": { "name": "CreateGroupPostReturn", @@ -2019,7 +2019,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CreateMailFolderData": { "name": "CreateMailFolderData", @@ -2089,7 +2089,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CreateMailFolderReturn": { "name": "CreateMailFolderReturn", @@ -2123,7 +2123,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CreateMailGroupData": { "name": "CreateMailGroupData", @@ -2184,7 +2184,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CustomerAccountCreateData": { "name": "CustomerAccountCreateData", @@ -2356,7 +2356,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "DefaultAlarmInfo": { "name": "DefaultAlarmInfo", @@ -2388,7 +2388,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "DeleteGroupData": { "name": "DeleteGroupData", @@ -2431,7 +2431,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "DeleteMailData": { "name": "DeleteMailData", @@ -2475,7 +2475,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "DeleteMailFolderData": { "name": "DeleteMailFolderData", @@ -2509,7 +2509,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "DraftAttachment": { "name": "DraftAttachment", @@ -2571,7 +2571,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "DraftCreateData": { "name": "DraftCreateData", @@ -2641,7 +2641,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "DraftCreateReturn": { "name": "DraftCreateReturn", @@ -2675,7 +2675,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "DraftData": { "name": "DraftData", @@ -2822,7 +2822,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "DraftRecipient": { "name": "DraftRecipient", @@ -2863,7 +2863,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "DraftUpdateData": { "name": "DraftUpdateData", @@ -2907,7 +2907,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "DraftUpdateReturn": { "name": "DraftUpdateReturn", @@ -2941,7 +2941,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "EmailTemplate": { "name": "EmailTemplate", @@ -3038,7 +3038,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "EmailTemplateContent": { "name": "EmailTemplateContent", @@ -3079,7 +3079,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "EncryptTutanotaPropertiesData": { "name": "EncryptTutanotaPropertiesData", @@ -3131,7 +3131,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "EncryptedMailAddress": { "name": "EncryptedMailAddress", @@ -3172,7 +3172,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "EntropyData": { "name": "EntropyData", @@ -3213,7 +3213,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ExternalUserData": { "name": "ExternalUserData", @@ -3346,7 +3346,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "File": { "name": "File", @@ -3481,7 +3481,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "FileSystem": { "name": "FileSystem", @@ -3560,7 +3560,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "GroupInvitationDeleteData": { "name": "GroupInvitationDeleteData", @@ -3594,7 +3594,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "GroupInvitationPostData": { "name": "GroupInvitationPostData", @@ -3638,7 +3638,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "GroupInvitationPostReturn": { "name": "GroupInvitationPostReturn", @@ -3692,7 +3692,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "GroupInvitationPutData": { "name": "GroupInvitationPutData", @@ -3762,7 +3762,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "GroupSettings": { "name": "GroupSettings", @@ -3833,7 +3833,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "Header": { "name": "Header", @@ -3874,7 +3874,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ImapFolder": { "name": "ImapFolder", @@ -3935,7 +3935,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "ImapSyncConfiguration": { "name": "ImapSyncConfiguration", @@ -4005,7 +4005,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "ImapSyncState": { "name": "ImapSyncState", @@ -4066,7 +4066,388 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" + }, + "ImportAttachment": { + "name": "ImportAttachment", + "since": 77, + "type": "AGGREGATED_TYPE", + "id": 1490, + "rootId": "CHR1dGFub3RhAAXS", + "versioned": false, + "encrypted": false, + "values": { + "_id": { + "final": true, + "name": "_id", + "id": 1491, + "since": 77, + "type": "CustomId", + "cardinality": "One", + "encrypted": false + }, + "ownerEncFileSessionKey": { + "final": true, + "name": "ownerEncFileSessionKey", + "id": 1492, + "since": 77, + "type": "Bytes", + "cardinality": "One", + "encrypted": false + } + }, + "associations": { + "existingFile": { + "final": true, + "name": "existingFile", + "id": 1494, + "since": 77, + "type": "LIST_ELEMENT_ASSOCIATION_GENERATED", + "cardinality": "ZeroOrOne", + "refType": "File", + "dependency": null + }, + "newFile": { + "final": true, + "name": "newFile", + "id": 1493, + "since": 77, + "type": "AGGREGATION", + "cardinality": "ZeroOrOne", + "refType": "NewImportAttachment", + "dependency": null + } + }, + "app": "tutanota", + "version": "77" + }, + "ImportMailData": { + "name": "ImportMailData", + "since": 77, + "type": "AGGREGATED_TYPE", + "id": 1495, + "rootId": "CHR1dGFub3RhAAXX", + "versioned": false, + "encrypted": false, + "values": { + "_id": { + "final": true, + "name": "_id", + "id": 1496, + "since": 77, + "type": "CustomId", + "cardinality": "One", + "encrypted": false + }, + "compressedBodyText": { + "final": true, + "name": "compressedBodyText", + "id": 1498, + "since": 77, + "type": "CompressedString", + "cardinality": "One", + "encrypted": true + }, + "compressedHeaders": { + "final": true, + "name": "compressedHeaders", + "id": 1511, + "since": 77, + "type": "CompressedString", + "cardinality": "One", + "encrypted": true + }, + "confidential": { + "final": true, + "name": "confidential", + "id": 1506, + "since": 77, + "type": "Boolean", + "cardinality": "One", + "encrypted": true + }, + "date": { + "final": true, + "name": "date", + "id": 1499, + "since": 77, + "type": "Date", + "cardinality": "One", + "encrypted": false + }, + "differentEnvelopeSender": { + "final": true, + "name": "differentEnvelopeSender", + "id": 1509, + "since": 77, + "type": "String", + "cardinality": "ZeroOrOne", + "encrypted": true + }, + "inReplyTo": { + "final": true, + "name": "inReplyTo", + "id": 1503, + "since": 77, + "type": "String", + "cardinality": "ZeroOrOne", + "encrypted": false + }, + "messageId": { + "final": true, + "name": "messageId", + "id": 1502, + "since": 77, + "type": "String", + "cardinality": "ZeroOrOne", + "encrypted": false + }, + "method": { + "final": true, + "name": "method", + "id": 1507, + "since": 77, + "type": "Number", + "cardinality": "One", + "encrypted": true + }, + "phishingStatus": { + "final": true, + "name": "phishingStatus", + "id": 1510, + "since": 77, + "type": "Number", + "cardinality": "One", + "encrypted": false + }, + "replyType": { + "final": false, + "name": "replyType", + "id": 1508, + "since": 77, + "type": "Number", + "cardinality": "One", + "encrypted": true + }, + "state": { + "final": true, + "name": "state", + "id": 1500, + "since": 77, + "type": "Number", + "cardinality": "One", + "encrypted": false + }, + "subject": { + "final": true, + "name": "subject", + "id": 1497, + "since": 77, + "type": "String", + "cardinality": "One", + "encrypted": true + }, + "unread": { + "final": true, + "name": "unread", + "id": 1501, + "since": 77, + "type": "Boolean", + "cardinality": "One", + "encrypted": false + } + }, + "associations": { + "importedAttachments": { + "final": true, + "name": "importedAttachments", + "id": 1514, + "since": 77, + "type": "AGGREGATION", + "cardinality": "Any", + "refType": "ImportAttachment", + "dependency": null + }, + "recipients": { + "final": true, + "name": "recipients", + "id": 1513, + "since": 77, + "type": "AGGREGATION", + "cardinality": "One", + "refType": "Recipients", + "dependency": null + }, + "references": { + "final": true, + "name": "references", + "id": 1504, + "since": 77, + "type": "AGGREGATION", + "cardinality": "Any", + "refType": "ImportMailDataMailReference", + "dependency": null + }, + "replyTos": { + "final": false, + "name": "replyTos", + "id": 1512, + "since": 77, + "type": "AGGREGATION", + "cardinality": "Any", + "refType": "EncryptedMailAddress", + "dependency": null + }, + "sender": { + "final": true, + "name": "sender", + "id": 1505, + "since": 77, + "type": "AGGREGATION", + "cardinality": "One", + "refType": "MailAddress", + "dependency": null + } + }, + "app": "tutanota", + "version": "77" + }, + "ImportMailDataMailReference": { + "name": "ImportMailDataMailReference", + "since": 77, + "type": "AGGREGATED_TYPE", + "id": 1479, + "rootId": "CHR1dGFub3RhAAXH", + "versioned": false, + "encrypted": false, + "values": { + "_id": { + "final": true, + "name": "_id", + "id": 1480, + "since": 77, + "type": "CustomId", + "cardinality": "One", + "encrypted": false + }, + "reference": { + "final": false, + "name": "reference", + "id": 1481, + "since": 77, + "type": "String", + "cardinality": "One", + "encrypted": false + } + }, + "associations": {}, + "app": "tutanota", + "version": "77" + }, + "ImportMailPostIn": { + "name": "ImportMailPostIn", + "since": 77, + "type": "DATA_TRANSFER_TYPE", + "id": 1515, + "rootId": "CHR1dGFub3RhAAXr", + "versioned": false, + "encrypted": true, + "values": { + "_format": { + "final": false, + "name": "_format", + "id": 1516, + "since": 77, + "type": "Number", + "cardinality": "One", + "encrypted": false + }, + "ownerEncSessionKey": { + "final": false, + "name": "ownerEncSessionKey", + "id": 1518, + "since": 77, + "type": "Bytes", + "cardinality": "One", + "encrypted": false + }, + "ownerGroup": { + "final": false, + "name": "ownerGroup", + "id": 1517, + "since": 77, + "type": "GeneratedId", + "cardinality": "One", + "encrypted": false + }, + "ownerKeyVersion": { + "final": false, + "name": "ownerKeyVersion", + "id": 1519, + "since": 77, + "type": "Number", + "cardinality": "One", + "encrypted": false + } + }, + "associations": { + "imports": { + "final": false, + "name": "imports", + "id": 1521, + "since": 77, + "type": "AGGREGATION", + "cardinality": "Any", + "refType": "ImportMailData", + "dependency": null + }, + "targetMailFolder": { + "final": true, + "name": "targetMailFolder", + "id": 1520, + "since": 77, + "type": "LIST_ELEMENT_ASSOCIATION_GENERATED", + "cardinality": "One", + "refType": "MailFolder", + "dependency": null + } + }, + "app": "tutanota", + "version": "77" + }, + "ImportMailPostOut": { + "name": "ImportMailPostOut", + "since": 77, + "type": "DATA_TRANSFER_TYPE", + "id": 1522, + "rootId": "CHR1dGFub3RhAAXy", + "versioned": false, + "encrypted": false, + "values": { + "_format": { + "final": false, + "name": "_format", + "id": 1523, + "since": 77, + "type": "Number", + "cardinality": "One", + "encrypted": false + } + }, + "associations": { + "mails": { + "final": false, + "name": "mails", + "id": 1524, + "since": 77, + "type": "LIST_ELEMENT_ASSOCIATION_GENERATED", + "cardinality": "Any", + "refType": "Mail", + "dependency": null + } + }, + "app": "tutanota", + "version": "77" }, "InboxRule": { "name": "InboxRule", @@ -4118,7 +4499,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "InternalGroupData": { "name": "InternalGroupData", @@ -4242,7 +4623,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "InternalRecipientKeyData": { "name": "InternalRecipientKeyData", @@ -4310,7 +4691,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "KnowledgeBaseEntry": { "name": "KnowledgeBaseEntry", @@ -4407,7 +4788,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "KnowledgeBaseEntryKeyword": { "name": "KnowledgeBaseEntryKeyword", @@ -4439,7 +4820,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ListUnsubscribeData": { "name": "ListUnsubscribeData", @@ -4491,7 +4872,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "Mail": { "name": "Mail", @@ -4766,7 +5147,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailAddress": { "name": "MailAddress", @@ -4818,7 +5199,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailAddressProperties": { "name": "MailAddressProperties", @@ -4859,7 +5240,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "MailBag": { "name": "MailBag", @@ -4893,7 +5274,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailBox": { "name": "MailBox", @@ -5041,7 +5422,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailDetails": { "name": "MailDetails", @@ -5123,7 +5504,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailDetailsBlob": { "name": "MailDetailsBlob", @@ -5202,7 +5583,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailDetailsDraft": { "name": "MailDetailsDraft", @@ -5281,7 +5662,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailDetailsDraftsRef": { "name": "MailDetailsDraftsRef", @@ -5315,7 +5696,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailFolder": { "name": "MailFolder", @@ -5450,7 +5831,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailFolderRef": { "name": "MailFolderRef", @@ -5484,7 +5865,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailSetEntry": { "name": "MailSetEntry", @@ -5545,7 +5926,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailboxGroupRoot": { "name": "MailboxGroupRoot", @@ -5656,7 +6037,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailboxProperties": { "name": "MailboxProperties", @@ -5744,7 +6125,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailboxServerProperties": { "name": "MailboxServerProperties", @@ -5803,7 +6184,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "MoveMailData": { "name": "MoveMailData", @@ -5857,7 +6238,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "NewDraftAttachment": { "name": "NewDraftAttachment", @@ -5918,7 +6299,86 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" + }, + "NewImportAttachment": { + "name": "NewImportAttachment", + "since": 77, + "type": "AGGREGATED_TYPE", + "id": 1482, + "rootId": "CHR1dGFub3RhAAXK", + "versioned": false, + "encrypted": false, + "values": { + "_id": { + "final": true, + "name": "_id", + "id": 1483, + "since": 77, + "type": "CustomId", + "cardinality": "One", + "encrypted": false + }, + "encCid": { + "final": true, + "name": "encCid", + "id": 1488, + "since": 77, + "type": "Bytes", + "cardinality": "ZeroOrOne", + "encrypted": false + }, + "encFileHash": { + "final": true, + "name": "encFileHash", + "id": 1485, + "since": 77, + "type": "Bytes", + "cardinality": "ZeroOrOne", + "encrypted": false + }, + "encFileName": { + "final": true, + "name": "encFileName", + "id": 1486, + "since": 77, + "type": "Bytes", + "cardinality": "One", + "encrypted": false + }, + "encMimeType": { + "final": true, + "name": "encMimeType", + "id": 1487, + "since": 77, + "type": "Bytes", + "cardinality": "One", + "encrypted": false + }, + "ownerEncFileHashSessionKey": { + "final": true, + "name": "ownerEncFileHashSessionKey", + "id": 1484, + "since": 77, + "type": "Bytes", + "cardinality": "ZeroOrOne", + "encrypted": false + } + }, + "associations": { + "referenceTokens": { + "final": true, + "name": "referenceTokens", + "id": 1489, + "since": 77, + "type": "AGGREGATION", + "cardinality": "Any", + "refType": "BlobReferenceTokenWrapper", + "dependency": "sys" + } + }, + "app": "tutanota", + "version": "77" }, "NewsId": { "name": "NewsId", @@ -5959,7 +6419,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "NewsIn": { "name": "NewsIn", @@ -5991,7 +6451,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "NewsOut": { "name": "NewsOut", @@ -6025,7 +6485,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "NotificationMail": { "name": "NotificationMail", @@ -6093,7 +6553,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "OutOfOfficeNotification": { "name": "OutOfOfficeNotification", @@ -6181,7 +6641,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "OutOfOfficeNotificationMessage": { "name": "OutOfOfficeNotificationMessage", @@ -6231,7 +6691,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "OutOfOfficeNotificationRecipientList": { "name": "OutOfOfficeNotificationRecipientList", @@ -6265,7 +6725,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "PhishingMarkerWebsocketData": { "name": "PhishingMarkerWebsocketData", @@ -6308,7 +6768,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "PhotosRef": { "name": "PhotosRef", @@ -6342,7 +6802,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "ReceiveInfoServiceData": { "name": "ReceiveInfoServiceData", @@ -6374,7 +6834,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "Recipients": { "name": "Recipients", @@ -6428,7 +6888,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "RemoteImapSyncInfo": { "name": "RemoteImapSyncInfo", @@ -6498,7 +6958,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "ReportMailPostData": { "name": "ReportMailPostData", @@ -6550,7 +7010,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "ReportedMailFieldMarker": { "name": "ReportedMailFieldMarker", @@ -6591,7 +7051,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "SecureExternalRecipientKeyData": { "name": "SecureExternalRecipientKeyData", @@ -6695,7 +7155,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "SendDraftData": { "name": "SendDraftData", @@ -6832,7 +7292,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "SendDraftReturn": { "name": "SendDraftReturn", @@ -6894,7 +7354,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "SharedGroupData": { "name": "SharedGroupData", @@ -6998,7 +7458,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "SimpleMoveMailPostIn": { "name": "SimpleMoveMailPostIn", @@ -7041,7 +7501,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "SpamResults": { "name": "SpamResults", @@ -7075,7 +7535,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "Subfiles": { "name": "Subfiles", @@ -7109,7 +7569,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "SymEncInternalRecipientKeyData": { "name": "SymEncInternalRecipientKeyData", @@ -7170,7 +7630,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "TemplateGroupRoot": { "name": "TemplateGroupRoot", @@ -7259,7 +7719,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "TranslationGetIn": { "name": "TranslationGetIn", @@ -7291,7 +7751,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "TranslationGetOut": { "name": "TranslationGetOut", @@ -7332,7 +7792,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "TutanotaProperties": { "name": "TutanotaProperties", @@ -7521,7 +7981,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "UnreadMailStatePostIn": { "name": "UnreadMailStatePostIn", @@ -7564,7 +8024,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "UpdateMailFolderData": { "name": "UpdateMailFolderData", @@ -7608,7 +8068,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "UserAccountCreateData": { "name": "UserAccountCreateData", @@ -7661,7 +8121,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "UserAccountUserData": { "name": "UserAccountUserData", @@ -7882,7 +8342,7 @@ export const typeModels = { }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "UserAreaGroupData": { "name": "UserAreaGroupData", @@ -7988,7 +8448,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "UserAreaGroupDeleteData": { "name": "UserAreaGroupDeleteData", @@ -8022,7 +8482,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "UserAreaGroupPostData": { "name": "UserAreaGroupPostData", @@ -8056,7 +8516,7 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" }, "UserSettingsGroupRoot": { "name": "UserSettingsGroupRoot", @@ -8162,6 +8622,6 @@ export const typeModels = { } }, "app": "tutanota", - "version": "76" + "version": "77" } } \ No newline at end of file diff --git a/src/common/api/entities/tutanota/TypeRefs.ts b/src/common/api/entities/tutanota/TypeRefs.ts index 6c77cf3bc14..efc8aabdce4 100644 --- a/src/common/api/entities/tutanota/TypeRefs.ts +++ b/src/common/api/entities/tutanota/TypeRefs.ts @@ -1009,6 +1009,94 @@ export type ImapSyncState = { folders: ImapFolder[]; } +export const ImportAttachmentTypeRef: TypeRef = new TypeRef("tutanota", "ImportAttachment") + +export function createImportAttachment(values: StrippedEntity): ImportAttachment { + return Object.assign(create(typeModels.ImportAttachment, ImportAttachmentTypeRef), values) +} + +export type ImportAttachment = { + _type: TypeRef; + + _id: Id; + ownerEncFileSessionKey: Uint8Array; + + existingFile: null | IdTuple; + newFile: null | NewImportAttachment; +} +export const ImportMailDataTypeRef: TypeRef = new TypeRef("tutanota", "ImportMailData") + +export function createImportMailData(values: StrippedEntity): ImportMailData { + return Object.assign(create(typeModels.ImportMailData, ImportMailDataTypeRef), values) +} + +export type ImportMailData = { + _type: TypeRef; + + _id: Id; + compressedBodyText: string; + compressedHeaders: string; + confidential: boolean; + date: Date; + differentEnvelopeSender: null | string; + inReplyTo: null | string; + messageId: null | string; + method: NumberString; + phishingStatus: NumberString; + replyType: NumberString; + state: NumberString; + subject: string; + unread: boolean; + + importedAttachments: ImportAttachment[]; + recipients: Recipients; + references: ImportMailDataMailReference[]; + replyTos: EncryptedMailAddress[]; + sender: MailAddress; +} +export const ImportMailDataMailReferenceTypeRef: TypeRef = new TypeRef("tutanota", "ImportMailDataMailReference") + +export function createImportMailDataMailReference(values: StrippedEntity): ImportMailDataMailReference { + return Object.assign(create(typeModels.ImportMailDataMailReference, ImportMailDataMailReferenceTypeRef), values) +} + +export type ImportMailDataMailReference = { + _type: TypeRef; + + _id: Id; + reference: string; +} +export const ImportMailPostInTypeRef: TypeRef = new TypeRef("tutanota", "ImportMailPostIn") + +export function createImportMailPostIn(values: StrippedEntity): ImportMailPostIn { + return Object.assign(create(typeModels.ImportMailPostIn, ImportMailPostInTypeRef), values) +} + +export type ImportMailPostIn = { + _type: TypeRef; + _errors: Object; + + _format: NumberString; + ownerEncSessionKey: Uint8Array; + ownerGroup: Id; + ownerKeyVersion: NumberString; + + imports: ImportMailData[]; + targetMailFolder: IdTuple; +} +export const ImportMailPostOutTypeRef: TypeRef = new TypeRef("tutanota", "ImportMailPostOut") + +export function createImportMailPostOut(values: StrippedEntity): ImportMailPostOut { + return Object.assign(create(typeModels.ImportMailPostOut, ImportMailPostOutTypeRef), values) +} + +export type ImportMailPostOut = { + _type: TypeRef; + + _format: NumberString; + + mails: IdTuple[]; +} export const InboxRuleTypeRef: TypeRef = new TypeRef("tutanota", "InboxRule") export function createInboxRule(values: StrippedEntity): InboxRule { @@ -1428,6 +1516,24 @@ export type NewDraftAttachment = { referenceTokens: BlobReferenceTokenWrapper[]; } +export const NewImportAttachmentTypeRef: TypeRef = new TypeRef("tutanota", "NewImportAttachment") + +export function createNewImportAttachment(values: StrippedEntity): NewImportAttachment { + return Object.assign(create(typeModels.NewImportAttachment, NewImportAttachmentTypeRef), values) +} + +export type NewImportAttachment = { + _type: TypeRef; + + _id: Id; + encCid: null | Uint8Array; + encFileHash: null | Uint8Array; + encFileName: Uint8Array; + encMimeType: Uint8Array; + ownerEncFileHashSessionKey: null | Uint8Array; + + referenceTokens: BlobReferenceTokenWrapper[]; +} export const NewsIdTypeRef: TypeRef = new TypeRef("tutanota", "NewsId") export function createNewsId(values: StrippedEntity): NewsId { diff --git a/tuta-sdk/rust/sdk/src/entities/generated/tutanota.rs b/tuta-sdk/rust/sdk/src/entities/generated/tutanota.rs index f8b9251abfa..6df61e5a93f 100644 --- a/tuta-sdk/rust/sdk/src/entities/generated/tutanota.rs +++ b/tuta-sdk/rust/sdk/src/entities/generated/tutanota.rs @@ -1227,6 +1227,109 @@ impl Entity for ImapSyncState { } } +#[derive(uniffi::Record, Clone, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Debug))] +pub struct ImportAttachment { + pub _id: Option, + #[serde(with = "serde_bytes")] + pub ownerEncFileSessionKey: Vec, + pub existingFile: Option, + pub newFile: Option, +} +impl Entity for ImportAttachment { + fn type_ref() -> TypeRef { + TypeRef { + app: "tutanota", + type_: "ImportAttachment", + } + } +} + +#[derive(uniffi::Record, Clone, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Debug))] +pub struct ImportMailData { + pub _id: Option, + pub compressedBodyText: String, + pub compressedHeaders: String, + pub confidential: bool, + pub date: DateTime, + pub differentEnvelopeSender: Option, + pub inReplyTo: Option, + pub messageId: Option, + pub method: i64, + pub phishingStatus: i64, + pub replyType: i64, + pub state: i64, + pub subject: String, + pub unread: bool, + pub importedAttachments: Vec, + pub recipients: Recipients, + pub references: Vec, + pub replyTos: Vec, + pub sender: MailAddress, + pub _finalIvs: HashMap, +} +impl Entity for ImportMailData { + fn type_ref() -> TypeRef { + TypeRef { + app: "tutanota", + type_: "ImportMailData", + } + } +} + +#[derive(uniffi::Record, Clone, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Debug))] +pub struct ImportMailDataMailReference { + pub _id: Option, + pub reference: String, +} +impl Entity for ImportMailDataMailReference { + fn type_ref() -> TypeRef { + TypeRef { + app: "tutanota", + type_: "ImportMailDataMailReference", + } + } +} + +#[derive(uniffi::Record, Clone, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Debug))] +pub struct ImportMailPostIn { + pub _format: i64, + #[serde(with = "serde_bytes")] + pub ownerEncSessionKey: Vec, + pub ownerGroup: GeneratedId, + pub ownerKeyVersion: i64, + pub imports: Vec, + pub targetMailFolder: IdTupleGenerated, + pub _errors: Option, + pub _finalIvs: HashMap, +} +impl Entity for ImportMailPostIn { + fn type_ref() -> TypeRef { + TypeRef { + app: "tutanota", + type_: "ImportMailPostIn", + } + } +} + +#[derive(uniffi::Record, Clone, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Debug))] +pub struct ImportMailPostOut { + pub _format: i64, + pub mails: Vec, +} +impl Entity for ImportMailPostOut { + fn type_ref() -> TypeRef { + TypeRef { + app: "tutanota", + type_: "ImportMailPostOut", + } + } +} + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct InboxRule { @@ -1727,6 +1830,31 @@ impl Entity for NewDraftAttachment { } } +#[derive(uniffi::Record, Clone, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Debug))] +pub struct NewImportAttachment { + pub _id: Option, + #[serde(with = "serde_bytes")] + pub encCid: Option>, + #[serde(with = "serde_bytes")] + pub encFileHash: Option>, + #[serde(with = "serde_bytes")] + pub encFileName: Vec, + #[serde(with = "serde_bytes")] + pub encMimeType: Vec, + #[serde(with = "serde_bytes")] + pub ownerEncFileHashSessionKey: Option>, + pub referenceTokens: Vec, +} +impl Entity for NewImportAttachment { + fn type_ref() -> TypeRef { + TypeRef { + app: "tutanota", + type_: "NewImportAttachment", + } + } +} + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct NewsId { diff --git a/tuta-sdk/rust/sdk/src/services/generated/tutanota.rs b/tuta-sdk/rust/sdk/src/services/generated/tutanota.rs index f7f8846c022..720a8bcd2d8 100644 --- a/tuta-sdk/rust/sdk/src/services/generated/tutanota.rs +++ b/tuta-sdk/rust/sdk/src/services/generated/tutanota.rs @@ -19,6 +19,8 @@ use crate::entities::generated::tutanota::GroupInvitationDeleteData; use crate::entities::generated::tutanota::GroupInvitationPostData; use crate::entities::generated::tutanota::GroupInvitationPostReturn; use crate::entities::generated::tutanota::GroupInvitationPutData; +use crate::entities::generated::tutanota::ImportMailPostIn; +use crate::entities::generated::tutanota::ImportMailPostOut; use crate::entities::generated::tutanota::ListUnsubscribeData; use crate::entities::generated::tutanota::MoveMailData; use crate::entities::generated::tutanota::NewsIn; @@ -44,7 +46,7 @@ use crate::services::{ use crate::ApiCallError; pub struct CalendarService; -crate::service_impl!(declare, CalendarService, "tutanota/calendarservice", 76); +crate::service_impl!(declare, CalendarService, "tutanota/calendarservice", 77); crate::service_impl!( POST, CalendarService, @@ -59,7 +61,7 @@ crate::service_impl!( declare, ContactListGroupService, "tutanota/contactlistgroupservice", - 76 + 77 ); crate::service_impl!( POST, @@ -75,13 +77,13 @@ crate::service_impl!( declare, CustomerAccountService, "tutanota/customeraccountservice", - 76 + 77 ); crate::service_impl!(POST, CustomerAccountService, CustomerAccountCreateData, ()); pub struct DraftService; -crate::service_impl!(declare, DraftService, "tutanota/draftservice", 76); +crate::service_impl!(declare, DraftService, "tutanota/draftservice", 77); crate::service_impl!(POST, DraftService, DraftCreateData, DraftCreateReturn); crate::service_impl!(PUT, DraftService, DraftUpdateData, DraftUpdateReturn); @@ -91,7 +93,7 @@ crate::service_impl!( declare, EncryptTutanotaPropertiesService, "tutanota/encrypttutanotapropertiesservice", - 76 + 77 ); crate::service_impl!( POST, @@ -102,7 +104,7 @@ crate::service_impl!( pub struct EntropyService; -crate::service_impl!(declare, EntropyService, "tutanota/entropyservice", 76); +crate::service_impl!(declare, EntropyService, "tutanota/entropyservice", 77); crate::service_impl!(PUT, EntropyService, EntropyData, ()); pub struct ExternalUserService; @@ -111,7 +113,7 @@ crate::service_impl!( declare, ExternalUserService, "tutanota/externaluserservice", - 76 + 77 ); crate::service_impl!(POST, ExternalUserService, ExternalUserData, ()); @@ -121,7 +123,7 @@ crate::service_impl!( declare, GroupInvitationService, "tutanota/groupinvitationservice", - 76 + 77 ); crate::service_impl!( POST, @@ -137,19 +139,24 @@ crate::service_impl!( () ); +pub struct ImportMailService; + +crate::service_impl!(declare, ImportMailService, "tutanota/importmailservice", 77); +crate::service_impl!(POST, ImportMailService, ImportMailPostIn, ImportMailPostOut); + pub struct ListUnsubscribeService; crate::service_impl!( declare, ListUnsubscribeService, "tutanota/listunsubscribeservice", - 76 + 77 ); crate::service_impl!(POST, ListUnsubscribeService, ListUnsubscribeData, ()); pub struct MailFolderService; -crate::service_impl!(declare, MailFolderService, "tutanota/mailfolderservice", 76); +crate::service_impl!(declare, MailFolderService, "tutanota/mailfolderservice", 77); crate::service_impl!( POST, MailFolderService, @@ -161,23 +168,23 @@ crate::service_impl!(DELETE, MailFolderService, DeleteMailFolderData, ()); pub struct MailGroupService; -crate::service_impl!(declare, MailGroupService, "tutanota/mailgroupservice", 76); +crate::service_impl!(declare, MailGroupService, "tutanota/mailgroupservice", 77); crate::service_impl!(POST, MailGroupService, CreateMailGroupData, ()); crate::service_impl!(DELETE, MailGroupService, DeleteGroupData, ()); pub struct MailService; -crate::service_impl!(declare, MailService, "tutanota/mailservice", 76); +crate::service_impl!(declare, MailService, "tutanota/mailservice", 77); crate::service_impl!(DELETE, MailService, DeleteMailData, ()); pub struct MoveMailService; -crate::service_impl!(declare, MoveMailService, "tutanota/movemailservice", 76); +crate::service_impl!(declare, MoveMailService, "tutanota/movemailservice", 77); crate::service_impl!(POST, MoveMailService, MoveMailData, ()); pub struct NewsService; -crate::service_impl!(declare, NewsService, "tutanota/newsservice", 76); +crate::service_impl!(declare, NewsService, "tutanota/newsservice", 77); crate::service_impl!(POST, NewsService, NewsIn, ()); crate::service_impl!(GET, NewsService, (), NewsOut); @@ -187,18 +194,18 @@ crate::service_impl!( declare, ReceiveInfoService, "tutanota/receiveinfoservice", - 76 + 77 ); crate::service_impl!(POST, ReceiveInfoService, ReceiveInfoServiceData, ()); pub struct ReportMailService; -crate::service_impl!(declare, ReportMailService, "tutanota/reportmailservice", 76); +crate::service_impl!(declare, ReportMailService, "tutanota/reportmailservice", 77); crate::service_impl!(POST, ReportMailService, ReportMailPostData, ()); pub struct SendDraftService; -crate::service_impl!(declare, SendDraftService, "tutanota/senddraftservice", 76); +crate::service_impl!(declare, SendDraftService, "tutanota/senddraftservice", 77); crate::service_impl!(POST, SendDraftService, SendDraftData, SendDraftReturn); pub struct SimpleMoveMailService; @@ -207,7 +214,7 @@ crate::service_impl!( declare, SimpleMoveMailService, "tutanota/simplemovemailservice", - 76 + 77 ); crate::service_impl!(POST, SimpleMoveMailService, SimpleMoveMailPostIn, ()); @@ -217,7 +224,7 @@ crate::service_impl!( declare, TemplateGroupService, "tutanota/templategroupservice", - 76 + 77 ); crate::service_impl!( POST, @@ -233,7 +240,7 @@ crate::service_impl!( declare, TranslationService, "tutanota/translationservice", - 76 + 77 ); crate::service_impl!(GET, TranslationService, TranslationGetIn, TranslationGetOut); @@ -243,7 +250,7 @@ crate::service_impl!( declare, UnreadMailStateService, "tutanota/unreadmailstateservice", - 76 + 77 ); crate::service_impl!(POST, UnreadMailStateService, UnreadMailStatePostIn, ()); @@ -253,6 +260,6 @@ crate::service_impl!( declare, UserAccountService, "tutanota/useraccountservice", - 76 + 77 ); crate::service_impl!(POST, UserAccountService, UserAccountCreateData, ()); diff --git a/tuta-sdk/rust/sdk/src/type_models/tutanota.json b/tuta-sdk/rust/sdk/src/type_models/tutanota.json index d08ad144744..cbebdee32e0 100644 --- a/tuta-sdk/rust/sdk/src/type_models/tutanota.json +++ b/tuta-sdk/rust/sdk/src/type_models/tutanota.json @@ -49,7 +49,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "Birthday": { "name": "Birthday", @@ -99,7 +99,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "Body": { "name": "Body", @@ -140,7 +140,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "CalendarDeleteData": { "name": "CalendarDeleteData", @@ -174,7 +174,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CalendarEvent": { "name": "CalendarEvent", @@ -373,7 +373,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CalendarEventAttendee": { "name": "CalendarEventAttendee", @@ -416,7 +416,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CalendarEventIndexRef": { "name": "CalendarEventIndexRef", @@ -450,7 +450,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CalendarEventUidIndex": { "name": "CalendarEventUidIndex", @@ -521,7 +521,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CalendarEventUpdate": { "name": "CalendarEventUpdate", @@ -609,7 +609,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CalendarEventUpdateList": { "name": "CalendarEventUpdateList", @@ -643,7 +643,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CalendarGroupRoot": { "name": "CalendarGroupRoot", @@ -742,7 +742,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CalendarRepeatRule": { "name": "CalendarRepeatRule", @@ -821,7 +821,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "Contact": { "name": "Contact", @@ -1144,7 +1144,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactAddress": { "name": "ContactAddress", @@ -1194,7 +1194,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactCustomDate": { "name": "ContactCustomDate", @@ -1244,7 +1244,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactList": { "name": "ContactList", @@ -1333,7 +1333,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactListEntry": { "name": "ContactListEntry", @@ -1410,7 +1410,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactListGroupRoot": { "name": "ContactListGroupRoot", @@ -1489,7 +1489,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactMailAddress": { "name": "ContactMailAddress", @@ -1539,7 +1539,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactMessengerHandle": { "name": "ContactMessengerHandle", @@ -1589,7 +1589,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactPhoneNumber": { "name": "ContactPhoneNumber", @@ -1639,7 +1639,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactPronouns": { "name": "ContactPronouns", @@ -1680,7 +1680,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactRelationship": { "name": "ContactRelationship", @@ -1730,7 +1730,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactSocialId": { "name": "ContactSocialId", @@ -1780,7 +1780,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ContactWebsite": { "name": "ContactWebsite", @@ -1830,7 +1830,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ConversationEntry": { "name": "ConversationEntry", @@ -1919,7 +1919,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CreateExternalUserGroupData": { "name": "CreateExternalUserGroupData", @@ -1978,7 +1978,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "CreateGroupPostReturn": { "name": "CreateGroupPostReturn", @@ -2012,7 +2012,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CreateMailFolderData": { "name": "CreateMailFolderData", @@ -2082,7 +2082,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CreateMailFolderReturn": { "name": "CreateMailFolderReturn", @@ -2116,7 +2116,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CreateMailGroupData": { "name": "CreateMailGroupData", @@ -2177,7 +2177,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "CustomerAccountCreateData": { "name": "CustomerAccountCreateData", @@ -2349,7 +2349,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "DefaultAlarmInfo": { "name": "DefaultAlarmInfo", @@ -2381,7 +2381,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "DeleteGroupData": { "name": "DeleteGroupData", @@ -2424,7 +2424,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "DeleteMailData": { "name": "DeleteMailData", @@ -2468,7 +2468,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "DeleteMailFolderData": { "name": "DeleteMailFolderData", @@ -2502,7 +2502,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "DraftAttachment": { "name": "DraftAttachment", @@ -2564,7 +2564,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "DraftCreateData": { "name": "DraftCreateData", @@ -2634,7 +2634,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "DraftCreateReturn": { "name": "DraftCreateReturn", @@ -2668,7 +2668,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "DraftData": { "name": "DraftData", @@ -2815,7 +2815,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "DraftRecipient": { "name": "DraftRecipient", @@ -2856,7 +2856,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "DraftUpdateData": { "name": "DraftUpdateData", @@ -2900,7 +2900,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "DraftUpdateReturn": { "name": "DraftUpdateReturn", @@ -2934,7 +2934,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "EmailTemplate": { "name": "EmailTemplate", @@ -3031,7 +3031,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "EmailTemplateContent": { "name": "EmailTemplateContent", @@ -3072,7 +3072,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "EncryptTutanotaPropertiesData": { "name": "EncryptTutanotaPropertiesData", @@ -3124,7 +3124,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "EncryptedMailAddress": { "name": "EncryptedMailAddress", @@ -3165,7 +3165,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "EntropyData": { "name": "EntropyData", @@ -3206,7 +3206,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ExternalUserData": { "name": "ExternalUserData", @@ -3339,7 +3339,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "File": { "name": "File", @@ -3474,7 +3474,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "FileSystem": { "name": "FileSystem", @@ -3553,7 +3553,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "GroupInvitationDeleteData": { "name": "GroupInvitationDeleteData", @@ -3587,7 +3587,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "GroupInvitationPostData": { "name": "GroupInvitationPostData", @@ -3631,7 +3631,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "GroupInvitationPostReturn": { "name": "GroupInvitationPostReturn", @@ -3685,7 +3685,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "GroupInvitationPutData": { "name": "GroupInvitationPutData", @@ -3755,7 +3755,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "GroupSettings": { "name": "GroupSettings", @@ -3826,7 +3826,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "Header": { "name": "Header", @@ -3867,7 +3867,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ImapFolder": { "name": "ImapFolder", @@ -3928,7 +3928,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "ImapSyncConfiguration": { "name": "ImapSyncConfiguration", @@ -3998,7 +3998,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "ImapSyncState": { "name": "ImapSyncState", @@ -4059,7 +4059,388 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" + }, + "ImportAttachment": { + "name": "ImportAttachment", + "since": 77, + "type": "AGGREGATED_TYPE", + "id": 1490, + "rootId": "CHR1dGFub3RhAAXS", + "versioned": false, + "encrypted": false, + "values": { + "_id": { + "final": true, + "name": "_id", + "id": 1491, + "since": 77, + "type": "CustomId", + "cardinality": "One", + "encrypted": false + }, + "ownerEncFileSessionKey": { + "final": true, + "name": "ownerEncFileSessionKey", + "id": 1492, + "since": 77, + "type": "Bytes", + "cardinality": "One", + "encrypted": false + } + }, + "associations": { + "existingFile": { + "final": true, + "name": "existingFile", + "id": 1494, + "since": 77, + "type": "LIST_ELEMENT_ASSOCIATION_GENERATED", + "cardinality": "ZeroOrOne", + "refType": "File", + "dependency": null + }, + "newFile": { + "final": true, + "name": "newFile", + "id": 1493, + "since": 77, + "type": "AGGREGATION", + "cardinality": "ZeroOrOne", + "refType": "NewImportAttachment", + "dependency": null + } + }, + "app": "tutanota", + "version": "77" + }, + "ImportMailData": { + "name": "ImportMailData", + "since": 77, + "type": "AGGREGATED_TYPE", + "id": 1495, + "rootId": "CHR1dGFub3RhAAXX", + "versioned": false, + "encrypted": false, + "values": { + "_id": { + "final": true, + "name": "_id", + "id": 1496, + "since": 77, + "type": "CustomId", + "cardinality": "One", + "encrypted": false + }, + "compressedBodyText": { + "final": true, + "name": "compressedBodyText", + "id": 1498, + "since": 77, + "type": "CompressedString", + "cardinality": "One", + "encrypted": true + }, + "compressedHeaders": { + "final": true, + "name": "compressedHeaders", + "id": 1511, + "since": 77, + "type": "CompressedString", + "cardinality": "One", + "encrypted": true + }, + "confidential": { + "final": true, + "name": "confidential", + "id": 1506, + "since": 77, + "type": "Boolean", + "cardinality": "One", + "encrypted": true + }, + "date": { + "final": true, + "name": "date", + "id": 1499, + "since": 77, + "type": "Date", + "cardinality": "One", + "encrypted": false + }, + "differentEnvelopeSender": { + "final": true, + "name": "differentEnvelopeSender", + "id": 1509, + "since": 77, + "type": "String", + "cardinality": "ZeroOrOne", + "encrypted": true + }, + "inReplyTo": { + "final": true, + "name": "inReplyTo", + "id": 1503, + "since": 77, + "type": "String", + "cardinality": "ZeroOrOne", + "encrypted": false + }, + "messageId": { + "final": true, + "name": "messageId", + "id": 1502, + "since": 77, + "type": "String", + "cardinality": "ZeroOrOne", + "encrypted": false + }, + "method": { + "final": true, + "name": "method", + "id": 1507, + "since": 77, + "type": "Number", + "cardinality": "One", + "encrypted": true + }, + "phishingStatus": { + "final": true, + "name": "phishingStatus", + "id": 1510, + "since": 77, + "type": "Number", + "cardinality": "One", + "encrypted": false + }, + "replyType": { + "final": false, + "name": "replyType", + "id": 1508, + "since": 77, + "type": "Number", + "cardinality": "One", + "encrypted": true + }, + "state": { + "final": true, + "name": "state", + "id": 1500, + "since": 77, + "type": "Number", + "cardinality": "One", + "encrypted": false + }, + "subject": { + "final": true, + "name": "subject", + "id": 1497, + "since": 77, + "type": "String", + "cardinality": "One", + "encrypted": true + }, + "unread": { + "final": true, + "name": "unread", + "id": 1501, + "since": 77, + "type": "Boolean", + "cardinality": "One", + "encrypted": false + } + }, + "associations": { + "importedAttachments": { + "final": true, + "name": "importedAttachments", + "id": 1514, + "since": 77, + "type": "AGGREGATION", + "cardinality": "Any", + "refType": "ImportAttachment", + "dependency": null + }, + "recipients": { + "final": true, + "name": "recipients", + "id": 1513, + "since": 77, + "type": "AGGREGATION", + "cardinality": "One", + "refType": "Recipients", + "dependency": null + }, + "references": { + "final": true, + "name": "references", + "id": 1504, + "since": 77, + "type": "AGGREGATION", + "cardinality": "Any", + "refType": "ImportMailDataMailReference", + "dependency": null + }, + "replyTos": { + "final": false, + "name": "replyTos", + "id": 1512, + "since": 77, + "type": "AGGREGATION", + "cardinality": "Any", + "refType": "EncryptedMailAddress", + "dependency": null + }, + "sender": { + "final": true, + "name": "sender", + "id": 1505, + "since": 77, + "type": "AGGREGATION", + "cardinality": "One", + "refType": "MailAddress", + "dependency": null + } + }, + "app": "tutanota", + "version": "77" + }, + "ImportMailDataMailReference": { + "name": "ImportMailDataMailReference", + "since": 77, + "type": "AGGREGATED_TYPE", + "id": 1479, + "rootId": "CHR1dGFub3RhAAXH", + "versioned": false, + "encrypted": false, + "values": { + "_id": { + "final": true, + "name": "_id", + "id": 1480, + "since": 77, + "type": "CustomId", + "cardinality": "One", + "encrypted": false + }, + "reference": { + "final": false, + "name": "reference", + "id": 1481, + "since": 77, + "type": "String", + "cardinality": "One", + "encrypted": false + } + }, + "associations": {}, + "app": "tutanota", + "version": "77" + }, + "ImportMailPostIn": { + "name": "ImportMailPostIn", + "since": 77, + "type": "DATA_TRANSFER_TYPE", + "id": 1515, + "rootId": "CHR1dGFub3RhAAXr", + "versioned": false, + "encrypted": true, + "values": { + "_format": { + "final": false, + "name": "_format", + "id": 1516, + "since": 77, + "type": "Number", + "cardinality": "One", + "encrypted": false + }, + "ownerEncSessionKey": { + "final": false, + "name": "ownerEncSessionKey", + "id": 1518, + "since": 77, + "type": "Bytes", + "cardinality": "One", + "encrypted": false + }, + "ownerGroup": { + "final": false, + "name": "ownerGroup", + "id": 1517, + "since": 77, + "type": "GeneratedId", + "cardinality": "One", + "encrypted": false + }, + "ownerKeyVersion": { + "final": false, + "name": "ownerKeyVersion", + "id": 1519, + "since": 77, + "type": "Number", + "cardinality": "One", + "encrypted": false + } + }, + "associations": { + "imports": { + "final": false, + "name": "imports", + "id": 1521, + "since": 77, + "type": "AGGREGATION", + "cardinality": "Any", + "refType": "ImportMailData", + "dependency": null + }, + "targetMailFolder": { + "final": true, + "name": "targetMailFolder", + "id": 1520, + "since": 77, + "type": "LIST_ELEMENT_ASSOCIATION_GENERATED", + "cardinality": "One", + "refType": "MailFolder", + "dependency": null + } + }, + "app": "tutanota", + "version": "77" + }, + "ImportMailPostOut": { + "name": "ImportMailPostOut", + "since": 77, + "type": "DATA_TRANSFER_TYPE", + "id": 1522, + "rootId": "CHR1dGFub3RhAAXy", + "versioned": false, + "encrypted": false, + "values": { + "_format": { + "final": false, + "name": "_format", + "id": 1523, + "since": 77, + "type": "Number", + "cardinality": "One", + "encrypted": false + } + }, + "associations": { + "mails": { + "final": false, + "name": "mails", + "id": 1524, + "since": 77, + "type": "LIST_ELEMENT_ASSOCIATION_GENERATED", + "cardinality": "Any", + "refType": "Mail", + "dependency": null + } + }, + "app": "tutanota", + "version": "77" }, "InboxRule": { "name": "InboxRule", @@ -4111,7 +4492,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "InternalGroupData": { "name": "InternalGroupData", @@ -4235,7 +4616,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "InternalRecipientKeyData": { "name": "InternalRecipientKeyData", @@ -4303,7 +4684,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "KnowledgeBaseEntry": { "name": "KnowledgeBaseEntry", @@ -4400,7 +4781,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "KnowledgeBaseEntryKeyword": { "name": "KnowledgeBaseEntryKeyword", @@ -4432,7 +4813,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "ListUnsubscribeData": { "name": "ListUnsubscribeData", @@ -4484,7 +4865,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "Mail": { "name": "Mail", @@ -4759,7 +5140,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailAddress": { "name": "MailAddress", @@ -4811,7 +5192,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailAddressProperties": { "name": "MailAddressProperties", @@ -4852,7 +5233,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "MailBag": { "name": "MailBag", @@ -4886,7 +5267,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailBox": { "name": "MailBox", @@ -5034,7 +5415,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailDetails": { "name": "MailDetails", @@ -5116,7 +5497,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailDetailsBlob": { "name": "MailDetailsBlob", @@ -5195,7 +5576,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailDetailsDraft": { "name": "MailDetailsDraft", @@ -5274,7 +5655,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailDetailsDraftsRef": { "name": "MailDetailsDraftsRef", @@ -5308,7 +5689,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailFolder": { "name": "MailFolder", @@ -5443,7 +5824,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailFolderRef": { "name": "MailFolderRef", @@ -5477,7 +5858,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailSetEntry": { "name": "MailSetEntry", @@ -5538,7 +5919,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailboxGroupRoot": { "name": "MailboxGroupRoot", @@ -5649,7 +6030,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailboxProperties": { "name": "MailboxProperties", @@ -5737,7 +6118,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "MailboxServerProperties": { "name": "MailboxServerProperties", @@ -5796,7 +6177,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "MoveMailData": { "name": "MoveMailData", @@ -5850,7 +6231,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "NewDraftAttachment": { "name": "NewDraftAttachment", @@ -5911,7 +6292,86 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" + }, + "NewImportAttachment": { + "name": "NewImportAttachment", + "since": 77, + "type": "AGGREGATED_TYPE", + "id": 1482, + "rootId": "CHR1dGFub3RhAAXK", + "versioned": false, + "encrypted": false, + "values": { + "_id": { + "final": true, + "name": "_id", + "id": 1483, + "since": 77, + "type": "CustomId", + "cardinality": "One", + "encrypted": false + }, + "encCid": { + "final": true, + "name": "encCid", + "id": 1488, + "since": 77, + "type": "Bytes", + "cardinality": "ZeroOrOne", + "encrypted": false + }, + "encFileHash": { + "final": true, + "name": "encFileHash", + "id": 1485, + "since": 77, + "type": "Bytes", + "cardinality": "ZeroOrOne", + "encrypted": false + }, + "encFileName": { + "final": true, + "name": "encFileName", + "id": 1486, + "since": 77, + "type": "Bytes", + "cardinality": "One", + "encrypted": false + }, + "encMimeType": { + "final": true, + "name": "encMimeType", + "id": 1487, + "since": 77, + "type": "Bytes", + "cardinality": "One", + "encrypted": false + }, + "ownerEncFileHashSessionKey": { + "final": true, + "name": "ownerEncFileHashSessionKey", + "id": 1484, + "since": 77, + "type": "Bytes", + "cardinality": "ZeroOrOne", + "encrypted": false + } + }, + "associations": { + "referenceTokens": { + "final": true, + "name": "referenceTokens", + "id": 1489, + "since": 77, + "type": "AGGREGATION", + "cardinality": "Any", + "refType": "BlobReferenceTokenWrapper", + "dependency": "sys" + } + }, + "app": "tutanota", + "version": "77" }, "NewsId": { "name": "NewsId", @@ -5952,7 +6412,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "NewsIn": { "name": "NewsIn", @@ -5984,7 +6444,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "NewsOut": { "name": "NewsOut", @@ -6018,7 +6478,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "NotificationMail": { "name": "NotificationMail", @@ -6086,7 +6546,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "OutOfOfficeNotification": { "name": "OutOfOfficeNotification", @@ -6174,7 +6634,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "OutOfOfficeNotificationMessage": { "name": "OutOfOfficeNotificationMessage", @@ -6224,7 +6684,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "OutOfOfficeNotificationRecipientList": { "name": "OutOfOfficeNotificationRecipientList", @@ -6258,7 +6718,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "PhishingMarkerWebsocketData": { "name": "PhishingMarkerWebsocketData", @@ -6301,7 +6761,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "PhotosRef": { "name": "PhotosRef", @@ -6335,7 +6795,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "ReceiveInfoServiceData": { "name": "ReceiveInfoServiceData", @@ -6367,7 +6827,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "Recipients": { "name": "Recipients", @@ -6421,7 +6881,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "RemoteImapSyncInfo": { "name": "RemoteImapSyncInfo", @@ -6491,7 +6951,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "ReportMailPostData": { "name": "ReportMailPostData", @@ -6543,7 +7003,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "ReportedMailFieldMarker": { "name": "ReportedMailFieldMarker", @@ -6584,7 +7044,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "SecureExternalRecipientKeyData": { "name": "SecureExternalRecipientKeyData", @@ -6688,7 +7148,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "SendDraftData": { "name": "SendDraftData", @@ -6825,7 +7285,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "SendDraftReturn": { "name": "SendDraftReturn", @@ -6887,7 +7347,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "SharedGroupData": { "name": "SharedGroupData", @@ -6991,7 +7451,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "SimpleMoveMailPostIn": { "name": "SimpleMoveMailPostIn", @@ -7034,7 +7494,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "SpamResults": { "name": "SpamResults", @@ -7068,7 +7528,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "Subfiles": { "name": "Subfiles", @@ -7102,7 +7562,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "SymEncInternalRecipientKeyData": { "name": "SymEncInternalRecipientKeyData", @@ -7163,7 +7623,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "TemplateGroupRoot": { "name": "TemplateGroupRoot", @@ -7252,7 +7712,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "TranslationGetIn": { "name": "TranslationGetIn", @@ -7284,7 +7744,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "TranslationGetOut": { "name": "TranslationGetOut", @@ -7325,7 +7785,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "TutanotaProperties": { "name": "TutanotaProperties", @@ -7514,7 +7974,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "UnreadMailStatePostIn": { "name": "UnreadMailStatePostIn", @@ -7557,7 +8017,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "UpdateMailFolderData": { "name": "UpdateMailFolderData", @@ -7601,7 +8061,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "UserAccountCreateData": { "name": "UserAccountCreateData", @@ -7654,7 +8114,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "UserAccountUserData": { "name": "UserAccountUserData", @@ -7875,7 +8335,7 @@ }, "associations": {}, "app": "tutanota", - "version": "76" + "version": "77" }, "UserAreaGroupData": { "name": "UserAreaGroupData", @@ -7981,7 +8441,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "UserAreaGroupDeleteData": { "name": "UserAreaGroupDeleteData", @@ -8015,7 +8475,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "UserAreaGroupPostData": { "name": "UserAreaGroupPostData", @@ -8049,7 +8509,7 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" }, "UserSettingsGroupRoot": { "name": "UserSettingsGroupRoot", @@ -8155,6 +8615,6 @@ } }, "app": "tutanota", - "version": "76" + "version": "77" } } From 958353dc1091d151a1587f8aacf69f84d3ab2a0f Mon Sep 17 00:00:00 2001 From: nig Date: Thu, 7 Nov 2024 16:50:19 +0100 Subject: [PATCH 03/32] eml import wip --- .../NativeCryptoFacadeReceiveDispatcher.kt | 15 +- .../generated_ipc/PersistedCredentials.kt | 3 +- .../generated_ipc/UnencryptedCredentials.kt | 3 +- buildSrc/DesktopBuilder.js | 15 +- buildSrc/DevBuild.js | 10 +- buildSrc/esbuildUtils.js | 37 +- buildSrc/packageBuilderFunctions.js | 4 +- ipc-schema/facades/MailImportFacade.json | 52 + ipc-schema/types/MailBundle.json | 2 +- ipc-schema/types/TutaCredentials.json | 7 + libs/electron-updater.mjs | 76 +- package-lock.json | 2287 +---------------- package.json | 1 + packages/node-mimimi/.cargo/config.toml | 2 + packages/node-mimimi/.gitignore | 201 ++ packages/node-mimimi/.npmignore | 13 + packages/node-mimimi/Cargo.toml | 56 + packages/node-mimimi/README.md | 38 + packages/node-mimimi/build.rs | 33 + .../node-mimimi/examples/import_imap_mail.rs | 92 + packages/node-mimimi/index.d.ts | 46 + packages/node-mimimi/java/build.gradle.kts | 24 + packages/node-mimimi/java/settings.gradle.kts | 24 + .../java/greenmailserver/GreenMailServer.java | 63 + packages/node-mimimi/make.js | 41 + .../npm/darwin-universal/README.md | 3 + .../npm/darwin-universal/package.json | 15 + .../node-mimimi/npm/linux-x64-gnu/README.md | 3 + .../npm/linux-x64-gnu/package.json | 21 + .../node-mimimi/npm/win32-x64-msvc/README.md | 3 + .../npm/win32-x64-msvc/package.json | 18 + packages/node-mimimi/package.json | 35 + packages/node-mimimi/rust-toolchain | 1 + packages/node-mimimi/rustfmt.toml | 16 + packages/node-mimimi/src/importer.rs | 493 ++++ .../src/importer/extend_mail_parser.rs | 174 ++ .../node-mimimi/src/importer/file_reader.rs | 1 + .../src/importer/file_reader/import_client.rs | 112 + .../node-mimimi/src/importer/imap_reader.rs | 24 + .../src/importer/imap_reader/import_client.rs | 158 ++ .../src/importer/importable_mail.rs | 881 +++++++ .../importer/plain_text_to_html_converter.rs | 164 ++ packages/node-mimimi/src/importer/status.rs | 1 + packages/node-mimimi/src/lib.rs | 7 + packages/node-mimimi/src/logging.rs | 6 + packages/node-mimimi/src/logging/console.rs | 92 + packages/node-mimimi/src/logging/logger.rs | 113 + packages/node-mimimi/src/tuta.rs | 1 + packages/node-mimimi/src/tuta/credentials.rs | 49 + packages/node-mimimi/src/tuta_imap.rs | 4 + packages/node-mimimi/src/tuta_imap/client.rs | 560 ++++ .../src/tuta_imap/client/tls_stream.rs | 109 + .../node-mimimi/src/tuta_imap/client/types.rs | 37 + packages/node-mimimi/src/tuta_imap/testing.rs | 161 ++ .../src/tuta_imap/testing/jvm_singeleton.rs | 21 + .../src/tuta_imap/testing/utils.rs | 5 + packages/node-mimimi/src/tuta_imap/utils.rs | 28 + packages/node-mimimi/test/Suite.ts | 16 + .../2002_06_12_doublebound-expected.json | 45 + .../2002_06_12_doublebound.msg | 240 ++ .../test/mimetools-testmsgs/README.md | 1 + .../mimetools-testmsgs/ak-0696-expected.json | 55 + .../test/mimetools-testmsgs/ak-0696.msg | 111 + ...ent-filename-encoding-Latin1-expected.json | 39 + .../attachment-filename-encoding-Latin1.msg | 20 + ...hment-filename-encoding-UTF8-expected.json | 39 + .../attachment-filename-encoding-UTF8.msg | 20 + .../mimetools-testmsgs/badbound-expected.json | 7 + .../test/mimetools-testmsgs/badbound.msg | 160 ++ .../mimetools-testmsgs/badfile-expected.json | 36 + .../test/mimetools-testmsgs/badfile.msg | 8 + .../bluedot-postcard-expected.json | 39 + .../mimetools-testmsgs/bluedot-postcard.msg | 138 + .../bluedot-simple-expected.json | 39 + .../mimetools-testmsgs/bluedot-simple.msg | 100 + .../double-bound-expected.json | 45 + .../test/mimetools-testmsgs/double-bound.msg | 67 + .../double-semicolon-expected.json | 7 + .../mimetools-testmsgs/double-semicolon.msg | 17 + .../double-semicolon2-expected.json | 7 + .../mimetools-testmsgs/double-semicolon2.msg | 17 + .../dup-names-expected.json | 77 + .../test/mimetools-testmsgs/dup-names.msg | 78 + .../empty-preamble-expected.json | 39 + .../mimetools-testmsgs/empty-preamble.msg | 27 + .../mimetools-testmsgs/frag-expected.json | 51 + .../test/mimetools-testmsgs/frag.msg | 1229 +++++++++ .../mimetools-testmsgs/german-expected.json | 36 + .../german-qp-expected.json | 36 + .../test/mimetools-testmsgs/german-qp.msg | 27 + .../test/mimetools-testmsgs/german.msg | 79 + .../hdr-fakeout-expected.json | 36 + .../test/mimetools-testmsgs/hdr-fakeout.msg | 15 + .../mimetools-testmsgs/infinite-expected.json | 30 + .../test/mimetools-testmsgs/infinite.msg | 92 + .../mimetools-testmsgs/intl-expected.json | 48 + .../test/mimetools-testmsgs/intl.msg | 12 + .../mimetools-testmsgs/jt-0498-expected.json | 45 + .../test/mimetools-testmsgs/jt-0498.msg | 107 + .../mimetools-testmsgs/lennie-expected.json | 51 + .../test/mimetools-testmsgs/lennie.msg | 84 + .../mp-msg-rfc822-expected.json | 55 + .../test/mimetools-testmsgs/mp-msg-rfc822.msg | 112 + .../multi-2evil-expected.json | 53 + .../test/mimetools-testmsgs/multi-2evil.msg | 58 + .../multi-2gifs-base64-expected.json | 39 + .../mimetools-testmsgs/multi-2gifs-base64.msg | 48 + .../multi-2gifs-expected.json | 53 + .../test/mimetools-testmsgs/multi-2gifs.msg | 57 + .../multi-bad-expected.json | 36 + .../test/mimetools-testmsgs/multi-bad.msg | 181 ++ .../multi-badnames-expected.json | 36 + .../mimetools-testmsgs/multi-badnames.msg | 30 + .../multi-clen-expected.json | 53 + .../test/mimetools-testmsgs/multi-clen.msg | 40 + .../multi-digest-expected.json | 45 + .../test/mimetools-testmsgs/multi-digest.msg | 30 + .../multi-frag-expected.json | 7 + .../test/mimetools-testmsgs/multi-frag.msg | 90 + .../multi-igor-expected.json | 61 + .../test/mimetools-testmsgs/multi-igor.msg | 198 ++ .../multi-igor2-expected.json | 61 + .../test/mimetools-testmsgs/multi-igor2.msg | 198 ++ .../multi-nested-expected.json | 69 + .../test/mimetools-testmsgs/multi-nested.msg | 89 + .../multi-nested2-expected.json | 69 + .../test/mimetools-testmsgs/multi-nested2.msg | 89 + .../multi-nested3-expected.json | 69 + .../test/mimetools-testmsgs/multi-nested3.msg | 89 + .../multi-simple-expected.json | 36 + .../test/mimetools-testmsgs/multi-simple.msg | 23 + .../multi-weirdspace-expected.json | 53 + .../mimetools-testmsgs/multi-weirdspace.msg | 56 + .../mimetools-testmsgs/not-mime-expected.json | 36 + .../test/mimetools-testmsgs/not-mime.msg | 17 + .../mimetools-testmsgs/re-fwd-expected.json | 39 + .../test/mimetools-testmsgs/re-fwd.msg | 33 + .../mimetools-testmsgs/russian-expected.json | 39 + .../test/mimetools-testmsgs/russian.msg | 7 + .../mimetools-testmsgs/sig-uu-expected.json | 30 + .../test/mimetools-testmsgs/sig-uu.msg | 29 + .../mimetools-testmsgs/simple-expected.json | 47 + .../test/mimetools-testmsgs/simple.msg | 20 + .../ticket-60931-expected.json | 30 + .../test/mimetools-testmsgs/ticket-60931.msg | 15 + .../mimetools-testmsgs/twopart-expected.json | 45 + .../test/mimetools-testmsgs/twopart.msg | 571 ++++ .../mimetools-testmsgs/uu-junk-expected.json | 36 + .../uu-junk-target-expected.json | 53 + .../mimetools-testmsgs/uu-junk-target.msg | 182 ++ .../test/mimetools-testmsgs/uu-junk.msg | 168 ++ .../uu-zeegee-expected.json | 30 + .../test/mimetools-testmsgs/uu-zeegee.msg | 125 + .../mimetools-testmsgs/x-gzip64-expected.json | 7 + .../test/mimetools-testmsgs/x-gzip64.msg | 13 + packages/node-mimimi/test/sample.eml | 39 + packages/node-mimimi/test/tsconfig.json | 15 + packages/node-mimimi/tsconfig.json | 5 + src/calendar-app/calendarLocator.ts | 2 + src/common/api/main/CommonLocator.ts | 2 + .../api/worker/rest/DefaultEntityRestCache.ts | 7 + src/common/desktop/DesktopMain.ts | 7 +- .../mailimport/DesktopMailImportFacade.ts | 63 + src/common/desktop/mailimport/MailImporter.ts | 61 + src/common/desktop/preload.js | 3 +- src/common/desktop/sse/SseClient.ts | 2 +- src/common/file/FileController.ts | 10 +- src/common/gui/AttachmentBubble.ts | 5 +- src/common/gui/base/GuiUtils.ts | 18 +- src/common/gui/base/NavButton.ts | 25 +- src/common/login/PostLoginActions.ts | 21 + src/common/misc/TranslationKey.ts | 2 + .../generatedipc/DesktopGlobalDispatcher.ts | 7 + .../common/generatedipc/MailImportFacade.ts | 33 + .../MailImportFacadeReceiveDispatcher.ts | 31 + .../MailImportFacadeSendDispatcher.ts | 22 + .../common/generatedipc/TutaCredentials.ts | 3 + .../native/main/NativeInterfaceFactory.ts | 4 + .../native/main/WebCommonNativeFacade.ts | 15 +- src/global.d.ts | 4 +- src/mail-app/app.ts | 1 + src/mail-app/mail/editor/MailEditor.ts | 5 +- src/mail-app/mail/view/MailFoldersView.ts | 5 +- src/mail-app/mail/view/MailListView.ts | 5 +- src/mail-app/mail/view/MailView.ts | 120 +- src/mail-app/mailLocator.ts | 32 +- src/mail-app/translations/en.ts | 4 +- .../api/worker/crypto/InstanceMapperTest.ts | 1 + test/types/test.d.ts | 1 + tsconfig.json | 3 + tuta-sdk/rust/Cargo.lock | 666 ----- tuta-sdk/rust/Cargo.toml | 6 +- tuta-sdk/rust/demo/Cargo.toml | 11 - tuta-sdk/rust/demo/src/main.rs | 122 - tuta-sdk/rust/sdk/Cargo.toml | 10 + .../rust/sdk/examples/get_default_folders.rs | 46 + tuta-sdk/rust/sdk/examples/http_request.rs | 27 + tuta-sdk/rust/sdk/src/crypto/crypto_facade.rs | 9 +- tuta-sdk/rust/sdk/src/crypto/key.rs | 12 +- tuta-sdk/rust/sdk/src/crypto_entity_client.rs | 4 + tuta-sdk/rust/sdk/src/entities.rs | 1 + .../rust/sdk/src/entities/size_estimator.rs | 687 +++++ tuta-sdk/rust/sdk/src/id/id_tuple.rs | 3 + tuta-sdk/rust/sdk/src/instance_mapper.rs | 20 +- tuta-sdk/rust/sdk/src/key_cache.rs | 2 +- tuta-sdk/rust/sdk/src/key_loader_facade.rs | 4 +- tuta-sdk/rust/sdk/src/lib.rs | 26 +- tuta-sdk/rust/sdk/src/mail_facade.rs | 61 + tuta-sdk/rust/sdk/src/tutanota_constants.rs | 15 + tuta-sdk/rust/sdk/src/user_facade.rs | 2 +- 210 files changed, 12826 insertions(+), 3179 deletions(-) create mode 100644 ipc-schema/facades/MailImportFacade.json create mode 100644 ipc-schema/types/TutaCredentials.json create mode 100644 packages/node-mimimi/.cargo/config.toml create mode 100644 packages/node-mimimi/.gitignore create mode 100644 packages/node-mimimi/.npmignore create mode 100644 packages/node-mimimi/Cargo.toml create mode 100644 packages/node-mimimi/README.md create mode 100644 packages/node-mimimi/build.rs create mode 100644 packages/node-mimimi/examples/import_imap_mail.rs create mode 100644 packages/node-mimimi/index.d.ts create mode 100644 packages/node-mimimi/java/build.gradle.kts create mode 100644 packages/node-mimimi/java/settings.gradle.kts create mode 100644 packages/node-mimimi/java/src/main/java/greenmailserver/GreenMailServer.java create mode 100644 packages/node-mimimi/make.js create mode 100644 packages/node-mimimi/npm/darwin-universal/README.md create mode 100644 packages/node-mimimi/npm/darwin-universal/package.json create mode 100644 packages/node-mimimi/npm/linux-x64-gnu/README.md create mode 100644 packages/node-mimimi/npm/linux-x64-gnu/package.json create mode 100644 packages/node-mimimi/npm/win32-x64-msvc/README.md create mode 100644 packages/node-mimimi/npm/win32-x64-msvc/package.json create mode 100644 packages/node-mimimi/package.json create mode 100644 packages/node-mimimi/rust-toolchain create mode 100644 packages/node-mimimi/rustfmt.toml create mode 100644 packages/node-mimimi/src/importer.rs create mode 100644 packages/node-mimimi/src/importer/extend_mail_parser.rs create mode 100644 packages/node-mimimi/src/importer/file_reader.rs create mode 100644 packages/node-mimimi/src/importer/file_reader/import_client.rs create mode 100644 packages/node-mimimi/src/importer/imap_reader.rs create mode 100644 packages/node-mimimi/src/importer/imap_reader/import_client.rs create mode 100644 packages/node-mimimi/src/importer/importable_mail.rs create mode 100644 packages/node-mimimi/src/importer/plain_text_to_html_converter.rs create mode 100644 packages/node-mimimi/src/importer/status.rs create mode 100644 packages/node-mimimi/src/lib.rs create mode 100644 packages/node-mimimi/src/logging.rs create mode 100644 packages/node-mimimi/src/logging/console.rs create mode 100644 packages/node-mimimi/src/logging/logger.rs create mode 100644 packages/node-mimimi/src/tuta.rs create mode 100644 packages/node-mimimi/src/tuta/credentials.rs create mode 100644 packages/node-mimimi/src/tuta_imap.rs create mode 100644 packages/node-mimimi/src/tuta_imap/client.rs create mode 100644 packages/node-mimimi/src/tuta_imap/client/tls_stream.rs create mode 100644 packages/node-mimimi/src/tuta_imap/client/types.rs create mode 100644 packages/node-mimimi/src/tuta_imap/testing.rs create mode 100644 packages/node-mimimi/src/tuta_imap/testing/jvm_singeleton.rs create mode 100644 packages/node-mimimi/src/tuta_imap/testing/utils.rs create mode 100644 packages/node-mimimi/src/tuta_imap/utils.rs create mode 100644 packages/node-mimimi/test/Suite.ts create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/2002_06_12_doublebound-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/2002_06_12_doublebound.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/README.md create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/ak-0696-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/ak-0696.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/attachment-filename-encoding-Latin1-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/attachment-filename-encoding-Latin1.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/attachment-filename-encoding-UTF8-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/attachment-filename-encoding-UTF8.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/badbound-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/badbound.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/badfile-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/badfile.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/bluedot-postcard-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/bluedot-postcard.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/bluedot-simple-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/bluedot-simple.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/double-bound-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/double-bound.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/double-semicolon-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/double-semicolon.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/double-semicolon2-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/double-semicolon2.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/dup-names-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/dup-names.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/empty-preamble-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/empty-preamble.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/frag-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/frag.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/german-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/german-qp-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/german-qp.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/german.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/hdr-fakeout-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/hdr-fakeout.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/infinite-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/infinite.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/intl-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/intl.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/jt-0498-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/jt-0498.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/lennie-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/lennie.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/mp-msg-rfc822-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/mp-msg-rfc822.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-2evil-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-2evil.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-2gifs-base64-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-2gifs-base64.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-2gifs-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-2gifs.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-bad-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-bad.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-badnames-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-badnames.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-clen-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-clen.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-digest-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-digest.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-frag-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-frag.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-igor-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-igor.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-igor2-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-igor2.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-nested-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-nested.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-nested2-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-nested2.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-nested3-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-nested3.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-simple-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-simple.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-weirdspace-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/multi-weirdspace.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/not-mime-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/not-mime.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/re-fwd-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/re-fwd.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/russian-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/russian.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/sig-uu-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/sig-uu.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/simple-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/simple.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/ticket-60931-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/ticket-60931.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/twopart-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/twopart.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/uu-junk-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/uu-junk-target-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/uu-junk-target.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/uu-junk.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/uu-zeegee-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/uu-zeegee.msg create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/x-gzip64-expected.json create mode 100644 packages/node-mimimi/test/mimetools-testmsgs/x-gzip64.msg create mode 100644 packages/node-mimimi/test/sample.eml create mode 100644 packages/node-mimimi/test/tsconfig.json create mode 100644 packages/node-mimimi/tsconfig.json create mode 100644 src/common/desktop/mailimport/DesktopMailImportFacade.ts create mode 100644 src/common/desktop/mailimport/MailImporter.ts create mode 100644 src/common/native/common/generatedipc/MailImportFacade.ts create mode 100644 src/common/native/common/generatedipc/MailImportFacadeReceiveDispatcher.ts create mode 100644 src/common/native/common/generatedipc/MailImportFacadeSendDispatcher.ts create mode 100644 src/common/native/common/generatedipc/TutaCredentials.ts delete mode 100644 tuta-sdk/rust/demo/Cargo.toml delete mode 100644 tuta-sdk/rust/demo/src/main.rs create mode 100644 tuta-sdk/rust/sdk/examples/get_default_folders.rs create mode 100644 tuta-sdk/rust/sdk/examples/http_request.rs create mode 100644 tuta-sdk/rust/sdk/src/entities/size_estimator.rs diff --git a/app-android/tutashared/src/main/java/de/tutao/tutashared/generated_ipc/NativeCryptoFacadeReceiveDispatcher.kt b/app-android/tutashared/src/main/java/de/tutao/tutashared/generated_ipc/NativeCryptoFacadeReceiveDispatcher.kt index 27933003084..733a50f1125 100644 --- a/app-android/tutashared/src/main/java/de/tutao/tutashared/generated_ipc/NativeCryptoFacadeReceiveDispatcher.kt +++ b/app-android/tutashared/src/main/java/de/tutao/tutashared/generated_ipc/NativeCryptoFacadeReceiveDispatcher.kt @@ -2,17 +2,16 @@ @file:Suppress("NAME_SHADOWING") - package de.tutao.tutashared.ipc -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json +import kotlinx.serialization.* +import kotlinx.serialization.json.* class NativeCryptoFacadeReceiveDispatcher( private val json: Json, private val facade: NativeCryptoFacade, ) { - + suspend fun dispatch(method: String, arg: List): String { when (method) { "rsaEncrypt" -> { @@ -26,7 +25,6 @@ class NativeCryptoFacadeReceiveDispatcher( ) return json.encodeToString(result) } - "rsaDecrypt" -> { val privateKey: RsaPrivateKey = json.decodeFromString(arg[0]) val data: DataWrapper = json.decodeFromString(arg[1]) @@ -36,7 +34,6 @@ class NativeCryptoFacadeReceiveDispatcher( ) return json.encodeToString(result) } - "aesEncryptFile" -> { val key: DataWrapper = json.decodeFromString(arg[0]) val fileUri: String = json.decodeFromString(arg[1]) @@ -48,7 +45,6 @@ class NativeCryptoFacadeReceiveDispatcher( ) return json.encodeToString(result) } - "aesDecryptFile" -> { val key: DataWrapper = json.decodeFromString(arg[0]) val fileUri: String = json.decodeFromString(arg[1]) @@ -58,7 +54,6 @@ class NativeCryptoFacadeReceiveDispatcher( ) return json.encodeToString(result) } - "argon2idGeneratePassphraseKey" -> { val passphrase: String = json.decodeFromString(arg[0]) val salt: DataWrapper = json.decodeFromString(arg[1]) @@ -68,7 +63,6 @@ class NativeCryptoFacadeReceiveDispatcher( ) return json.encodeToString(result) } - "generateKyberKeypair" -> { val seed: DataWrapper = json.decodeFromString(arg[0]) val result: KyberKeyPair = this.facade.generateKyberKeypair( @@ -76,7 +70,6 @@ class NativeCryptoFacadeReceiveDispatcher( ) return json.encodeToString(result) } - "kyberEncapsulate" -> { val publicKey: KyberPublicKey = json.decodeFromString(arg[0]) val seed: DataWrapper = json.decodeFromString(arg[1]) @@ -86,7 +79,6 @@ class NativeCryptoFacadeReceiveDispatcher( ) return json.encodeToString(result) } - "kyberDecapsulate" -> { val privateKey: KyberPrivateKey = json.decodeFromString(arg[0]) val ciphertext: DataWrapper = json.decodeFromString(arg[1]) @@ -96,7 +88,6 @@ class NativeCryptoFacadeReceiveDispatcher( ) return json.encodeToString(result) } - else -> throw Error("unknown method for NativeCryptoFacade: $method") } } diff --git a/app-android/tutashared/src/main/java/de/tutao/tutashared/generated_ipc/PersistedCredentials.kt b/app-android/tutashared/src/main/java/de/tutao/tutashared/generated_ipc/PersistedCredentials.kt index d53164af879..4fecd524443 100644 --- a/app-android/tutashared/src/main/java/de/tutao/tutashared/generated_ipc/PersistedCredentials.kt +++ b/app-android/tutashared/src/main/java/de/tutao/tutashared/generated_ipc/PersistedCredentials.kt @@ -3,7 +3,8 @@ package de.tutao.tutashared.ipc -import kotlinx.serialization.Serializable +import kotlinx.serialization.* +import kotlinx.serialization.json.* /** diff --git a/app-android/tutashared/src/main/java/de/tutao/tutashared/generated_ipc/UnencryptedCredentials.kt b/app-android/tutashared/src/main/java/de/tutao/tutashared/generated_ipc/UnencryptedCredentials.kt index c031d4109b7..20170615e6a 100644 --- a/app-android/tutashared/src/main/java/de/tutao/tutashared/generated_ipc/UnencryptedCredentials.kt +++ b/app-android/tutashared/src/main/java/de/tutao/tutashared/generated_ipc/UnencryptedCredentials.kt @@ -3,7 +3,8 @@ package de.tutao.tutashared.ipc -import kotlinx.serialization.Serializable +import kotlinx.serialization.* +import kotlinx.serialization.json.* /** diff --git a/buildSrc/DesktopBuilder.js b/buildSrc/DesktopBuilder.js index 8e720566d07..08c94619ec5 100644 --- a/buildSrc/DesktopBuilder.js +++ b/buildSrc/DesktopBuilder.js @@ -121,7 +121,10 @@ async function rollupDesktop(dirname, outDir, version, platform, architecture, d input: [path.join(dirname, "src/common/desktop/DesktopMain.ts"), path.join(dirname, "src/common/desktop/sqlworker.ts")], // some transitive dep of a transitive dev-dep requires https://www.npmjs.com/package/url // which rollup for some reason won't distinguish from the node builtin. - external: ["url", "util", "path", "fs", "os", "http", "https", "crypto", "child_process", "electron"], + external: (id, parent, isResolved) => { + if (parent != null && parent.endsWith("node-mimimi/dist/binding.cjs")) return true + return ["url", "util", "path", "fs", "os", "http", "https", "crypto", "child_process", "electron"].includes(id) + }, preserveEntrySignatures: false, plugins: [ copyNativeModulePlugin({ @@ -131,6 +134,16 @@ async function rollupDesktop(dirname, outDir, version, platform, architecture, d architecture, nodeModule: "better-sqlite3", }), + { + // todo: this needs to work everywhere + name: "copy-mimimi-plugin", + async buildStart() { + const normalDst = path.join(path.normalize("./build/desktop/"), "node-mimimi.linux-x64-gnu.node") + const dstDir = path.dirname(normalDst) + await fs.promises.mkdir(dstDir, { recursive: true }) + await fs.promises.copyFile("./packages/node-mimimi/dist/node-mimimi.linux-x64-gnu.node", normalDst) + }, + }, typescript({ tsconfig: "tsconfig.json", outDir, diff --git a/buildSrc/DevBuild.js b/buildSrc/DevBuild.js index 298142e7f79..203a9e6f691 100644 --- a/buildSrc/DevBuild.js +++ b/buildSrc/DevBuild.js @@ -4,7 +4,7 @@ import { build as esbuild } from "esbuild" import { getTutanotaAppVersion, runStep, writeFile } from "./buildUtils.js" import "zx/globals" import * as env from "./env.js" -import { externalTranslationsPlugin, libDeps, preludeEnvPlugin, sqliteNativePlugin } from "./esbuildUtils.js" +import { externalTranslationsPlugin, libDeps, mimimiNativePlugin, preludeEnvPlugin, sqliteNativePlugin } from "./esbuildUtils.js" import { fileURLToPath } from "node:url" import * as LaunchHtml from "./LaunchHtml.js" import os from "node:os" @@ -173,9 +173,9 @@ async function buildDesktopPart({ version, app }) { format: "cjs", sourcemap: "linked", platform: "node", - external: ["electron"], + external: ["electron", "*.node"], banner: { - js: `globalThis.buildOptions = globalThis.buildOptions ?? {} + js: `globalThis.buildOptions = globalThis.buildOptions ?? {} globalThis.buildOptions.sqliteNativePath = "./better-sqlite3.node";`, }, plugins: [ @@ -187,6 +187,10 @@ globalThis.buildOptions.sqliteNativePath = "./better-sqlite3.node";`, architecture: process.arch, nativeBindingPath: "./better_sqlite3.node", }), + mimimiNativePlugin({ + dstPath: `./${buildDir}/desktop/`, + platform: process.platform, + }), preludeEnvPlugin(env.create({ staticUrl: null, version, mode: "Desktop", dist: false, domainConfigs })), externalTranslationsPlugin(), ], diff --git a/buildSrc/esbuildUtils.js b/buildSrc/esbuildUtils.js index 172d64f864e..28c61b127c7 100644 --- a/buildSrc/esbuildUtils.js +++ b/buildSrc/esbuildUtils.js @@ -6,7 +6,7 @@ import { aliasPath as esbuildPluginAliasPath } from "esbuild-plugin-alias-path" /** * Little plugin that obtains compiled better-sqlite3, copies it to dstPath and sets the path to nativeBindingPath. - * We do not use default file loader from esbuild, it is much simpler and reliable to do it manually and it doesn't work for dynamic import (like in this case) + * We do not use default file loader from esbuild, it is much simpler and reliable to do it manually, and it doesn't work for dynamic import (like in this case) * anyway. * It will also replace `buildOptions.sqliteNativePath` with the nativeBindingPath */ @@ -37,6 +37,41 @@ export function sqliteNativePlugin({ environment, dstPath, nativeBindingPath, pl } } +export function mimimiNativePlugin({ dstPath, platform }) { + return { + name: "mimimi-native-plugin", + setup(build) { + const options = build.initialOptions + options.define = options.define ?? {} + + build.onStart(async () => { + let nativeBinaryName + switch (platform) { + case "linux": + nativeBinaryName = "node-mimimi.linux-x64-gnu.node" + break + case "win32": + nativeBinaryName = "node-mimimi.win32-x64-msvc.node" + break + case "darwin": + nativeBinaryName = "node-mimimi.darwin-universal.node" + break + default: + throw Error(`could not find node-mimimi binary: platform ${platform} is unknown`) + } + + // Replace mentions of buildOptions.mimimiNativePath with the actual path + options.define["buildOptions.mimimiNativePath"] = `"./${nativeBinaryName}"` + + const nativeBinarySourcePath = path.join(process.cwd(), "./packages/node-mimimi/dist", nativeBinaryName) + + await fs.promises.mkdir(path.dirname(dstPath), { recursive: true }) + await fs.promises.copyFile(nativeBinarySourcePath, path.join(process.cwd(), dstPath, nativeBinaryName)) + }) + }, + } +} + /** Little plugin that replaces imports for libs from dependencyMap with their prebuilt versions in libs directory. */ export function libDeps(prefix = ".") { const absoluteDependencyMap = Object.fromEntries( diff --git a/buildSrc/packageBuilderFunctions.js b/buildSrc/packageBuilderFunctions.js index a8bddf024f7..917cb938206 100644 --- a/buildSrc/packageBuilderFunctions.js +++ b/buildSrc/packageBuilderFunctions.js @@ -12,7 +12,7 @@ export async function buildRuntimePackages() { // tsconfig is rather JSON5, if it becomes a problem switch to JSON5 parser here const tsconfig = JSON.parse(await fs.readFile("tsconfig.json", { encoding: "utf-8" })) const packagePaths = tsconfig.references.map((ref) => ref.path) - await $`npx tsc -b ${packagePaths}` + await Promise.all(packagePaths.map((dir) => $`cd ${dir} && npm run build`)) } /** @@ -20,5 +20,5 @@ export async function buildRuntimePackages() { */ export async function buildPackages(pathPrefix = ".") { const packages = await glob(`${pathPrefix}/packages/*`, { deep: 1, onlyDirectories: true }) - await $`npx tsc -b ${packages}` + await Promise.all(packages.map((dir) => $`cd ${dir} && npm run build`)) } diff --git a/ipc-schema/facades/MailImportFacade.json b/ipc-schema/facades/MailImportFacade.json new file mode 100644 index 00000000000..6f3c6f0465e --- /dev/null +++ b/ipc-schema/facades/MailImportFacade.json @@ -0,0 +1,52 @@ +{ + "name": "MailImportFacade", + "type": "facade", + "senders": ["web"], + "receivers": ["desktop"], + "doc": "Facade implemented by the native desktop client enabling mail imports, both from files, and via IMAP.", + "methods": { + "setupImapImport": { + "doc": "Initializing an IMAP import.", + "arg": [ + { + "apiUrl": "string" + }, + { + "unencryptedTutaCredentials": "UnencryptedCredentials" + } + ], + "ret": "void" + }, + "startImapImport": { + "doc": "Start an IMAP import.", + "arg": [], + "ret": "void" + }, + "stopImapImport": { + "doc": "Stop a running IMAP import.", + "arg": [], + "ret": "void" + }, + "importFromFiles": { + "doc": "Import multiple mails from .eml or .mbox files.", + "arg": [ + { + "apiUrl": "string" + }, + { + "unencryptedTutaCredentials": "UnencryptedCredentials" + }, + { + "targetOwnerGroup": "string" + }, + { + "targetFolder": "List" + }, + { + "filePaths": "List" + } + ], + "ret": "string" + } + } +} diff --git a/ipc-schema/types/MailBundle.json b/ipc-schema/types/MailBundle.json index 1b0a4fd50b5..5339e21ebec 100644 --- a/ipc-schema/types/MailBundle.json +++ b/ipc-schema/types/MailBundle.json @@ -2,6 +2,6 @@ "name": "MailBundle", "type": "typeref", "location": { - "typescript": "../src/mail-app/mail/export/Bundler.js" + "typescript": "../src/common/mailFunctionality/SharedMailUtils.js" } } diff --git a/ipc-schema/types/TutaCredentials.json b/ipc-schema/types/TutaCredentials.json new file mode 100644 index 00000000000..b58639378f5 --- /dev/null +++ b/ipc-schema/types/TutaCredentials.json @@ -0,0 +1,7 @@ +{ + "name": "TutaCredentials", + "type": "typeref", + "location": { + "typescript": "../packages/node-mimimi/dist/binding.cjs" + } +} diff --git a/libs/electron-updater.mjs b/libs/electron-updater.mjs index 6bf6bb563e3..fd45915a411 100644 --- a/libs/electron-updater.mjs +++ b/libs/electron-updater.mjs @@ -11584,46 +11584,54 @@ const coerce$1 = (version, options) => { }; var coerce_1 = coerce$1; -class LRUCache { - constructor () { - this.max = 1000; - this.map = new Map(); - } +var lrucache; +var hasRequiredLrucache; + +function requireLrucache () { + if (hasRequiredLrucache) return lrucache; + hasRequiredLrucache = 1; + class LRUCache { + constructor () { + this.max = 1000; + this.map = new Map(); + } - get (key) { - const value = this.map.get(key); - if (value === undefined) { - return undefined - } else { - // Remove the key from the map and add it to the end - this.map.delete(key); - this.map.set(key, value); - return value - } - } + get (key) { + const value = this.map.get(key); + if (value === undefined) { + return undefined + } else { + // Remove the key from the map and add it to the end + this.map.delete(key); + this.map.set(key, value); + return value + } + } - delete (key) { - return this.map.delete(key) - } + delete (key) { + return this.map.delete(key) + } - set (key, value) { - const deleted = this.delete(key); + set (key, value) { + const deleted = this.delete(key); - if (!deleted && value !== undefined) { - // If cache is full, delete the least recently used item - if (this.map.size >= this.max) { - const firstKey = this.map.keys().next().value; - this.delete(firstKey); - } + if (!deleted && value !== undefined) { + // If cache is full, delete the least recently used item + if (this.map.size >= this.max) { + const firstKey = this.map.keys().next().value; + this.delete(firstKey); + } - this.map.set(key, value); - } + this.map.set(key, value); + } - return this - } -} + return this + } + } -var lrucache = LRUCache; + lrucache = LRUCache; + return lrucache; +} var range; var hasRequiredRange; @@ -11845,7 +11853,7 @@ function requireRange () { range = Range; - const LRU = lrucache; + const LRU = requireLrucache(); const cache = new LRU(); const parseOptions = parseOptions_1; diff --git a/package-lock.json b/package-lock.json index ccde90023e8..02ca6b3f7ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "./packages/*" ], "dependencies": { + "@tutao/node-mimimi": "251.241030.0", "@tutao/oxmsg": "0.0.9-beta.0", "@tutao/tuta-wasm-loader": "251.241113.0", "@tutao/tutanota-crypto": "251.241113.0", @@ -94,8 +95,6 @@ }, "node_modules/@babel/code-frame": { "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", - "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", "dev": true, "license": "MIT", "dependencies": { @@ -108,8 +107,6 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", - "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", "dev": true, "license": "MIT", "engines": { @@ -118,8 +115,6 @@ }, "node_modules/@babel/highlight": { "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", - "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", "dev": true, "license": "MIT", "dependencies": { @@ -134,8 +129,6 @@ }, "node_modules/@babel/highlight/node_modules/ansi-styles": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "license": "MIT", "dependencies": { @@ -147,8 +140,6 @@ }, "node_modules/@babel/highlight/node_modules/chalk": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -162,8 +153,6 @@ }, "node_modules/@babel/highlight/node_modules/color-convert": { "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "license": "MIT", "dependencies": { @@ -172,15 +161,11 @@ }, "node_modules/@babel/highlight/node_modules/color-name": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true, "license": "MIT" }, "node_modules/@babel/highlight/node_modules/escape-string-regexp": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "license": "MIT", "engines": { @@ -189,8 +174,6 @@ }, "node_modules/@babel/highlight/node_modules/has-flag": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, "license": "MIT", "engines": { @@ -199,8 +182,6 @@ }, "node_modules/@babel/highlight/node_modules/supports-color": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "license": "MIT", "dependencies": { @@ -212,8 +193,6 @@ }, "node_modules/@develar/schema-utils": { "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", "dev": true, "license": "MIT", "dependencies": { @@ -230,8 +209,6 @@ }, "node_modules/@electron/asar": { "version": "3.2.13", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.13.tgz", - "integrity": "sha512-pY5z2qQSwbFzJsBdgfJIzXf5ElHTVMutC2dxh0FD60njknMu3n1NnTABOcQwbb5/v5soqE79m9UjaJryBf3epg==", "dev": true, "license": "MIT", "dependencies": { @@ -249,8 +226,6 @@ }, "node_modules/@electron/asar/node_modules/brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -260,8 +235,6 @@ }, "node_modules/@electron/asar/node_modules/commander": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", "dev": true, "license": "MIT", "engines": { @@ -270,9 +243,6 @@ }, "node_modules/@electron/asar/node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", "dependencies": { @@ -292,8 +262,6 @@ }, "node_modules/@electron/asar/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -305,8 +273,6 @@ }, "node_modules/@electron/get": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", "license": "MIT", "dependencies": { "debug": "^4.1.1", @@ -326,8 +292,6 @@ }, "node_modules/@electron/get/node_modules/fs-extra": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -340,8 +304,6 @@ }, "node_modules/@electron/get/node_modules/jsonfile": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" @@ -349,8 +311,6 @@ }, "node_modules/@electron/get/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -358,8 +318,6 @@ }, "node_modules/@electron/get/node_modules/universalify": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "license": "MIT", "engines": { "node": ">= 4.0.0" @@ -367,8 +325,6 @@ }, "node_modules/@electron/notarize": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.4.0.tgz", - "integrity": "sha512-ArHnRPIJJGrmV+uWNQSINAht+cM4gAo3uA3WFI54bYF93mzmD15gzhPQ0Dd+v/fkMhnRiiIO8NNkGdn87Vsy0g==", "dev": true, "license": "MIT", "dependencies": { @@ -382,8 +338,6 @@ }, "node_modules/@electron/notarize/node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -398,8 +352,6 @@ }, "node_modules/@electron/osx-sign": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.0.5.tgz", - "integrity": "sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -420,8 +372,6 @@ }, "node_modules/@electron/osx-sign/node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "dependencies": { @@ -435,8 +385,6 @@ }, "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", "dev": true, "license": "MIT", "engines": { @@ -448,8 +396,6 @@ }, "node_modules/@electron/universal": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.5.1.tgz", - "integrity": "sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==", "dev": true, "license": "MIT", "dependencies": { @@ -467,8 +413,6 @@ }, "node_modules/@electron/universal/node_modules/brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -478,8 +422,6 @@ }, "node_modules/@electron/universal/node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -494,8 +436,6 @@ }, "node_modules/@electron/universal/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -505,282 +445,8 @@ "node": "*" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/linux-x64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", - "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", "cpu": [ "x64" ], @@ -794,129 +460,8 @@ "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", - "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dev": true, "license": "MIT", "dependencies": { @@ -931,8 +476,6 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", - "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", "dev": true, "license": "MIT", "engines": { @@ -941,8 +484,6 @@ }, "node_modules/@eslint/eslintrc": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "license": "MIT", "dependencies": { @@ -965,8 +506,6 @@ }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -976,8 +515,6 @@ }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -989,8 +526,6 @@ }, "node_modules/@eslint/js": { "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "license": "MIT", "engines": { @@ -999,9 +534,6 @@ }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "deprecated": "Use @eslint/config-array instead", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1015,8 +547,6 @@ }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -1026,8 +556,6 @@ }, "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -1039,8 +567,6 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1053,16 +579,11 @@ }, "node_modules/@humanwhocodes/object-schema": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, "license": "ISC", "dependencies": { @@ -1079,8 +600,6 @@ }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", "engines": { @@ -1092,15 +611,11 @@ }, "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "license": "MIT", "dependencies": { @@ -1117,8 +632,6 @@ }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1133,8 +646,6 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "license": "MIT", "dependencies": { @@ -1148,8 +659,6 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", "engines": { @@ -1158,8 +667,6 @@ }, "node_modules/@jridgewell/set-array": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "license": "MIT", "engines": { @@ -1168,8 +675,6 @@ }, "node_modules/@jridgewell/source-map": { "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1179,15 +684,11 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1197,8 +698,6 @@ }, "node_modules/@malept/cross-spawn-promise": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", - "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", "dev": true, "funding": [ { @@ -1220,8 +719,6 @@ }, "node_modules/@malept/flatpak-bundler": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", - "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1236,8 +733,6 @@ }, "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1250,10 +745,25 @@ "node": ">=10" } }, + "node_modules/@napi-rs/cli": { + "version": "2.18.4", + "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.4.tgz", + "integrity": "sha512-SgJeA4df9DE2iAEpr3M2H0OKl/yjtg1BnRI5/JyowS71tUWhrfSu2LT0V3vlHET+g1hBVlrO60PmEXwUEKp8Mg==", + "dev": true, + "license": "MIT", + "bin": { + "napi": "scripts/index.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -1265,8 +775,6 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "license": "MIT", "engines": { "node": ">= 8" @@ -1274,8 +782,6 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -1287,8 +793,6 @@ }, "node_modules/@npmcli/agent": { "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", - "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", "dev": true, "license": "ISC", "dependencies": { @@ -1304,8 +808,6 @@ }, "node_modules/@npmcli/agent/node_modules/agent-base": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, "license": "MIT", "dependencies": { @@ -1317,8 +819,6 @@ }, "node_modules/@npmcli/agent/node_modules/http-proxy-agent": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", "dependencies": { @@ -1331,8 +831,6 @@ }, "node_modules/@npmcli/agent/node_modules/https-proxy-agent": { "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "license": "MIT", "dependencies": { @@ -1345,15 +843,11 @@ }, "node_modules/@npmcli/agent/node_modules/lru-cache": { "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, "license": "ISC" }, "node_modules/@npmcli/fs": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", - "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", "dev": true, "license": "ISC", "dependencies": { @@ -1365,8 +859,6 @@ }, "node_modules/@octokit/app": { "version": "15.1.0", - "resolved": "https://registry.npmjs.org/@octokit/app/-/app-15.1.0.tgz", - "integrity": "sha512-TkBr7QgOmE6ORxvIAhDbZsqPkF7RSqTY4pLTtUQCvr6dTXqvi2fFo46q3h1lxlk/sGMQjqyZ0kEahkD/NyzOHg==", "dev": true, "license": "MIT", "dependencies": { @@ -1384,8 +876,6 @@ }, "node_modules/@octokit/auth-app": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-7.1.1.tgz", - "integrity": "sha512-kRAd6yelV9OgvlEJE88H0VLlQdZcag9UlLr7dV0YYP37X8PPDvhgiTy66QVhDXdyoT0AleFN2w/qXkPdrSzINg==", "dev": true, "license": "MIT", "dependencies": { @@ -1404,15 +894,11 @@ }, "node_modules/@octokit/auth-app/node_modules/lru-cache": { "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, "license": "ISC" }, "node_modules/@octokit/auth-oauth-app": { "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-8.1.1.tgz", - "integrity": "sha512-5UtmxXAvU2wfcHIPPDWzVSAWXVJzG3NWsxb7zCFplCWEmMCArSZV0UQu5jw5goLQXbFyOr5onzEH37UJB3zQQg==", "dev": true, "license": "MIT", "dependencies": { @@ -1428,8 +914,6 @@ }, "node_modules/@octokit/auth-oauth-device": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-7.1.1.tgz", - "integrity": "sha512-HWl8lYueHonuyjrKKIup/1tiy0xcmQCdq5ikvMO1YwkNNkxb6DXfrPjrMYItNLyCP/o2H87WuijuE+SlBTT8eg==", "dev": true, "license": "MIT", "dependencies": { @@ -1444,8 +928,6 @@ }, "node_modules/@octokit/auth-oauth-user": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-5.1.1.tgz", - "integrity": "sha512-rRkMz0ErOppdvEfnemHJXgZ9vTPhBuC6yASeFaB7I2yLMd7QpjfrL1mnvRPlyKo+M6eeLxrKanXJ9Qte29SRsw==", "dev": true, "license": "MIT", "dependencies": { @@ -1461,8 +943,6 @@ }, "node_modules/@octokit/auth-token": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", - "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==", "dev": true, "license": "MIT", "engines": { @@ -1471,8 +951,6 @@ }, "node_modules/@octokit/auth-unauthenticated": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-6.1.0.tgz", - "integrity": "sha512-zPSmfrUAcspZH/lOFQnVnvjQZsIvmfApQH6GzJrkIunDooU1Su2qt2FfMTSVPRp7WLTQyC20Kd55lF+mIYaohQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1485,8 +963,6 @@ }, "node_modules/@octokit/core": { "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", - "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", "dev": true, "license": "MIT", "dependencies": { @@ -1504,8 +980,6 @@ }, "node_modules/@octokit/endpoint": { "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", - "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1518,8 +992,6 @@ }, "node_modules/@octokit/graphql": { "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz", - "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==", "dev": true, "license": "MIT", "dependencies": { @@ -1533,8 +1005,6 @@ }, "node_modules/@octokit/oauth-app": { "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-7.1.3.tgz", - "integrity": "sha512-EHXbOpBkSGVVGF1W+NLMmsnSsJRkcrnVmDKt0TQYRBb6xWfWzoi9sBD4DIqZ8jGhOWO/V8t4fqFyJ4vDQDn9bg==", "dev": true, "license": "MIT", "dependencies": { @@ -1553,8 +1023,6 @@ }, "node_modules/@octokit/oauth-authorization-url": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-7.1.1.tgz", - "integrity": "sha512-ooXV8GBSabSWyhLUowlMIVd9l1s2nsOGQdlP2SQ4LnkEsGXzeCvbSbCPdZThXhEFzleGPwbapT0Sb+YhXRyjCA==", "dev": true, "license": "MIT", "engines": { @@ -1563,8 +1031,6 @@ }, "node_modules/@octokit/oauth-methods": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-5.1.2.tgz", - "integrity": "sha512-C5lglRD+sBlbrhCUTxgJAFjWgJlmTx5bQ7Ch0+2uqRjYv7Cfb5xpX4WuSC9UgQna3sqRGBL9EImX9PvTpMaQ7g==", "dev": true, "license": "MIT", "dependencies": { @@ -1579,22 +1045,16 @@ }, "node_modules/@octokit/openapi-types": { "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", - "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", "dev": true, "license": "MIT" }, "node_modules/@octokit/openapi-webhooks-types": { "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-webhooks-types/-/openapi-webhooks-types-8.3.0.tgz", - "integrity": "sha512-vKLsoR4xQxg4Z+6rU/F65ItTUz/EXbD+j/d4mlq2GW8TsA4Tc8Kdma2JTAAJ5hrKWUQzkR/Esn2fjsqiVRYaQg==", "dev": true, "license": "MIT" }, "node_modules/@octokit/plugin-paginate-graphql": { "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-5.2.3.tgz", - "integrity": "sha512-EzFueuXVU3VHv5FwEXbdznn9EmyF0vA5LGDX6a8fJ9YJAlDgdYHRKJMO4Ghl2PPPJBxIPMDUJMnlUHqcvP7AnQ==", "dev": true, "license": "MIT", "engines": { @@ -1606,8 +1066,6 @@ }, "node_modules/@octokit/plugin-paginate-rest": { "version": "11.3.5", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.5.tgz", - "integrity": "sha512-cgwIRtKrpwhLoBi0CUNuY83DPGRMaWVjqVI/bGKsLJ4PzyWZNaEmhHroI2xlrVXkk6nFv0IsZpOp+ZWSWUS2AQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1622,8 +1080,6 @@ }, "node_modules/@octokit/plugin-request-log": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz", - "integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==", "dev": true, "license": "MIT", "engines": { @@ -1635,8 +1091,6 @@ }, "node_modules/@octokit/plugin-rest-endpoint-methods": { "version": "13.2.6", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.6.tgz", - "integrity": "sha512-wMsdyHMjSfKjGINkdGKki06VEkgdEldIGstIEyGX0wbYHGByOwN/KiM+hAAlUwAtPkP3gvXtVQA9L3ITdV2tVw==", "dev": true, "license": "MIT", "dependencies": { @@ -1651,8 +1105,6 @@ }, "node_modules/@octokit/plugin-retry": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-7.1.2.tgz", - "integrity": "sha512-XOWnPpH2kJ5VTwozsxGurw+svB2e61aWlmk5EVIYZPwFK5F9h4cyPyj9CIKRyMXMHSwpIsI3mPOdpMmrRhe7UQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1669,8 +1121,6 @@ }, "node_modules/@octokit/plugin-throttling": { "version": "9.3.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-9.3.2.tgz", - "integrity": "sha512-FqpvcTpIWFpMMwIeSoypoJXysSAQ3R+ALJhXXSG1HTP3YZOIeLmcNcimKaXxTcws+Sh6yoRl13SJ5r8sXc1Fhw==", "dev": true, "license": "MIT", "dependencies": { @@ -1686,8 +1136,6 @@ }, "node_modules/@octokit/request": { "version": "9.1.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", - "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", "dev": true, "license": "MIT", "dependencies": { @@ -1702,8 +1150,6 @@ }, "node_modules/@octokit/request-error": { "version": "6.1.5", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz", - "integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1715,8 +1161,6 @@ }, "node_modules/@octokit/rest": { "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.0.2.tgz", - "integrity": "sha512-+CiLisCoyWmYicH25y1cDfCrv41kRSvTq6pPWtRroRJzhsCZWZyCqGyI8foJT5LmScADSwRAnr/xo+eewL04wQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1731,8 +1175,6 @@ }, "node_modules/@octokit/types": { "version": "13.6.1", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", - "integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", "dev": true, "license": "MIT", "dependencies": { @@ -1741,8 +1183,6 @@ }, "node_modules/@octokit/webhooks": { "version": "13.3.0", - "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-13.3.0.tgz", - "integrity": "sha512-TUkJLtI163Bz5+JK0O+zDkQpn4gKwN+BovclUvCj6pI/6RXrFqQvUMRS2M+Rt8Rv0qR3wjoMoOPmpJKeOh0nBg==", "dev": true, "license": "MIT", "dependencies": { @@ -1756,8 +1196,6 @@ }, "node_modules/@octokit/webhooks-methods": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-5.1.0.tgz", - "integrity": "sha512-yFZa3UH11VIxYnnoOYCVoJ3q4ChuSOk2IVBBQ0O3xtKX4x9bmKb/1t+Mxixv2iUhzMdOl1qeWJqEhouXXzB3rQ==", "dev": true, "license": "MIT", "engines": { @@ -1766,8 +1204,6 @@ }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, "license": "MIT", "optional": true, @@ -1777,8 +1213,6 @@ }, "node_modules/@rollup/plugin-commonjs": { "version": "26.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-26.0.1.tgz", - "integrity": "sha512-UnsKoZK6/aGIH6AdkptXhNvhaqftcjq3zZdT+LY5Ftms6JR06nADcDsYp5hTU9E2lbJUEOhdlY5J4DNTneM+jQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1803,8 +1237,6 @@ }, "node_modules/@rollup/plugin-json": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", - "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", "dev": true, "license": "MIT", "dependencies": { @@ -1824,8 +1256,6 @@ }, "node_modules/@rollup/plugin-node-resolve": { "version": "15.2.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", - "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1850,8 +1280,6 @@ }, "node_modules/@rollup/plugin-terser": { "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", - "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", "dev": true, "license": "MIT", "dependencies": { @@ -1873,8 +1301,6 @@ }, "node_modules/@rollup/plugin-typescript": { "version": "11.1.6", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.6.tgz", - "integrity": "sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==", "dev": true, "license": "MIT", "dependencies": { @@ -1900,8 +1326,6 @@ }, "node_modules/@rollup/pluginutils": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", - "integrity": "sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==", "dev": true, "license": "MIT", "dependencies": { @@ -1921,164 +1345,8 @@ } } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", - "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", - "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", - "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", - "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", - "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", - "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", - "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", - "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", - "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", - "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", - "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", - "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", "cpu": [ "x64" ], @@ -2091,8 +1359,6 @@ }, "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", - "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", "cpu": [ "x64" ], @@ -2103,52 +1369,8 @@ "linux" ] }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", - "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", - "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", - "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@sindresorhus/is": { "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "license": "MIT", "engines": { "node": ">=10" @@ -2159,8 +1381,6 @@ }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", "license": "MIT", "dependencies": { "defer-to-connect": "^2.0.0" @@ -2171,8 +1391,6 @@ }, "node_modules/@tootallnate/once": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "dev": true, "license": "MIT", "engines": { @@ -2183,14 +1401,16 @@ "resolved": "packages/licc", "link": true }, + "node_modules/@tutao/node-mimimi": { + "resolved": "packages/node-mimimi", + "link": true + }, "node_modules/@tutao/otest": { "resolved": "packages/otest", "link": true }, "node_modules/@tutao/oxmsg": { "version": "0.0.9-beta.0", - "resolved": "https://registry.npmjs.org/@tutao/oxmsg/-/oxmsg-0.0.9-beta.0.tgz", - "integrity": "sha512-h091LekQ4ZjxBsW1n5ZDZlYdSdmWHIc8r51L2NtPoQvq6vHl4J04Pm2xlfk1YeMHGnzsdKhniPnnHekwzVa6/Q==", "license": "MIT", "dependencies": { "address-rfc2822": "^2.0.6", @@ -2226,15 +1446,11 @@ }, "node_modules/@types/aws-lambda": { "version": "8.10.145", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.145.tgz", - "integrity": "sha512-dtByW6WiFk5W5Jfgz1VM+YPA21xMXTuSFoLYIDY0L44jDLLflVPtZkYuu3/YxpGcvjzKFBZLU+GyKjR0HOYtyw==", "dev": true, "license": "MIT" }, "node_modules/@types/better-sqlite3": { "version": "7.4.2", - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.4.2.tgz", - "integrity": "sha512-HUXWMOmRgOrXJ0SKt6kxqUaZtGkr0HCuaEt/76LojT6bkTu0lb0uhr3K1su9T09mskDKyQwNMvT7WithFN10PQ==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -2242,8 +1458,6 @@ }, "node_modules/@types/body-parser": { "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", "dev": true, "license": "MIT", "dependencies": { @@ -2253,8 +1467,6 @@ }, "node_modules/@types/cacheable-request": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", "license": "MIT", "dependencies": { "@types/http-cache-semantics": "*", @@ -2265,8 +1477,6 @@ }, "node_modules/@types/connect": { "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dev": true, "license": "MIT", "dependencies": { @@ -2275,8 +1485,6 @@ }, "node_modules/@types/debug": { "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2285,8 +1493,6 @@ }, "node_modules/@types/dompurify": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", - "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", "license": "MIT", "dependencies": { "@types/trusted-types": "*" @@ -2294,15 +1500,11 @@ }, "node_modules/@types/estree": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true, "license": "MIT" }, "node_modules/@types/express": { "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2314,8 +1516,6 @@ }, "node_modules/@types/express-serve-static-core": { "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", "dev": true, "license": "MIT", "dependencies": { @@ -2327,8 +1527,6 @@ }, "node_modules/@types/fs-extra": { "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -2336,8 +1534,6 @@ }, "node_modules/@types/glob": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", "dev": true, "license": "MIT", "dependencies": { @@ -2347,28 +1543,20 @@ }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", "license": "MIT" }, "node_modules/@types/http-errors": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, "node_modules/@types/jsonfile": { "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", - "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", "dev": true, "license": "MIT", "optional": true, @@ -2378,8 +1566,6 @@ }, "node_modules/@types/keyv": { "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -2387,8 +1573,6 @@ }, "node_modules/@types/linkifyjs": { "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@types/linkifyjs/-/linkifyjs-2.1.7.tgz", - "integrity": "sha512-+SIYXs1lajyD7t/2+V9GLfdFlc/6Nr2tr65kjA2F5oOzBlPH+NiPqySJDHzREoGcL91Au9Qef8M5JdZiRXsaJw==", "license": "MIT", "dependencies": { "@types/react": "*" @@ -2396,47 +1580,33 @@ }, "node_modules/@types/luxon": { "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", - "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", "license": "MIT" }, "node_modules/@types/mime": { "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true, "license": "MIT" }, "node_modules/@types/minimatch": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", "dev": true, "license": "MIT" }, "node_modules/@types/minimist": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", "license": "MIT" }, "node_modules/@types/mithril": { "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/mithril/-/mithril-2.0.11.tgz", - "integrity": "sha512-2tYTImXc7RzWkPpgcbnSKpV46DQI4Bm8CfgmkrIbst8MJlX6d8hdgy2yQCEf5NZYLGNyK4xbzb4rr8VPmk0iXQ==", "license": "MIT" }, "node_modules/@types/ms": { "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { "version": "22.7.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", - "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -2444,8 +1614,6 @@ }, "node_modules/@types/node-forge": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.0.0.tgz", - "integrity": "sha512-h0bgwPKq5u99T9Gor4qtV1lCZ41xNkai0pie1n/a2mh2/4+jENWOlo7AJ4YKxTZAnSZ8FRurUpdIN7ohaPPuHA==", "dev": true, "license": "MIT", "dependencies": { @@ -2454,15 +1622,11 @@ }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true, "license": "MIT" }, "node_modules/@types/pako": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.3.tgz", - "integrity": "sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==", "dev": true, "license": "MIT" }, @@ -2479,34 +1643,24 @@ }, "node_modules/@types/prop-types": { "version": "15.7.13", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", - "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", "license": "MIT" }, "node_modules/@types/qrcode-svg": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@types/qrcode-svg/-/qrcode-svg-1.1.4.tgz", - "integrity": "sha512-gh+atEBHXpU5iO72Tg4q03YdGKoY0zH1Yr4mGl+NSzFpyPuJcgurs8F3aRpH0Gs93GFuB1rDoQj6U4Xshn72PA==", "license": "MIT" }, "node_modules/@types/qs": { "version": "6.9.16", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", - "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.11", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz", - "integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==", "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -2515,15 +1669,11 @@ }, "node_modules/@types/resolve": { "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true, "license": "MIT" }, "node_modules/@types/responselike": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -2531,15 +1681,11 @@ }, "node_modules/@types/semver": { "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true, "license": "MIT" }, "node_modules/@types/send": { "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", "dev": true, "license": "MIT", "dependencies": { @@ -2549,8 +1695,6 @@ }, "node_modules/@types/serve-static": { "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", "dev": true, "license": "MIT", "dependencies": { @@ -2561,14 +1705,10 @@ }, "node_modules/@types/systemjs": { "version": "6.13.5", - "resolved": "https://registry.npmjs.org/@types/systemjs/-/systemjs-6.13.5.tgz", - "integrity": "sha512-VWG7Z1/cb90UQF3HjkVcE+PB2kts93mW/94XQ2XUyHk+4wpzVrTdfXw0xeoaVyI/2XUuBRuCA7Is25RhEfHXNg==", "license": "MIT" }, "node_modules/@types/trusted-types": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, "node_modules/@types/verror": { @@ -2580,20 +1720,14 @@ }, "node_modules/@types/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", - "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", "license": "MIT" }, "node_modules/@types/winreg": { "version": "1.2.36", - "resolved": "https://registry.npmjs.org/@types/winreg/-/winreg-1.2.36.tgz", - "integrity": "sha512-DtafHy5A8hbaosXrbr7YdjQZaqVewXmiasRS5J4tYMzt3s1gkh40ixpxgVFfKiQ0JIYetTJABat47v9cpr/sQg==", "license": "MIT" }, "node_modules/@types/yauzl": { "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "license": "MIT", "optional": true, "dependencies": { @@ -2602,8 +1736,6 @@ }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "dev": true, "license": "MIT", "dependencies": { @@ -2637,8 +1769,6 @@ }, "node_modules/@typescript-eslint/parser": { "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "license": "BSD-2-Clause", "peer": true, @@ -2666,8 +1796,6 @@ }, "node_modules/@typescript-eslint/scope-manager": { "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", "dev": true, "license": "MIT", "dependencies": { @@ -2684,8 +1812,6 @@ }, "node_modules/@typescript-eslint/type-utils": { "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", "dev": true, "license": "MIT", "dependencies": { @@ -2712,8 +1838,6 @@ }, "node_modules/@typescript-eslint/types": { "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", "dev": true, "license": "MIT", "engines": { @@ -2726,8 +1850,6 @@ }, "node_modules/@typescript-eslint/typescript-estree": { "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2754,8 +1876,6 @@ }, "node_modules/@typescript-eslint/utils": { "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2781,8 +1901,6 @@ }, "node_modules/@typescript-eslint/visitor-keys": { "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", "dev": true, "license": "MIT", "dependencies": { @@ -2799,15 +1917,11 @@ }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true, "license": "ISC" }, "node_modules/@xmldom/xmldom": { "version": "0.8.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", - "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", "devOptional": true, "license": "MIT", "engines": { @@ -2816,15 +1930,11 @@ }, "node_modules/7zip-bin": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", - "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", "dev": true, "license": "MIT" }, "node_modules/abbrev": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", - "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", "dev": true, "license": "ISC", "engines": { @@ -2833,8 +1943,6 @@ }, "node_modules/accepts": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "dev": true, "license": "MIT", "dependencies": { @@ -2847,8 +1955,6 @@ }, "node_modules/acorn": { "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "license": "MIT", "bin": { @@ -2860,8 +1966,6 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2870,8 +1974,6 @@ }, "node_modules/address-rfc2822": { "version": "2.2.2", - "resolved": "https://registry.npmjs.org/address-rfc2822/-/address-rfc2822-2.2.2.tgz", - "integrity": "sha512-Kkl42jmfpSjkAtuGYnfD4cwGpGtNvrYaCdDFesb8z9GxlWNAaB/SLkDfmGxhh/qH/GfvKmMxt6Nt6Ek4qjFC7w==", "license": "MIT", "dependencies": { "email-addresses": "^5.0.0" @@ -2879,8 +1981,6 @@ }, "node_modules/adler-32": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", - "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", "license": "Apache-2.0", "engines": { "node": ">=0.8" @@ -2888,8 +1988,6 @@ }, "node_modules/agent-base": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2901,8 +1999,6 @@ }, "node_modules/aggregate-error": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "dev": true, "license": "MIT", "dependencies": { @@ -2915,8 +2011,6 @@ }, "node_modules/ajv": { "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2932,8 +2026,6 @@ }, "node_modules/ajv-keywords": { "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2942,8 +2034,6 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "devOptional": true, "license": "MIT", "engines": { @@ -2952,8 +2042,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2968,8 +2056,6 @@ }, "node_modules/anymatch": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "license": "ISC", "dependencies": { @@ -2982,15 +2068,11 @@ }, "node_modules/app-builder-bin": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz", - "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==", "dev": true, "license": "MIT" }, "node_modules/app-builder-lib": { "version": "24.13.3", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.13.3.tgz", - "integrity": "sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig==", "dev": true, "license": "MIT", "dependencies": { @@ -3032,8 +2114,6 @@ }, "node_modules/app-builder-lib/node_modules/@electron/notarize": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.2.1.tgz", - "integrity": "sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg==", "dev": true, "license": "MIT", "dependencies": { @@ -3047,8 +2127,6 @@ }, "node_modules/app-builder-lib/node_modules/@electron/notarize/node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3063,8 +2141,6 @@ }, "node_modules/app-builder-lib/node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3078,8 +2154,6 @@ }, "node_modules/archiver": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", - "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", "dev": true, "license": "MIT", "peer": true, @@ -3098,8 +2172,6 @@ }, "node_modules/archiver-utils": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", "dev": true, "license": "MIT", "peer": true, @@ -3121,8 +2193,6 @@ }, "node_modules/archiver-utils/node_modules/brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "peer": true, @@ -3133,9 +2203,6 @@ }, "node_modules/archiver-utils/node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", "peer": true, @@ -3156,8 +2223,6 @@ }, "node_modules/archiver-utils/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "peer": true, @@ -3170,8 +2235,6 @@ }, "node_modules/archiver-utils/node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", "peer": true, @@ -3187,16 +2250,12 @@ }, "node_modules/archiver-utils/node_modules/safe-buffer": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, "license": "MIT", "peer": true }, "node_modules/archiver-utils/node_modules/string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", "peer": true, @@ -3206,21 +2265,15 @@ }, "node_modules/argparse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, "node_modules/array-flatten": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "dev": true, "license": "MIT" }, "node_modules/array-union": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, "license": "MIT", "engines": { @@ -3249,15 +2302,11 @@ }, "node_modules/async": { "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "dev": true, "license": "MIT" }, "node_modules/async-exit-hook": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", - "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", "dev": true, "license": "MIT", "engines": { @@ -3266,15 +2315,11 @@ }, "node_modules/asynckit": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true, "license": "MIT" }, "node_modules/at-least-node": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true, "license": "ISC", "engines": { @@ -3283,8 +2328,6 @@ }, "node_modules/author-regex": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/author-regex/-/author-regex-1.0.0.tgz", - "integrity": "sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==", "dev": true, "license": "MIT", "engines": { @@ -3293,14 +2336,10 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "funding": [ { "type": "github", @@ -3319,8 +2358,6 @@ }, "node_modules/before-after-hook": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", - "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", "dev": true, "license": "Apache-2.0" }, @@ -3339,8 +2376,6 @@ }, "node_modules/binary-extensions": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "license": "MIT", "engines": { @@ -3352,8 +2387,6 @@ }, "node_modules/bindings": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", "license": "MIT", "dependencies": { "file-uri-to-path": "1.0.0" @@ -3361,8 +2394,6 @@ }, "node_modules/bl": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -3372,15 +2403,11 @@ }, "node_modules/bluebird": { "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "dev": true, "license": "MIT" }, "node_modules/bluebird-lst": { "version": "1.0.9", - "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", - "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", "dev": true, "license": "MIT", "dependencies": { @@ -3389,8 +2416,6 @@ }, "node_modules/body-parser": { "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, "license": "MIT", "dependencies": { @@ -3414,8 +2439,6 @@ }, "node_modules/body-parser/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { @@ -3424,8 +2447,6 @@ }, "node_modules/body-parser/node_modules/iconv-lite": { "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "license": "MIT", "dependencies": { @@ -3437,29 +2458,21 @@ }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, "node_modules/boolean": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", "license": "MIT", "optional": true }, "node_modules/bottleneck": { "version": "2.19.5", - "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", - "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", "dev": true, "license": "MIT" }, "node_modules/brace-expansion": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "license": "MIT", "dependencies": { @@ -3468,8 +2481,6 @@ }, "node_modules/braces": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -3480,8 +2491,6 @@ }, "node_modules/browserslist": { "version": "4.24.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", - "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", "dev": true, "funding": [ { @@ -3513,8 +2522,6 @@ }, "node_modules/buffer": { "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "funding": [ { "type": "github", @@ -3537,8 +2544,6 @@ }, "node_modules/buffer-crc32": { "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "license": "MIT", "engines": { "node": "*" @@ -3546,8 +2551,6 @@ }, "node_modules/buffer-equal": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", - "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", "dev": true, "license": "MIT", "engines": { @@ -3559,15 +2562,11 @@ }, "node_modules/buffer-from": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, "license": "MIT" }, "node_modules/builder-util": { "version": "24.13.1", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.13.1.tgz", - "integrity": "sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==", "dev": true, "license": "MIT", "dependencies": { @@ -3591,8 +2590,6 @@ }, "node_modules/builder-util-runtime": { "version": "9.2.4", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", - "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", "dev": true, "license": "MIT", "dependencies": { @@ -3605,8 +2602,6 @@ }, "node_modules/builder-util/node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3620,8 +2615,6 @@ }, "node_modules/builtin-modules": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", "dev": true, "license": "MIT", "engines": { @@ -3633,8 +2626,6 @@ }, "node_modules/bytebuffer": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/bytebuffer/-/bytebuffer-5.0.1.tgz", - "integrity": "sha512-IuzSdmADppkZ6DlpycMkm8l9zeEq16fWtLvunEwFiYciR/BHo4E8/xs5piFquG+Za8OWmMqHF8zuRviz2LHvRQ==", "license": "Apache-2.0", "dependencies": { "long": "~3" @@ -3645,8 +2636,6 @@ }, "node_modules/bytes": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, "license": "MIT", "engines": { @@ -3655,8 +2644,6 @@ }, "node_modules/cacache": { "version": "18.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", - "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3679,15 +2666,11 @@ }, "node_modules/cacache/node_modules/lru-cache": { "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, "license": "ISC" }, "node_modules/cacheable-lookup": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", "license": "MIT", "engines": { "node": ">=10.6.0" @@ -3695,8 +2678,6 @@ }, "node_modules/cacheable-request": { "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", "license": "MIT", "dependencies": { "clone-response": "^1.0.2", @@ -3713,8 +2694,6 @@ }, "node_modules/call-bind": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, "license": "MIT", "dependencies": { @@ -3733,8 +2712,6 @@ }, "node_modules/callsites": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { @@ -3743,8 +2720,6 @@ }, "node_modules/caniuse-lite": { "version": "1.0.30001667", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz", - "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==", "dev": true, "funding": [ { @@ -3764,8 +2739,6 @@ }, "node_modules/cborg": { "version": "4.2.2", - "resolved": "https://registry.npmjs.org/cborg/-/cborg-4.2.2.tgz", - "integrity": "sha512-A0z7WhnY4HDLrVdnQI4i/OLG3kANHotk5NzDpr2iauf4xrmQPwJCxlbCnIXkVrFtsr8G3omfvvr5oF50i1Zt8g==", "license": "Apache-2.0", "bin": { "cborg": "lib/bin.js" @@ -3773,8 +2746,6 @@ }, "node_modules/cfb": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", - "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", "license": "Apache-2.0", "dependencies": { "adler-32": "~1.3.0", @@ -3786,8 +2757,6 @@ }, "node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -3803,8 +2772,6 @@ }, "node_modules/chokidar": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -3828,8 +2795,6 @@ }, "node_modules/chownr": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "license": "ISC", "engines": { "node": ">=10" @@ -3837,15 +2802,11 @@ }, "node_modules/chromium-pickle-js": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", "dev": true, "license": "MIT" }, "node_modules/ci-info": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", - "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", "dev": true, "funding": [ { @@ -3860,8 +2821,6 @@ }, "node_modules/clean-regexp": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", - "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", "dev": true, "license": "MIT", "dependencies": { @@ -3873,8 +2832,6 @@ }, "node_modules/clean-regexp/node_modules/escape-string-regexp": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "license": "MIT", "engines": { @@ -3883,8 +2840,6 @@ }, "node_modules/clean-stack": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true, "license": "MIT", "engines": { @@ -3910,8 +2865,6 @@ }, "node_modules/cliui": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3925,8 +2878,6 @@ }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3943,8 +2894,6 @@ }, "node_modules/clone-response": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", "license": "MIT", "dependencies": { "mimic-response": "^1.0.0" @@ -3955,8 +2904,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -3968,15 +2915,11 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "devOptional": true, "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "license": "MIT", "dependencies": { @@ -3988,8 +2931,6 @@ }, "node_modules/commander": { "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, "license": "MIT", "engines": { @@ -3998,15 +2939,11 @@ }, "node_modules/commondir": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true, "license": "MIT" }, "node_modules/compare-version": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", - "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", "dev": true, "license": "MIT", "engines": { @@ -4015,8 +2952,6 @@ }, "node_modules/compress-commons": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", - "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", "dev": true, "license": "MIT", "peer": true, @@ -4032,14 +2967,10 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, "node_modules/config-file-ts": { "version": "0.2.6", - "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.6.tgz", - "integrity": "sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w==", "dev": true, "license": "MIT", "dependencies": { @@ -4049,8 +2980,6 @@ }, "node_modules/content-disposition": { "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4062,8 +2991,6 @@ }, "node_modules/content-type": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, "license": "MIT", "engines": { @@ -4072,8 +2999,6 @@ }, "node_modules/cookie": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "dev": true, "license": "MIT", "engines": { @@ -4082,15 +3007,11 @@ }, "node_modules/cookie-signature": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "dev": true, "license": "MIT" }, "node_modules/core-js-compat": { "version": "3.38.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", - "integrity": "sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==", "dev": true, "license": "MIT", "dependencies": { @@ -4103,8 +3024,6 @@ }, "node_modules/core-util-is": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "license": "MIT" }, "node_modules/crc": { @@ -4119,8 +3038,6 @@ }, "node_modules/crc-32": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", "license": "Apache-2.0", "bin": { "crc32": "bin/crc32.njs" @@ -4131,8 +3048,6 @@ }, "node_modules/crc32-stream": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", - "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", "dev": true, "license": "MIT", "peer": true, @@ -4146,8 +3061,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "license": "MIT", "dependencies": { @@ -4161,8 +3074,6 @@ }, "node_modules/cross-spawn-windows-exe": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/cross-spawn-windows-exe/-/cross-spawn-windows-exe-1.2.0.tgz", - "integrity": "sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw==", "dev": true, "funding": [ { @@ -4186,8 +3097,6 @@ }, "node_modules/cssstyle": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", - "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", "dev": true, "license": "MIT", "dependencies": { @@ -4199,14 +3108,10 @@ }, "node_modules/csstype": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", "license": "MIT", "engines": { "node": ">= 12" @@ -4214,8 +3119,6 @@ }, "node_modules/data-urls": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, "license": "MIT", "dependencies": { @@ -4228,8 +3131,6 @@ }, "node_modules/debug": { "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4245,15 +3146,11 @@ }, "node_modules/decimal.js": { "version": "10.4.3", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true, "license": "MIT" }, "node_modules/decompress-response": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" @@ -4267,8 +3164,6 @@ }, "node_modules/decompress-response/node_modules/mimic-response": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "license": "MIT", "engines": { "node": ">=10" @@ -4279,8 +3174,6 @@ }, "node_modules/deep-extend": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "license": "MIT", "engines": { "node": ">=4.0.0" @@ -4288,15 +3181,11 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "node_modules/deepmerge": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, "license": "MIT", "engines": { @@ -4305,8 +3194,6 @@ }, "node_modules/defer-to-connect": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", "license": "MIT", "engines": { "node": ">=10" @@ -4314,8 +3201,6 @@ }, "node_modules/define-data-property": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "devOptional": true, "license": "MIT", "dependencies": { @@ -4332,8 +3217,6 @@ }, "node_modules/define-lazy-prop": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", "dev": true, "license": "MIT", "engines": { @@ -4342,8 +3225,6 @@ }, "node_modules/define-properties": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "license": "MIT", "optional": true, "dependencies": { @@ -4360,8 +3241,6 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "license": "MIT", "engines": { @@ -4370,8 +3249,6 @@ }, "node_modules/depd": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, "license": "MIT", "engines": { @@ -4380,8 +3257,6 @@ }, "node_modules/destroy": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true, "license": "MIT", "engines": { @@ -4391,8 +3266,6 @@ }, "node_modules/detect-libc": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -4400,15 +3273,11 @@ }, "node_modules/detect-node": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "license": "MIT", "optional": true }, "node_modules/dir-compare": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", - "integrity": "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==", "dev": true, "license": "MIT", "dependencies": { @@ -4418,8 +3287,6 @@ }, "node_modules/dir-compare/node_modules/brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -4429,8 +3296,6 @@ }, "node_modules/dir-compare/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -4442,8 +3307,6 @@ }, "node_modules/dir-glob": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "license": "MIT", "dependencies": { "path-type": "^4.0.0" @@ -4454,8 +3317,6 @@ }, "node_modules/dmg-builder": { "version": "24.13.3", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.13.3.tgz", - "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4472,8 +3333,6 @@ }, "node_modules/dmg-builder/node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4513,8 +3372,6 @@ }, "node_modules/doctrine": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4526,14 +3383,10 @@ }, "node_modules/dompurify": { "version": "3.1.6", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", - "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==", "license": "(MPL-2.0 OR Apache-2.0)" }, "node_modules/dotenv": { "version": "9.0.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", - "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -4542,35 +3395,25 @@ }, "node_modules/dotenv-expand": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", - "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/duplexer": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "license": "MIT" }, "node_modules/eastasianwidth": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true, "license": "MIT" }, "node_modules/ee-first": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true, "license": "MIT" }, "node_modules/ejs": { "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4585,9 +3428,8 @@ }, "node_modules/electron": { "version": "32.2.2", - "resolved": "https://registry.npmjs.org/electron/-/electron-32.2.2.tgz", - "integrity": "sha512-c7TRE42JcgEmJ4elJyCdKk/2os0UX7YMkRDeXBkxFEoM34iX1/2x+c5T9PgeroKz8FEG7omRU5TvjulqVtXvdw==", "hasInstallScript": true, + "license": "MIT", "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^20.9.0", @@ -4602,8 +3444,6 @@ }, "node_modules/electron-builder": { "version": "24.13.3", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.13.3.tgz", - "integrity": "sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg==", "dev": true, "license": "MIT", "dependencies": { @@ -4629,8 +3469,6 @@ }, "node_modules/electron-builder-squirrel-windows": { "version": "24.13.3", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-24.13.3.tgz", - "integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==", "dev": true, "license": "MIT", "peer": true, @@ -4643,8 +3481,6 @@ }, "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "peer": true, @@ -4659,8 +3495,6 @@ }, "node_modules/electron-builder/node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4674,9 +3508,6 @@ }, "node_modules/electron-packager": { "version": "17.1.2", - "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-17.1.2.tgz", - "integrity": "sha512-XofXdikjYI7MVBcnXeoOvRR+yFFFHOLs3J7PF5KYQweigtgLshcH4W660PsvHr4lYZ03JBpLyEcUB8DzHZ+BNw==", - "deprecated": "Please use @electron/packager moving forward. There is no API change, just a package name change", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4712,8 +3543,6 @@ }, "node_modules/electron-packager/node_modules/@electron/notarize": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-1.2.4.tgz", - "integrity": "sha512-W5GQhJEosFNafewnS28d3bpQ37/s91CDWqxVchHfmv2dQSTWpOzNlUVQwYzC1ay5bChRV/A9BTL68yj0Pa+TSg==", "dev": true, "license": "MIT", "dependencies": { @@ -4726,8 +3555,6 @@ }, "node_modules/electron-packager/node_modules/@electron/notarize/node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4742,8 +3569,6 @@ }, "node_modules/electron-publish": { "version": "24.13.1", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.13.1.tgz", - "integrity": "sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==", "dev": true, "license": "MIT", "dependencies": { @@ -4758,8 +3583,6 @@ }, "node_modules/electron-publish/node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4773,15 +3596,11 @@ }, "node_modules/electron-to-chromium": { "version": "1.5.33", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.33.tgz", - "integrity": "sha512-+cYTcFB1QqD4j4LegwLfpCNxifb6dDFUAwk6RsLusCwIaZI6or2f+q8rs5tTB2YC53HhOlIbEaqHMAAC8IOIwA==", "dev": true, "license": "ISC" }, "node_modules/electron-updater": { "version": "6.3.4", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.4.tgz", - "integrity": "sha512-uZUo7p1Y53G4tl6Cgw07X1yF8Jlz6zhaL7CQJDZ1fVVkOaBfE2cWtx80avwDVi8jHp+I/FWawrMgTAeCCNIfAg==", "license": "MIT", "dependencies": { "builder-util-runtime": "9.2.5", @@ -4796,8 +3615,6 @@ }, "node_modules/electron-updater/node_modules/builder-util-runtime": { "version": "9.2.5", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.5.tgz", - "integrity": "sha512-HjIDfhvqx/8B3TDN4GbABQcgpewTU4LMRTQPkVpKYV3lsuxEJoIfvg09GyWTNmfVNSUAYf+fbTN//JX4TH20pg==", "license": "MIT", "dependencies": { "debug": "^4.3.4", @@ -4809,8 +3626,6 @@ }, "node_modules/electron-updater/node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -4823,8 +3638,6 @@ }, "node_modules/electron/node_modules/@types/node": { "version": "20.16.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz", - "integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==", "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -4832,21 +3645,15 @@ }, "node_modules/email-addresses": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz", - "integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==", "license": "MIT" }, "node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "devOptional": true, "license": "MIT" }, "node_modules/encodeurl": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "license": "MIT", "engines": { @@ -4855,8 +3662,6 @@ }, "node_modules/encoding": { "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "dev": true, "license": "MIT", "optional": true, @@ -4866,8 +3671,6 @@ }, "node_modules/end-of-stream": { "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -4875,8 +3678,6 @@ }, "node_modules/entities": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -4888,8 +3689,6 @@ }, "node_modules/env-paths": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "license": "MIT", "engines": { "node": ">=6" @@ -4897,15 +3696,11 @@ }, "node_modules/err-code": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "dev": true, "license": "MIT" }, "node_modules/error-ex": { "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, "license": "MIT", "dependencies": { @@ -4914,8 +3709,6 @@ }, "node_modules/es-define-property": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -4927,8 +3720,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "devOptional": true, "license": "MIT", "engines": { @@ -4937,15 +3728,11 @@ }, "node_modules/es6-error": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "license": "MIT", "optional": true }, "node_modules/esbuild": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", - "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4984,8 +3771,6 @@ }, "node_modules/esbuild-plugin-alias-path": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esbuild-plugin-alias-path/-/esbuild-plugin-alias-path-2.0.2.tgz", - "integrity": "sha512-YK8H9bzx6/CG6YBV11XjoNLjRhNZP0Ta4xZ3ATHhPn7pN8ljQGg+zne4d47DpIzF8/sX2qM+xQWev0CvaD2rSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4999,8 +3784,6 @@ }, "node_modules/esbuild-plugin-alias-path/node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5014,8 +3797,6 @@ }, "node_modules/escalade": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { @@ -5024,15 +3805,11 @@ }, "node_modules/escape-html": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "devOptional": true, "license": "MIT", "engines": { @@ -5044,9 +3821,6 @@ }, "node_modules/eslint": { "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", "dependencies": { @@ -5101,8 +3875,6 @@ }, "node_modules/eslint-config-prettier": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "license": "MIT", "bin": { @@ -5114,8 +3886,6 @@ }, "node_modules/eslint-plugin-unicorn": { "version": "55.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz", - "integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==", "dev": true, "license": "MIT", "dependencies": { @@ -5148,8 +3918,6 @@ }, "node_modules/eslint-plugin-unicorn/node_modules/globals": { "version": "15.10.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.10.0.tgz", - "integrity": "sha512-tqFIbz83w4Y5TCbtgjZjApohbuh7K9BxGYFm7ifwDR240tvdb7P9x+/9VvUKlmkPoiknoJtanI8UOrqxS3a7lQ==", "dev": true, "license": "MIT", "engines": { @@ -5161,8 +3929,6 @@ }, "node_modules/eslint-scope": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5175,8 +3941,6 @@ }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5188,8 +3952,6 @@ }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -5199,8 +3961,6 @@ }, "node_modules/eslint/node_modules/eslint-scope": { "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5216,8 +3976,6 @@ }, "node_modules/eslint/node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5226,8 +3984,6 @@ }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -5239,8 +3995,6 @@ }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -5252,8 +4006,6 @@ }, "node_modules/espree": { "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5270,8 +4022,6 @@ }, "node_modules/esquery": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5283,8 +4033,6 @@ }, "node_modules/esquery/node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5293,8 +4041,6 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5306,8 +4052,6 @@ }, "node_modules/esrecurse/node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5316,8 +4060,6 @@ }, "node_modules/estraverse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5326,15 +4068,11 @@ }, "node_modules/estree-walker": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true, "license": "MIT" }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5343,8 +4081,6 @@ }, "node_modules/etag": { "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, "license": "MIT", "engines": { @@ -5353,8 +4089,6 @@ }, "node_modules/event-stream": { "version": "3.3.4", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", "license": "MIT", "dependencies": { "duplexer": "~0.1.1", @@ -5368,8 +4102,6 @@ }, "node_modules/expand-template": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", "license": "(MIT OR WTFPL)", "engines": { "node": ">=6" @@ -5377,15 +4109,11 @@ }, "node_modules/exponential-backoff": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", - "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", "dev": true, "license": "Apache-2.0" }, "node_modules/express": { "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "dev": true, "license": "MIT", "dependencies": { @@ -5427,8 +4155,6 @@ }, "node_modules/express/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { @@ -5437,15 +4163,11 @@ }, "node_modules/express/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, "node_modules/extract-zip": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", @@ -5474,15 +4196,11 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "devOptional": true, "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -5497,22 +4215,16 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "devOptional": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fastq": { "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -5520,8 +4232,6 @@ }, "node_modules/fd-slicer": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "license": "MIT", "dependencies": { "pend": "~1.2.0" @@ -5529,8 +4239,6 @@ }, "node_modules/fetch-blob": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", "funding": [ { "type": "github", @@ -5552,8 +4260,6 @@ }, "node_modules/file-entry-cache": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "license": "MIT", "dependencies": { @@ -5565,14 +4271,10 @@ }, "node_modules/file-uri-to-path": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "license": "MIT" }, "node_modules/filelist": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5581,8 +4283,6 @@ }, "node_modules/filename-reserved-regex": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", - "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", "dev": true, "license": "MIT", "engines": { @@ -5591,8 +4291,6 @@ }, "node_modules/filenamify": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", - "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", "dev": true, "license": "MIT", "dependencies": { @@ -5609,8 +4307,6 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -5621,8 +4317,6 @@ }, "node_modules/finalhandler": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5640,8 +4334,6 @@ }, "node_modules/finalhandler/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { @@ -5650,15 +4342,11 @@ }, "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, "node_modules/find-up": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -5674,8 +4362,6 @@ }, "node_modules/flat-cache": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "license": "MIT", "dependencies": { @@ -5689,15 +4375,11 @@ }, "node_modules/flatted": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true, "license": "ISC" }, "node_modules/flora-colossus": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/flora-colossus/-/flora-colossus-2.0.0.tgz", - "integrity": "sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA==", "dev": true, "license": "MIT", "dependencies": { @@ -5710,8 +4392,6 @@ }, "node_modules/flora-colossus/node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5725,8 +4405,6 @@ }, "node_modules/foreground-child": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, "license": "ISC", "dependencies": { @@ -5742,8 +4420,6 @@ }, "node_modules/form-data": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "dev": true, "license": "MIT", "dependencies": { @@ -5757,8 +4433,6 @@ }, "node_modules/formdata-polyfill": { "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", "license": "MIT", "dependencies": { "fetch-blob": "^3.1.2" @@ -5769,8 +4443,6 @@ }, "node_modules/forwarded": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, "license": "MIT", "engines": { @@ -5779,8 +4451,6 @@ }, "node_modules/fresh": { "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true, "license": "MIT", "engines": { @@ -5789,20 +4459,14 @@ }, "node_modules/from": { "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", "license": "MIT" }, "node_modules/fs-constants": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, "node_modules/fs-extra": { "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, "license": "MIT", "dependencies": { @@ -5816,8 +4480,6 @@ }, "node_modules/fs-minipass": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", "dev": true, "license": "ISC", "dependencies": { @@ -5829,29 +4491,10 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/full-icu": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/full-icu/-/full-icu-1.5.0.tgz", - "integrity": "sha512-BxB2otKUSFyvENjbI8EtQscpiPOEnhrf5V4MVpa6PjzsrLmdKKUUhulbydsfKS4ve6cGXNVRLlrOjizby/ZfDA==", "dev": true, "hasInstallScript": true, "license": "Unicode-DFS-2016", @@ -5865,8 +4508,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5874,8 +4515,6 @@ }, "node_modules/galactus": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/galactus/-/galactus-1.0.0.tgz", - "integrity": "sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5889,8 +4528,6 @@ }, "node_modules/galactus/node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5904,8 +4541,6 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, "license": "ISC", "engines": { @@ -5914,8 +4549,6 @@ }, "node_modules/get-intrinsic": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5934,8 +4567,6 @@ }, "node_modules/get-package-info": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-package-info/-/get-package-info-1.0.0.tgz", - "integrity": "sha512-SCbprXGAPdIhKAXiG+Mk6yeoFH61JlYunqdFQFHDtLjJlDjFf6x07dsS8acO+xWt52jpdVo49AlVDnUVK1sDNw==", "dev": true, "license": "MIT", "dependencies": { @@ -5950,8 +4581,6 @@ }, "node_modules/get-package-info/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { @@ -5960,8 +4589,6 @@ }, "node_modules/get-package-info/node_modules/find-up": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5973,8 +4600,6 @@ }, "node_modules/get-package-info/node_modules/locate-path": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", "dev": true, "license": "MIT", "dependencies": { @@ -5987,15 +4612,11 @@ }, "node_modules/get-package-info/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, "node_modules/get-package-info/node_modules/p-limit": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", "dev": true, "license": "MIT", "dependencies": { @@ -6007,8 +4628,6 @@ }, "node_modules/get-package-info/node_modules/p-locate": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", "dev": true, "license": "MIT", "dependencies": { @@ -6020,8 +4639,6 @@ }, "node_modules/get-package-info/node_modules/path-exists": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", "dev": true, "license": "MIT", "engines": { @@ -6030,8 +4647,6 @@ }, "node_modules/get-package-info/node_modules/path-type": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6043,8 +4658,6 @@ }, "node_modules/get-package-info/node_modules/read-pkg": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA==", "dev": true, "license": "MIT", "dependencies": { @@ -6058,8 +4671,6 @@ }, "node_modules/get-package-info/node_modules/read-pkg-up": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w==", "dev": true, "license": "MIT", "dependencies": { @@ -6072,8 +4683,6 @@ }, "node_modules/get-stream": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "license": "MIT", "dependencies": { "pump": "^3.0.0" @@ -6087,14 +4696,10 @@ }, "node_modules/github-from-package": { "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "license": "MIT" }, "node_modules/glob": { "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "license": "ISC", "dependencies": { @@ -6114,8 +4719,6 @@ }, "node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -6126,8 +4729,6 @@ }, "node_modules/glob/node_modules/minimatch": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { @@ -6142,8 +4743,6 @@ }, "node_modules/global-agent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", "license": "BSD-3-Clause", "optional": true, "dependencies": { @@ -6160,8 +4759,6 @@ }, "node_modules/globals": { "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6176,8 +4773,6 @@ }, "node_modules/globalthis": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "license": "MIT", "optional": true, "dependencies": { @@ -6193,8 +4788,6 @@ }, "node_modules/globby": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, "license": "MIT", "dependencies": { @@ -6214,8 +4807,6 @@ }, "node_modules/gopd": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -6227,8 +4818,6 @@ }, "node_modules/got": { "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", "license": "MIT", "dependencies": { "@sindresorhus/is": "^4.0.0", @@ -6252,21 +4841,15 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true, "license": "MIT" }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { @@ -6275,8 +4858,6 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -6288,8 +4869,6 @@ }, "node_modules/has-proto": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "devOptional": true, "license": "MIT", "engines": { @@ -6301,8 +4880,6 @@ }, "node_modules/has-symbols": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "devOptional": true, "license": "MIT", "engines": { @@ -6314,8 +4891,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -6326,8 +4901,6 @@ }, "node_modules/hosted-git-info": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, "license": "ISC", "dependencies": { @@ -6339,8 +4912,6 @@ }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6352,14 +4923,10 @@ }, "node_modules/http-cache-semantics": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "license": "BSD-2-Clause" }, "node_modules/http-errors": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6375,8 +4942,6 @@ }, "node_modules/http-proxy-agent": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", "dev": true, "license": "MIT", "dependencies": { @@ -6390,8 +4955,6 @@ }, "node_modules/http2-wrapper": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", "license": "MIT", "dependencies": { "quick-lru": "^5.1.1", @@ -6403,8 +4966,6 @@ }, "node_modules/https-proxy-agent": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, "license": "MIT", "dependencies": { @@ -6434,8 +4995,6 @@ }, "node_modules/iconv-lite": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -6446,8 +5005,6 @@ }, "node_modules/ieee754": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "funding": [ { "type": "github", @@ -6466,8 +5023,6 @@ }, "node_modules/ignore": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "license": "MIT", "engines": { "node": ">= 4" @@ -6475,14 +5030,10 @@ }, "node_modules/immediate": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT" }, "node_modules/import-fresh": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, "license": "MIT", "dependencies": { @@ -6498,8 +5049,6 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { @@ -6508,8 +5057,6 @@ }, "node_modules/indent-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "license": "MIT", "engines": { @@ -6518,9 +5065,6 @@ }, "node_modules/inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -6529,20 +5073,14 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/ip-address": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "dev": true, "license": "MIT", "dependencies": { @@ -6555,8 +5093,6 @@ }, "node_modules/ipaddr.js": { "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, "license": "MIT", "engines": { @@ -6565,15 +5101,11 @@ }, "node_modules/is-arrayish": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, "license": "MIT" }, "node_modules/is-binary-path": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, "license": "MIT", "dependencies": { @@ -6585,8 +5117,6 @@ }, "node_modules/is-builtin-module": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", - "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", "dev": true, "license": "MIT", "dependencies": { @@ -6601,8 +5131,6 @@ }, "node_modules/is-ci": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6614,8 +5142,6 @@ }, "node_modules/is-ci/node_modules/ci-info": { "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true, "funding": [ { @@ -6630,8 +5156,6 @@ }, "node_modules/is-core-module": { "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -6645,8 +5169,6 @@ }, "node_modules/is-docker": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "dev": true, "license": "MIT", "bin": { @@ -6661,8 +5183,6 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6670,8 +5190,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "devOptional": true, "license": "MIT", "engines": { @@ -6680,8 +5198,6 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -6692,22 +5208,16 @@ }, "node_modules/is-lambda": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", "dev": true, "license": "MIT" }, "node_modules/is-module": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", "dev": true, "license": "MIT" }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "license": "MIT", "engines": { "node": ">=0.12.0" @@ -6715,8 +5225,6 @@ }, "node_modules/is-path-inside": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, "license": "MIT", "engines": { @@ -6725,8 +5233,6 @@ }, "node_modules/is-plain-obj": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6734,15 +5240,11 @@ }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true, "license": "MIT" }, "node_modules/is-reference": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6751,8 +5253,6 @@ }, "node_modules/is-regexp": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6760,8 +5260,6 @@ }, "node_modules/is-wsl": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, "license": "MIT", "dependencies": { @@ -6773,14 +5271,10 @@ }, "node_modules/isarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, "node_modules/isbinaryfile": { "version": "5.0.2", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.2.tgz", - "integrity": "sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg==", "dev": true, "license": "MIT", "engines": { @@ -6792,14 +5286,10 @@ }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, "node_modules/jackspeak": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -6814,8 +5304,6 @@ }, "node_modules/jake": { "version": "10.9.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -6833,8 +5321,6 @@ }, "node_modules/jake/node_modules/brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -6844,8 +5330,6 @@ }, "node_modules/jake/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -6857,15 +5341,11 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -6876,15 +5356,11 @@ }, "node_modules/jsbn": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", "dev": true, "license": "MIT" }, "node_modules/jsdom": { "version": "25.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.0.tgz", - "integrity": "sha512-OhoFVT59T7aEq75TVw9xxEfkXgacpqAhQaYgP9y/fDqWQCMB/b1H66RfmPm/MaeaAIU9nDwMOVTlPN51+ao6CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6924,8 +5400,6 @@ }, "node_modules/jsdom/node_modules/agent-base": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, "license": "MIT", "dependencies": { @@ -6937,8 +5411,6 @@ }, "node_modules/jsdom/node_modules/http-proxy-agent": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", "dependencies": { @@ -6951,8 +5423,6 @@ }, "node_modules/jsdom/node_modules/https-proxy-agent": { "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "license": "MIT", "dependencies": { @@ -6965,8 +5435,6 @@ }, "node_modules/jsesc": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "dev": true, "license": "MIT", "bin": { @@ -6978,42 +5446,30 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "devOptional": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/json-stringify-safe": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "license": "ISC", "optional": true }, "node_modules/json5": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -7024,8 +5480,6 @@ }, "node_modules/jsonfile": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -7036,8 +5490,6 @@ }, "node_modules/jszip": { "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "license": "(MIT OR GPL-3.0-or-later)", "dependencies": { "lie": "~3.3.0", @@ -7048,14 +5500,10 @@ }, "node_modules/jszip/node_modules/pako": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, "node_modules/jszip/node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -7069,14 +5517,10 @@ }, "node_modules/jszip/node_modules/safe-buffer": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, "node_modules/jszip/node_modules/string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -7084,8 +5528,6 @@ }, "node_modules/junk": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", - "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==", "dev": true, "license": "MIT", "engines": { @@ -7094,8 +5536,6 @@ }, "node_modules/keyv": { "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "license": "MIT", "dependencies": { "json-buffer": "3.0.1" @@ -7103,14 +5543,10 @@ }, "node_modules/lazy-val": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", - "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", "license": "MIT" }, "node_modules/lazystream": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", "dev": true, "license": "MIT", "peer": true, @@ -7123,8 +5559,6 @@ }, "node_modules/lazystream/node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", "peer": true, @@ -7140,16 +5574,12 @@ }, "node_modules/lazystream/node_modules/safe-buffer": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, "license": "MIT", "peer": true }, "node_modules/lazystream/node_modules/string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", "peer": true, @@ -7159,8 +5589,6 @@ }, "node_modules/levn": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7173,8 +5601,6 @@ }, "node_modules/lie": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "license": "MIT", "dependencies": { "immediate": "~3.0.5" @@ -7182,15 +5608,11 @@ }, "node_modules/lines-and-columns": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true, "license": "MIT" }, "node_modules/linkify-html": { "version": "4.1.3", - "resolved": "https://registry.npmjs.org/linkify-html/-/linkify-html-4.1.3.tgz", - "integrity": "sha512-Ejb8X/pOxB4IVqG1U37tnF85UW3JtX+eHudH3zlZ2pODz2e/J7zQ/vj+VDWffwhTecJqdRehhluwrRmKoJz+iQ==", "license": "MIT", "peerDependencies": { "linkifyjs": "^4.0.0" @@ -7198,14 +5620,10 @@ }, "node_modules/linkifyjs": { "version": "4.1.3", - "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.3.tgz", - "integrity": "sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==", "license": "MIT" }, "node_modules/load-json-file": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7220,8 +5638,6 @@ }, "node_modules/load-json-file/node_modules/parse-json": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7233,8 +5649,6 @@ }, "node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -7249,80 +5663,58 @@ }, "node_modules/lodash": { "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, "node_modules/lodash.defaults": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", "dev": true, "license": "MIT", "peer": true }, "node_modules/lodash.difference": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", "dev": true, "license": "MIT", "peer": true }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", "license": "MIT" }, "node_modules/lodash.flatten": { "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", "dev": true, "license": "MIT", "peer": true }, "node_modules/lodash.get": { "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "dev": true, "license": "MIT" }, "node_modules/lodash.isequal": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "dev": true, "license": "MIT", "peer": true }, "node_modules/lodash.merge": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, "license": "MIT" }, "node_modules/lodash.union": { "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", "dev": true, "license": "MIT", "peer": true }, "node_modules/long": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", - "integrity": "sha512-ZYvPPOMqUwPoDsbJaR10iQJYnMuZhRTvHYl62ErLIEX7RgFlziSBUUvrt3OVfc47QlHHpzPZYP17g3Fv7oeJkg==", "license": "Apache-2.0", "engines": { "node": ">=0.6" @@ -7330,8 +5722,6 @@ }, "node_modules/lowercase-keys": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", "license": "MIT", "engines": { "node": ">=8" @@ -7339,8 +5729,6 @@ }, "node_modules/lru-cache": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "license": "ISC", "dependencies": { @@ -7352,8 +5740,6 @@ }, "node_modules/luxon": { "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", "license": "MIT", "engines": { "node": ">=12" @@ -7361,8 +5747,6 @@ }, "node_modules/magic-string": { "version": "0.30.11", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", - "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "dev": true, "license": "MIT", "dependencies": { @@ -7371,8 +5755,6 @@ }, "node_modules/make-fetch-happen": { "version": "13.0.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", - "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", "dev": true, "license": "ISC", "dependencies": { @@ -7394,14 +5776,10 @@ } }, "node_modules/map-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==" + "version": "0.1.0" }, "node_modules/matcher": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", "license": "MIT", "optional": true, "dependencies": { @@ -7413,8 +5791,6 @@ }, "node_modules/media-typer": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true, "license": "MIT", "engines": { @@ -7423,8 +5799,6 @@ }, "node_modules/merge-descriptors": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "dev": true, "license": "MIT", "funding": { @@ -7433,8 +5807,6 @@ }, "node_modules/merge2": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "license": "MIT", "engines": { "node": ">= 8" @@ -7442,8 +5814,6 @@ }, "node_modules/methods": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "dev": true, "license": "MIT", "engines": { @@ -7452,8 +5822,6 @@ }, "node_modules/micromatch": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -7465,8 +5833,6 @@ }, "node_modules/mime": { "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, "license": "MIT", "bin": { @@ -7478,8 +5844,6 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", "engines": { @@ -7488,8 +5852,6 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", "dependencies": { @@ -7501,8 +5863,6 @@ }, "node_modules/mimic-response": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "license": "MIT", "engines": { "node": ">=4" @@ -7510,8 +5870,6 @@ }, "node_modules/min-indent": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "dev": true, "license": "MIT", "engines": { @@ -7520,8 +5878,6 @@ }, "node_modules/minimatch": { "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "license": "ISC", "dependencies": { @@ -7533,8 +5889,6 @@ }, "node_modules/minimist": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7542,8 +5896,6 @@ }, "node_modules/minipass": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, "license": "ISC", "engines": { @@ -7552,8 +5904,6 @@ }, "node_modules/minipass-collect": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", - "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "dev": true, "license": "ISC", "dependencies": { @@ -7565,8 +5915,6 @@ }, "node_modules/minipass-fetch": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", - "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", "dev": true, "license": "MIT", "dependencies": { @@ -7583,8 +5931,6 @@ }, "node_modules/minipass-flush": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", "dev": true, "license": "ISC", "dependencies": { @@ -7596,8 +5942,6 @@ }, "node_modules/minipass-flush/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "license": "ISC", "dependencies": { @@ -7609,8 +5953,6 @@ }, "node_modules/minipass-pipeline": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", "dev": true, "license": "ISC", "dependencies": { @@ -7622,8 +5964,6 @@ }, "node_modules/minipass-pipeline/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "license": "ISC", "dependencies": { @@ -7635,8 +5975,6 @@ }, "node_modules/minipass-sized": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", "dev": true, "license": "ISC", "dependencies": { @@ -7648,8 +5986,6 @@ }, "node_modules/minipass-sized/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "license": "ISC", "dependencies": { @@ -7661,8 +5997,6 @@ }, "node_modules/minizlib": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "license": "MIT", "dependencies": { "minipass": "^3.0.0", @@ -7674,8 +6008,6 @@ }, "node_modules/minizlib/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -7686,8 +6018,6 @@ }, "node_modules/mithril": { "version": "2.2.2", - "resolved": "https://registry.npmjs.org/mithril/-/mithril-2.2.2.tgz", - "integrity": "sha512-YRm6eLv2UUaWaWHdH8L+desW9+DN7+oM34CxJv6tT2e1lNVue8bxQlknQeDRn9aKlO8sIujm2wqUHwM+Hb1wGQ==", "license": "MIT", "dependencies": { "ospec": "4.0.0" @@ -7698,8 +6028,6 @@ }, "node_modules/mkdirp": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" @@ -7710,40 +6038,28 @@ }, "node_modules/mkdirp-classic": { "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/napi-build-utils": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", "license": "MIT" }, "node_modules/natural-compare": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, "node_modules/natural-compare-lite": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true, "license": "MIT" }, "node_modules/negotiator": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true, "license": "MIT", "engines": { @@ -7752,8 +6068,6 @@ }, "node_modules/node-abi": { "version": "3.68.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.68.0.tgz", - "integrity": "sha512-7vbj10trelExNjFSBm5kTvZXXa7pZyKWx9RCKIyqe6I9Ev3IzGpQoqBP3a+cOdxY+pWj6VkP28n/2wWysBHD/A==", "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -7771,8 +6085,6 @@ }, "node_modules/node-domexception": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", "funding": [ { "type": "github", @@ -7790,8 +6102,6 @@ }, "node_modules/node-fetch": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "license": "MIT", "dependencies": { "data-uri-to-buffer": "^4.0.0", @@ -7808,8 +6118,6 @@ }, "node_modules/node-gyp": { "version": "10.2.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.2.0.tgz", - "integrity": "sha512-sp3FonBAaFe4aYTcFdZUn2NYkbP7xroPGYvQmP4Nl5PxamznItBnNCgjrVTKrEfQynInMsJvZrdmqUnysCJ8rw==", "dev": true, "license": "MIT", "dependencies": { @@ -7833,8 +6141,6 @@ }, "node_modules/node-gyp/node_modules/isexe": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, "license": "ISC", "engines": { @@ -7843,8 +6149,6 @@ }, "node_modules/node-gyp/node_modules/which": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", "dev": true, "license": "ISC", "dependencies": { @@ -7859,15 +6163,11 @@ }, "node_modules/node-releases": { "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true, "license": "MIT" }, "node_modules/nopt": { "version": "7.2.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", - "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", "dev": true, "license": "ISC", "dependencies": { @@ -7882,8 +6182,6 @@ }, "node_modules/normalize-package-data": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7895,15 +6193,11 @@ }, "node_modules/normalize-package-data/node_modules/hosted-git-info": { "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true, "license": "ISC" }, "node_modules/normalize-package-data/node_modules/semver": { "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "bin": { @@ -7912,8 +6206,6 @@ }, "node_modules/normalize-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "license": "MIT", "engines": { @@ -7922,8 +6214,6 @@ }, "node_modules/normalize-url": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "license": "MIT", "engines": { "node": ">=10" @@ -7934,15 +6224,11 @@ }, "node_modules/nwsapi": { "version": "2.2.13", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", - "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==", "dev": true, "license": "MIT" }, "node_modules/object-inspect": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", "dev": true, "license": "MIT", "engines": { @@ -7954,8 +6240,6 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "license": "MIT", "optional": true, "engines": { @@ -7964,8 +6248,6 @@ }, "node_modules/octokit": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/octokit/-/octokit-4.0.2.tgz", - "integrity": "sha512-wbqF4uc1YbcldtiBFfkSnquHtECEIpYD78YUXI6ri1Im5OO2NLo6ZVpRdbJpdnpZ05zMrVPssNiEo6JQtea+Qg==", "dev": true, "license": "MIT", "dependencies": { @@ -7986,8 +6268,6 @@ }, "node_modules/on-finished": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, "license": "MIT", "dependencies": { @@ -7999,8 +6279,6 @@ }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", "dependencies": { "wrappy": "1" @@ -8008,8 +6286,6 @@ }, "node_modules/open": { "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8026,8 +6302,6 @@ }, "node_modules/optionator": { "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -8044,8 +6318,6 @@ }, "node_modules/ospec": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ospec/-/ospec-4.0.0.tgz", - "integrity": "sha512-MpDtkpscOxHYb4w71v7GB4LBsRuzxZnM+HdwjhzJQzu+5EJvA80yxTaKw+wp5Dmf5RV2/Bg3Uvz2vlI/PhW9Ow==", "license": "MIT", "dependencies": { "glob": "^7.1.3" @@ -8056,8 +6328,6 @@ }, "node_modules/ospec/node_modules/brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -8066,9 +6336,6 @@ }, "node_modules/ospec/node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -8087,8 +6354,6 @@ }, "node_modules/ospec/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -8099,8 +6364,6 @@ }, "node_modules/p-cancelable": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "license": "MIT", "engines": { "node": ">=8" @@ -8108,8 +6371,6 @@ }, "node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8124,8 +6385,6 @@ }, "node_modules/p-locate": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -8140,8 +6399,6 @@ }, "node_modules/p-map": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8156,8 +6413,6 @@ }, "node_modules/p-try": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", "dev": true, "license": "MIT", "engines": { @@ -8166,21 +6421,15 @@ }, "node_modules/package-json-from-dist": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/pako": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", - "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", "license": "(MIT AND Zlib)" }, "node_modules/parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { @@ -8192,8 +6441,6 @@ }, "node_modules/parse-author": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-author/-/parse-author-2.0.0.tgz", - "integrity": "sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==", "dev": true, "license": "MIT", "dependencies": { @@ -8205,8 +6452,6 @@ }, "node_modules/parse-json": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, "license": "MIT", "dependencies": { @@ -8224,8 +6469,6 @@ }, "node_modules/parse5": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", "dev": true, "license": "MIT", "dependencies": { @@ -8237,8 +6480,6 @@ }, "node_modules/parseurl": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, "license": "MIT", "engines": { @@ -8247,8 +6488,6 @@ }, "node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { @@ -8257,8 +6496,6 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8266,8 +6503,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { @@ -8276,14 +6511,10 @@ }, "node_modules/path-parse": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -8299,22 +6530,16 @@ }, "node_modules/path-scurry/node_modules/lru-cache": { "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, "license": "ISC" }, "node_modules/path-to-regexp": { "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", "dev": true, "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "license": "MIT", "engines": { "node": ">=8" @@ -8322,8 +6547,6 @@ }, "node_modules/pause-stream": { "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", "license": [ "MIT", "Apache2" @@ -8334,21 +6557,15 @@ }, "node_modules/pend": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -8359,8 +6576,6 @@ }, "node_modules/pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, "license": "MIT", "engines": { @@ -8369,8 +6584,6 @@ }, "node_modules/plist": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -8384,8 +6597,6 @@ }, "node_modules/pluralize": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", "dev": true, "license": "MIT", "engines": { @@ -8394,8 +6605,6 @@ }, "node_modules/prebuild-install": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", - "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", "license": "MIT", "dependencies": { "detect-libc": "^2.0.0", @@ -8420,8 +6629,6 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -8430,8 +6637,6 @@ }, "node_modules/prettier": { "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, "license": "MIT", "bin": { @@ -8446,8 +6651,6 @@ }, "node_modules/proc-log": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", - "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", "dev": true, "license": "ISC", "engines": { @@ -8456,14 +6659,10 @@ }, "node_modules/process-nextick-args": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, "node_modules/progress": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "license": "MIT", "engines": { "node": ">=0.4.0" @@ -8471,8 +6670,6 @@ }, "node_modules/promise-retry": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", "dev": true, "license": "MIT", "dependencies": { @@ -8485,8 +6682,6 @@ }, "node_modules/proxy-addr": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, "license": "MIT", "dependencies": { @@ -8499,8 +6694,6 @@ }, "node_modules/ps-tree": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", - "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", "license": "MIT", "dependencies": { "event-stream": "=3.3.4" @@ -8514,15 +6707,11 @@ }, "node_modules/psl": { "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "dev": true, "license": "MIT" }, "node_modules/pump": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -8531,8 +6720,6 @@ }, "node_modules/punycode": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "devOptional": true, "license": "MIT", "engines": { @@ -8541,8 +6728,6 @@ }, "node_modules/qrcode-svg": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/qrcode-svg/-/qrcode-svg-1.1.0.tgz", - "integrity": "sha512-XyQCIXux1zEIA3NPb0AeR8UMYvXZzWEhgdBgBjH9gO7M48H9uoHzviNz8pXw3UzrAcxRRRn9gxHewAVK7bn9qw==", "license": "MIT", "bin": { "qrcode-svg": "bin/qrcode-svg.js" @@ -8550,8 +6735,6 @@ }, "node_modules/qs": { "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -8566,15 +6749,11 @@ }, "node_modules/querystringify": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "dev": true, "license": "MIT" }, "node_modules/queue-microtask": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "funding": [ { "type": "github", @@ -8593,8 +6772,6 @@ }, "node_modules/quibble": { "version": "0.7.0", - "resolved": "https://registry.npmjs.org/quibble/-/quibble-0.7.0.tgz", - "integrity": "sha512-uiqtYLo6p6vWR/G3Ltsg0NU1xw43RcNGadYP+d/DF3zLQTyOt8uC7L2mmcJ97au1QE1YdmCD+HVIIq/RGtkbWA==", "license": "MIT", "dependencies": { "lodash": "^4.17.21", @@ -8606,8 +6783,6 @@ }, "node_modules/quick-lru": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "license": "MIT", "engines": { "node": ">=10" @@ -8618,8 +6793,6 @@ }, "node_modules/randombytes": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8628,8 +6801,6 @@ }, "node_modules/range-parser": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true, "license": "MIT", "engines": { @@ -8638,8 +6809,6 @@ }, "node_modules/raw-body": { "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dev": true, "license": "MIT", "dependencies": { @@ -8654,8 +6823,6 @@ }, "node_modules/raw-body/node_modules/iconv-lite": { "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "license": "MIT", "dependencies": { @@ -8667,8 +6834,6 @@ }, "node_modules/rc": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", @@ -8682,8 +6847,6 @@ }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8691,8 +6854,6 @@ }, "node_modules/rcedit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-3.1.0.tgz", - "integrity": "sha512-WRlRdY1qZbu1L11DklT07KuHfRk42l0NFFJdaExELEu4fEQ982bP5Z6OWGPj/wLLIuKRQDCxZJGAwoFsxhZhNA==", "dev": true, "license": "MIT", "dependencies": { @@ -8704,8 +6865,6 @@ }, "node_modules/read-config-file": { "version": "6.3.2", - "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz", - "integrity": "sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -8722,8 +6881,6 @@ }, "node_modules/read-pkg": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", "dev": true, "license": "MIT", "dependencies": { @@ -8738,8 +6895,6 @@ }, "node_modules/read-pkg-up": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", "dev": true, "license": "MIT", "dependencies": { @@ -8756,8 +6911,6 @@ }, "node_modules/read-pkg-up/node_modules/find-up": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "license": "MIT", "dependencies": { @@ -8770,8 +6923,6 @@ }, "node_modules/read-pkg-up/node_modules/locate-path": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "license": "MIT", "dependencies": { @@ -8783,8 +6934,6 @@ }, "node_modules/read-pkg-up/node_modules/p-limit": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "license": "MIT", "dependencies": { @@ -8799,8 +6948,6 @@ }, "node_modules/read-pkg-up/node_modules/p-locate": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "license": "MIT", "dependencies": { @@ -8812,8 +6959,6 @@ }, "node_modules/read-pkg-up/node_modules/p-try": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, "license": "MIT", "engines": { @@ -8822,8 +6967,6 @@ }, "node_modules/read-pkg-up/node_modules/type-fest": { "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -8832,8 +6975,6 @@ }, "node_modules/read-pkg/node_modules/type-fest": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -8842,8 +6983,6 @@ }, "node_modules/readable-stream": { "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -8856,8 +6995,6 @@ }, "node_modules/readdir-glob": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", - "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -8867,8 +7004,6 @@ }, "node_modules/readdirp": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "license": "MIT", "dependencies": { @@ -8880,8 +7015,6 @@ }, "node_modules/regexp-tree": { "version": "0.1.27", - "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", - "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", "dev": true, "license": "MIT", "bin": { @@ -8890,8 +7023,6 @@ }, "node_modules/regjsparser": { "version": "0.10.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz", - "integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -8903,8 +7034,6 @@ }, "node_modules/regjsparser/node_modules/jsesc": { "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", "dev": true, "bin": { "jsesc": "bin/jsesc" @@ -8912,8 +7041,6 @@ }, "node_modules/require-directory": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, "license": "MIT", "engines": { @@ -8922,15 +7049,11 @@ }, "node_modules/requires-port": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true, "license": "MIT" }, "node_modules/resolve": { "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", @@ -8946,14 +7069,10 @@ }, "node_modules/resolve-alpn": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "license": "MIT" }, "node_modules/resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { @@ -8962,8 +7081,6 @@ }, "node_modules/responselike": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", "license": "MIT", "dependencies": { "lowercase-keys": "^2.0.0" @@ -8974,8 +7091,6 @@ }, "node_modules/retry": { "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "dev": true, "license": "MIT", "engines": { @@ -8984,8 +7099,6 @@ }, "node_modules/reusify": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -8994,9 +7107,6 @@ }, "node_modules/rimraf": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", "dependencies": { @@ -9011,8 +7121,6 @@ }, "node_modules/rimraf/node_modules/brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -9022,9 +7130,6 @@ }, "node_modules/rimraf/node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", "dependencies": { @@ -9044,8 +7149,6 @@ }, "node_modules/rimraf/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -9057,8 +7160,6 @@ }, "node_modules/roarr": { "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", "license": "BSD-3-Clause", "optional": true, "dependencies": { @@ -9075,8 +7176,6 @@ }, "node_modules/rollup": { "version": "4.24.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", - "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", "dev": true, "license": "MIT", "dependencies": { @@ -9111,8 +7210,6 @@ }, "node_modules/rollup-plugin-visualizer": { "version": "5.12.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz", - "integrity": "sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9138,15 +7235,11 @@ }, "node_modules/rrweb-cssom": { "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", - "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", "dev": true, "license": "MIT" }, "node_modules/run-parallel": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "funding": [ { "type": "github", @@ -9168,8 +7261,6 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "funding": [ { "type": "github", @@ -9188,14 +7279,10 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, "node_modules/sanitize-filename": { "version": "1.6.3", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", - "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", "dev": true, "license": "WTFPL OR ISC", "dependencies": { @@ -9204,14 +7291,10 @@ }, "node_modules/sax": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "license": "ISC" }, "node_modules/saxes": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, "license": "ISC", "dependencies": { @@ -9223,8 +7306,6 @@ }, "node_modules/semver": { "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -9235,15 +7316,11 @@ }, "node_modules/semver-compare": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", "license": "MIT", "optional": true }, "node_modules/send": { "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, "license": "MIT", "dependencies": { @@ -9267,8 +7344,6 @@ }, "node_modules/send/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { @@ -9277,15 +7352,11 @@ }, "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, "node_modules/send/node_modules/encodeurl": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true, "license": "MIT", "engines": { @@ -9294,8 +7365,6 @@ }, "node_modules/send/node_modules/mime": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, "license": "MIT", "bin": { @@ -9307,8 +7376,6 @@ }, "node_modules/serialize-error": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", "license": "MIT", "optional": true, "dependencies": { @@ -9323,8 +7390,6 @@ }, "node_modules/serialize-error/node_modules/type-fest": { "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "license": "(MIT OR CC0-1.0)", "optional": true, "engines": { @@ -9336,8 +7401,6 @@ }, "node_modules/serialize-javascript": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -9346,8 +7409,6 @@ }, "node_modules/serve-static": { "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, "license": "MIT", "dependencies": { @@ -9362,8 +7423,6 @@ }, "node_modules/set-function-length": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "license": "MIT", "dependencies": { @@ -9380,21 +7439,15 @@ }, "node_modules/setimmediate": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "license": "MIT" }, "node_modules/setprototypeof": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true, "license": "ISC" }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { @@ -9406,8 +7459,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { @@ -9416,8 +7467,6 @@ }, "node_modules/side-channel": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "license": "MIT", "dependencies": { @@ -9435,8 +7484,6 @@ }, "node_modules/signal-exit": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, "license": "ISC", "engines": { @@ -9448,8 +7495,6 @@ }, "node_modules/simple-concat": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", "funding": [ { "type": "github", @@ -9468,8 +7513,6 @@ }, "node_modules/simple-get": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "funding": [ { "type": "github", @@ -9493,8 +7536,6 @@ }, "node_modules/simple-update-notifier": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", "dev": true, "license": "MIT", "dependencies": { @@ -9506,8 +7547,6 @@ }, "node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", "engines": { @@ -9531,8 +7570,6 @@ }, "node_modules/smart-buffer": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "devOptional": true, "license": "MIT", "engines": { @@ -9542,15 +7579,11 @@ }, "node_modules/smob": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", - "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", "dev": true, "license": "MIT" }, "node_modules/socks": { "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", "dev": true, "license": "MIT", "dependencies": { @@ -9564,8 +7597,6 @@ }, "node_modules/socks-proxy-agent": { "version": "8.0.4", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", - "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", "dev": true, "license": "MIT", "dependencies": { @@ -9579,8 +7610,6 @@ }, "node_modules/socks-proxy-agent/node_modules/agent-base": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, "license": "MIT", "dependencies": { @@ -9592,8 +7621,6 @@ }, "node_modules/source-map": { "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -9602,8 +7629,6 @@ }, "node_modules/source-map-support": { "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "license": "MIT", "dependencies": { @@ -9613,8 +7638,6 @@ }, "node_modules/source-map-support/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -9623,8 +7646,6 @@ }, "node_modules/spdx-correct": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -9634,15 +7655,11 @@ }, "node_modules/spdx-exceptions": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true, "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -9652,15 +7669,11 @@ }, "node_modules/spdx-license-ids": { "version": "3.0.20", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", - "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", "dev": true, "license": "CC0-1.0" }, "node_modules/split": { "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", "license": "MIT", "dependencies": { "through": "2" @@ -9671,21 +7684,15 @@ }, "node_modules/sprintf-js": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "devOptional": true, "license": "BSD-3-Clause" }, "node_modules/squire-rte": { "version": "2.2.7", - "resolved": "https://registry.npmjs.org/squire-rte/-/squire-rte-2.2.7.tgz", - "integrity": "sha512-BUMRrqnckM+hV3lwjajcckViS1aKgYFImA3Sjctygsln7f7DXILsYj7SOEdgJgNkTDGMBVkKIkTs0v0wOj2VrQ==", "license": "MIT" }, "node_modules/ssri": { "version": "10.0.6", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", - "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", "dev": true, "license": "ISC", "dependencies": { @@ -9697,8 +7704,6 @@ }, "node_modules/stat-mode": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", - "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", "dev": true, "license": "MIT", "engines": { @@ -9707,8 +7712,6 @@ }, "node_modules/statuses": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true, "license": "MIT", "engines": { @@ -9717,8 +7720,6 @@ }, "node_modules/stream-combiner": { "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", "license": "MIT", "dependencies": { "duplexer": "~0.1.1" @@ -9726,8 +7727,6 @@ }, "node_modules/string_decoder": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -9735,8 +7734,6 @@ }, "node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "devOptional": true, "license": "MIT", "dependencies": { @@ -9751,8 +7748,6 @@ "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -9766,8 +7761,6 @@ }, "node_modules/stringify-object-es5": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/stringify-object-es5/-/stringify-object-es5-2.5.0.tgz", - "integrity": "sha512-vE7Xdx9ylG4JI16zy7/ObKUB+MtxuMcWlj/WHHr3+yAlQoN6sst2stU9E+2Qs3OrlJw/Pf3loWxL1GauEHf6MA==", "license": "BSD-2-Clause", "dependencies": { "is-plain-obj": "^1.0.0", @@ -9779,8 +7772,6 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "devOptional": true, "license": "MIT", "dependencies": { @@ -9793,8 +7784,6 @@ "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -9806,8 +7795,6 @@ }, "node_modules/strip-bom": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, "license": "MIT", "engines": { @@ -9816,8 +7803,6 @@ }, "node_modules/strip-indent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9829,8 +7814,6 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", "engines": { @@ -9842,8 +7825,6 @@ }, "node_modules/strip-outer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", - "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", "dev": true, "license": "MIT", "dependencies": { @@ -9855,8 +7836,6 @@ }, "node_modules/strip-outer/node_modules/escape-string-regexp": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "license": "MIT", "engines": { @@ -9865,8 +7844,6 @@ }, "node_modules/sumchecker": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", "license": "Apache-2.0", "dependencies": { "debug": "^4.1.0" @@ -9877,8 +7854,6 @@ }, "node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -9890,8 +7865,6 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -9902,21 +7875,15 @@ }, "node_modules/symbol-tree": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true, "license": "MIT" }, "node_modules/systemjs": { "version": "6.15.1", - "resolved": "https://registry.npmjs.org/systemjs/-/systemjs-6.15.1.tgz", - "integrity": "sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==", "license": "MIT" }, "node_modules/tar": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "license": "ISC", "dependencies": { "chownr": "^2.0.0", @@ -9932,8 +7899,6 @@ }, "node_modules/tar-fs": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -9944,14 +7909,10 @@ }, "node_modules/tar-fs/node_modules/chownr": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, "node_modules/tar-stream": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "license": "MIT", "dependencies": { "bl": "^4.0.3", @@ -9966,8 +7927,6 @@ }, "node_modules/tar/node_modules/fs-minipass": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "license": "ISC", "dependencies": { "minipass": "^3.0.0" @@ -9978,8 +7937,6 @@ }, "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -9990,8 +7947,6 @@ }, "node_modules/tar/node_modules/minipass": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "license": "ISC", "engines": { "node": ">=8" @@ -9999,8 +7954,6 @@ }, "node_modules/temp-file": { "version": "3.4.0", - "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", - "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", "dev": true, "license": "MIT", "dependencies": { @@ -10010,8 +7963,6 @@ }, "node_modules/temp-file/node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10025,8 +7976,6 @@ }, "node_modules/terser": { "version": "5.34.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz", - "integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -10044,15 +7993,11 @@ }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "license": "MIT" }, "node_modules/testdouble": { "version": "3.18.0", - "resolved": "https://registry.npmjs.org/testdouble/-/testdouble-3.18.0.tgz", - "integrity": "sha512-awRay/WxNHYz0SJrjvvg1xE4QQkbKgWFN1VNhhb132JSO2FSWUW4cebUtD0HjWWwrvpN3uFsVeaUhwpmVlzlkg==", "license": "MIT", "dependencies": { "lodash": "^4.17.21", @@ -10066,33 +8011,23 @@ }, "node_modules/text-table": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true, "license": "MIT" }, "node_modules/theredoc": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/theredoc/-/theredoc-1.0.0.tgz", - "integrity": "sha512-KU3SA3TjRRM932jpNfD3u4Ec3bSvedyo5ITPI7zgWYnKep7BwQQaxlhI9qbO+lKJoRnoAbEVfMcAHRuKVYikDA==", "license": "MIT" }, "node_modules/through": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "license": "MIT" }, "node_modules/tiny-typed-emitter": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", - "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", "license": "MIT" }, "node_modules/tmp": { "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", "dev": true, "license": "MIT", "engines": { @@ -10101,8 +8036,6 @@ }, "node_modules/tmp-promise": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", - "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10111,8 +8044,6 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -10123,8 +8054,6 @@ }, "node_modules/toidentifier": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, "license": "MIT", "engines": { @@ -10133,8 +8062,6 @@ }, "node_modules/tough-cookie": { "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -10149,8 +8076,6 @@ }, "node_modules/tough-cookie/node_modules/universalify": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true, "license": "MIT", "engines": { @@ -10159,8 +8084,6 @@ }, "node_modules/tr46": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", - "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", "dev": true, "license": "MIT", "dependencies": { @@ -10172,8 +8095,6 @@ }, "node_modules/trim-repeated": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", - "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", "dev": true, "license": "MIT", "dependencies": { @@ -10185,8 +8106,6 @@ }, "node_modules/trim-repeated/node_modules/escape-string-regexp": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "license": "MIT", "engines": { @@ -10195,8 +8114,6 @@ }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", "dev": true, "license": "WTFPL", "dependencies": { @@ -10205,15 +8122,11 @@ }, "node_modules/tslib": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true, "license": "0BSD" }, "node_modules/tsutils": { "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", "dev": true, "license": "MIT", "dependencies": { @@ -10228,8 +8141,6 @@ }, "node_modules/tunnel-agent": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" @@ -10240,8 +8151,6 @@ }, "node_modules/type-check": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -10253,8 +8162,6 @@ }, "node_modules/type-fest": { "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -10266,8 +8173,6 @@ }, "node_modules/type-is": { "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, "license": "MIT", "dependencies": { @@ -10280,8 +8185,6 @@ }, "node_modules/typescript": { "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -10294,8 +8197,6 @@ }, "node_modules/undici": { "version": "6.19.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.2.tgz", - "integrity": "sha512-JfjKqIauur3Q6biAtHJ564e3bWa8VvT+7cSiOJHFbX4Erv6CLGDpg8z+Fmg/1OI/47RA+GI2QZaF48SSaLvyBA==", "license": "MIT", "engines": { "node": ">=18.17" @@ -10303,14 +8204,10 @@ }, "node_modules/undici-types": { "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "license": "MIT" }, "node_modules/unique-filename": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", - "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", "dev": true, "license": "ISC", "dependencies": { @@ -10322,8 +8219,6 @@ }, "node_modules/unique-slug": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", - "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", "dev": true, "license": "ISC", "dependencies": { @@ -10335,22 +8230,16 @@ }, "node_modules/universal-github-app-jwt": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.0.tgz", - "integrity": "sha512-G5o6f95b5BggDGuUfKDApKaCgNYy2x7OdHY0zSMF081O0EJobw+1130VONhrA7ezGSV2FNOGyM+KQpQZAr9bIQ==", "dev": true, "license": "MIT" }, "node_modules/universal-user-agent": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", - "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", "dev": true, "license": "ISC" }, "node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "license": "MIT", "engines": { "node": ">= 10.0.0" @@ -10358,8 +8247,6 @@ }, "node_modules/unpipe": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, "license": "MIT", "engines": { @@ -10368,8 +8255,6 @@ }, "node_modules/update-browserslist-db": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -10399,8 +8284,6 @@ }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "devOptional": true, "license": "BSD-2-Clause", "dependencies": { @@ -10409,8 +8292,6 @@ }, "node_modules/url-parse": { "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10420,21 +8301,15 @@ }, "node_modules/utf8-byte-length": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", - "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", "dev": true, "license": "(WTFPL OR MIT)" }, "node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, "node_modules/utils-merge": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "dev": true, "license": "MIT", "engines": { @@ -10443,8 +8318,6 @@ }, "node_modules/uuid": { "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "license": "MIT", "bin": { "uuid": "dist/bin/uuid" @@ -10452,8 +8325,6 @@ }, "node_modules/validate-npm-package-license": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -10463,8 +8334,6 @@ }, "node_modules/vary": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, "license": "MIT", "engines": { @@ -10488,8 +8357,6 @@ }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "license": "MIT", "dependencies": { @@ -10501,8 +8368,6 @@ }, "node_modules/web-streams-polyfill": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", "license": "MIT", "engines": { "node": ">= 8" @@ -10510,8 +8375,6 @@ }, "node_modules/webidl-conversions": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -10520,8 +8383,6 @@ }, "node_modules/whatwg-encoding": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10533,8 +8394,6 @@ }, "node_modules/whatwg-mimetype": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", "engines": { @@ -10543,8 +8402,6 @@ }, "node_modules/whatwg-url": { "version": "14.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", - "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", "dev": true, "license": "MIT", "dependencies": { @@ -10557,8 +8414,6 @@ }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -10572,14 +8427,10 @@ }, "node_modules/winreg": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/winreg/-/winreg-1.2.4.tgz", - "integrity": "sha512-IHpzORub7kYlb8A43Iig3reOvlcBJGX9gZ0WycHhghHtA65X0LYnMRuJs+aH1abVnMJztQkvQNlltnbPi5aGIA==", "license": "BSD-2-Clause" }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { @@ -10588,8 +8439,6 @@ }, "node_modules/wrap-ansi": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10607,8 +8456,6 @@ "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { @@ -10625,8 +8472,6 @@ }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", "engines": { @@ -10638,8 +8483,6 @@ }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, "license": "MIT", "engines": { @@ -10651,15 +8494,11 @@ }, "node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, "license": "MIT" }, "node_modules/wrap-ansi/node_modules/string-width": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "license": "MIT", "dependencies": { @@ -10676,8 +8515,6 @@ }, "node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10692,14 +8529,10 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, "node_modules/ws": { "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, "license": "MIT", "engines": { @@ -10720,8 +8553,6 @@ }, "node_modules/xhr2": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz", - "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==", "dev": true, "license": "MIT", "engines": { @@ -10730,8 +8561,6 @@ }, "node_modules/xml-name-validator": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -10740,8 +8569,6 @@ }, "node_modules/xmlbuilder": { "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", "devOptional": true, "license": "MIT", "engines": { @@ -10750,15 +8577,11 @@ }, "node_modules/xmlchars": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true, "license": "MIT" }, "node_modules/y18n": { "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, "license": "ISC", "engines": { @@ -10767,14 +8590,10 @@ }, "node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, "node_modules/yaml": { "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", - "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -10785,8 +8604,6 @@ }, "node_modules/yargs": { "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "license": "MIT", "dependencies": { @@ -10804,8 +8621,6 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { @@ -10814,8 +8629,6 @@ }, "node_modules/yauzl": { "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", @@ -10824,8 +8637,6 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -10837,8 +8648,6 @@ }, "node_modules/zip-stream": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", - "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", "dev": true, "license": "MIT", "peer": true, @@ -10853,8 +8662,6 @@ }, "node_modules/zip-stream/node_modules/archiver-utils": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", - "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", "dev": true, "license": "MIT", "peer": true, @@ -10876,8 +8683,6 @@ }, "node_modules/zip-stream/node_modules/brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "peer": true, @@ -10888,9 +8693,6 @@ }, "node_modules/zip-stream/node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", "peer": true, @@ -10911,8 +8713,6 @@ }, "node_modules/zip-stream/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "peer": true, @@ -10925,8 +8725,6 @@ }, "node_modules/zx": { "version": "8.1.5", - "resolved": "https://registry.npmjs.org/zx/-/zx-8.1.5.tgz", - "integrity": "sha512-gvmiYPvDDEz2Gcc37x7pJkipTKcFIE18q9QlSI1p5qoPDtoSn3jmGuWD0eEb7HuxEH5aDD7N/RVgH8BqSxbKzA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -10942,8 +8740,6 @@ }, "node_modules/zx/node_modules/@types/fs-extra": { "version": "11.0.4", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", - "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", "dev": true, "license": "MIT", "optional": true, @@ -10973,14 +8769,10 @@ }, "packages/licc/node_modules/@types/node": { "version": "17.0.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", - "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", "license": "MIT" }, "packages/licc/node_modules/chalk": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -10991,8 +8783,6 @@ }, "packages/licc/node_modules/commander": { "version": "9.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.3.0.tgz", - "integrity": "sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw==", "license": "MIT", "engines": { "node": "^12.20.0 || >=14" @@ -11000,8 +8790,6 @@ }, "packages/licc/node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -11014,8 +8802,6 @@ }, "packages/licc/node_modules/globby": { "version": "13.2.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", - "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", "license": "MIT", "dependencies": { "dir-glob": "^3.0.1", @@ -11033,8 +8819,6 @@ }, "packages/licc/node_modules/slash": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", "license": "MIT", "engines": { "node": ">=12" @@ -11045,8 +8829,6 @@ }, "packages/licc/node_modules/zx": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/zx/-/zx-6.1.0.tgz", - "integrity": "sha512-LcvyN946APdktLPm1ta4wfNiJaDuq1iHOTDwApP69ug5hNYWzMYaVMC5Ek6Ny4nnSLpJ6wdY42feR/2LY5/nVA==", "license": "Apache-2.0", "dependencies": { "@types/fs-extra": "^9.0.13", @@ -11069,6 +8851,27 @@ "node": ">= 16.0.0" } }, + "packages/node-mimimi": { + "name": "@tutao/node-mimimi", + "version": "250.241018.00", + "license": "MIT", + "devDependencies": { + "@napi-rs/cli": "^2.18.4", + "@tutao/otest": "250.241018.0", + "typescript": "5.3.3", + "zx": "8.1.5" + }, + "engines": { + "node": ">= 20" + } + }, + "packages/node-mimimi/node_modules/@tutao/otest": { + "version": "250.241018.0", + "resolved": "https://registry.npmjs.org/@tutao/otest/-/otest-250.241018.0.tgz", + "integrity": "sha512-XOd6+jMWga8A1VmbUT/uTXeFGdvq/4xhQN539kYDOdDTtXCnnQnXxLj75/To+9qN4WAwEAOtlDBlmCkAq+RHkw==", + "dev": true, + "license": "GPL-3.0" + }, "packages/otest": { "name": "@tutao/otest", "version": "251.241113.0", diff --git a/package.json b/package.json index af6180cab46..bb6c9db50e2 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "fix": "npm run style:fix && npm run lint:fix" }, "dependencies": { + "@tutao/node-mimimi": "251.241030.0", "@tutao/oxmsg": "0.0.9-beta.0", "@tutao/tuta-wasm-loader": "251.241113.0", "@tutao/tutanota-crypto": "251.241113.0", diff --git a/packages/node-mimimi/.cargo/config.toml b/packages/node-mimimi/.cargo/config.toml new file mode 100644 index 00000000000..0c17df095ca --- /dev/null +++ b/packages/node-mimimi/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.x86_64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] \ No newline at end of file diff --git a/packages/node-mimimi/.gitignore b/packages/node-mimimi/.gitignore new file mode 100644 index 00000000000..4c46a384c6f --- /dev/null +++ b/packages/node-mimimi/.gitignore @@ -0,0 +1,201 @@ +# Created by https://www.toptal.com/developers/gitignore/api/node +# Edit at https://www.toptal.com/developers/gitignore?templates=node + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# End of https://www.toptal.com/developers/gitignore/api/node + +# Created by https://www.toptal.com/developers/gitignore/api/macos +# Edit at https://www.toptal.com/developers/gitignore?templates=macos + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +# End of https://www.toptal.com/developers/gitignore/api/macos + +# Created by https://www.toptal.com/developers/gitignore/api/windows +# Edit at https://www.toptal.com/developers/gitignore?templates=windows + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/windows + +#Added by cargo + +/target +Cargo.lock + +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +*.node + +/java/.idea +/java/build +.gradle \ No newline at end of file diff --git a/packages/node-mimimi/.npmignore b/packages/node-mimimi/.npmignore new file mode 100644 index 00000000000..ec144db2a71 --- /dev/null +++ b/packages/node-mimimi/.npmignore @@ -0,0 +1,13 @@ +target +Cargo.lock +.cargo +.github +npm +.eslintrc +.prettierignore +rustfmt.toml +yarn.lock +*.node +.yarn +__test__ +renovate.json diff --git a/packages/node-mimimi/Cargo.toml b/packages/node-mimimi/Cargo.toml new file mode 100644 index 00000000000..06706f6a38c --- /dev/null +++ b/packages/node-mimimi/Cargo.toml @@ -0,0 +1,56 @@ +[package] +edition = "2021" +name = "tutao_node-mimimi" +version = "251.241030.0" + +[lib] +# need to have lib to be able to use this from rust examples +crate-type = ["cdylib", "lib"] + +[profile.release] +lto = true +strip = "symbols" + + +[dependencies] +tuta-sdk = { path = "../../tuta-sdk/rust/sdk", features = ["net"] } +async-trait = "0.1.83" +rand = { version = "0.8.5" } +mail-parser = { version = "0.9.4" } +thiserror = { version = "1.0.64" } +# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix +napi = { version = "2.16.12", default-features = false, features = ["napi9", "async", "tokio_rt"] } +napi-derive = { version = "2.16.12", optional = true } +mail-builder = { version = "0.3.2" } + +# used for tuta-imap +serde = { version = "1.0.210", features = ["derive"] } +log = { version = "0.4.22" } +rustls = { version = "0.23.13", features = ["std"] } +regex = "1.11.1" + +[dependencies.imap-codec] +git = "https://github.com/duesee/imap-codec.git" +rev = "16a5c182285c7a895b06276f68a116caf2cb294f" +features = ["serde", "starttls", "ext_id", "ext_metadata"] + + +[build-dependencies] +napi-build = { version = "2.1.3" } + +[dev-dependencies] +tokio = { version = "1", features = ["full"] } +serde_json = "1" + +# tuta-imap +j4rs = { version = "0.20.0" } +lazy_static = { version = "0.2.11" } + +[features] +default = ["javascript"] +# needed to turn off the autogenerated ffi when using the examples +javascript = ["dep:napi-derive"] + +[[example]] +name = "import_imap_mail" +path = "examples/import_imap_mail.rs" \ No newline at end of file diff --git a/packages/node-mimimi/README.md b/packages/node-mimimi/README.md new file mode 100644 index 00000000000..eab039c5a9b --- /dev/null +++ b/packages/node-mimimi/README.md @@ -0,0 +1,38 @@ +# Node-Mimimi + +**M**ini **IM**AP **IM**porter **I**mplementation + +A native node module enabling the tuta mail desktop client to import mail from IMAP servers into your tuta account. +It's using [napi-rs](https://napi.rs/docs/introduction/getting-started) for project setup and to generate the bindings. + +## Building + +napi-rs by default generates a common js module that is supposed to be compatible with ESM named imports, but we had +problems getting it to import in all cases. One solution was found on +the [napi-rs github](https://github.com/napi-rs/napi-rs/issues/1429#issuecomment-1379743978). It works, but requires us +to build like this: + +`napi build --platform . --js binding.cjs --dts binding.d.cts` + +# Compilation + +See https://napi.rs/docs/cross-build/summary + +### Setup + +1. `apt install clang llvm` + +#### Linux (from linux): + +1. `rustup target add x86_64-unknown-linux-gnu` + +#### Windows (from linux): + +1. `rustup target add x86_64-pc-windows-msvc` +2. `cargo install cargo-xwin` + +#### MacOS (**only** from MacOS): + +1. `rustup target add x86_64-apple-darwin` +2. `rustup target add aarch64-apple-darwin` + diff --git a/packages/node-mimimi/build.rs b/packages/node-mimimi/build.rs new file mode 100644 index 00000000000..2797a5f0440 --- /dev/null +++ b/packages/node-mimimi/build.rs @@ -0,0 +1,33 @@ +extern crate napi_build; +use std::process::Command; + +const GREENMAIL_TEST_SERVER_JAR: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/java/build/libs/greenmail-test-server.jar" +); +const BUILD_WATCHLIST: &[&str] = &["/java/src/", "/java/build/libs/greenmail-test-server.jar"]; + +fn main() { + #[cfg(feature = "javascript")] + napi_build::setup(); + + println!("cargo::rustc-env=GREENMAIL_TEST_SERVER_JAR={GREENMAIL_TEST_SERVER_JAR}",); + for watch in BUILD_WATCHLIST { + println!("cargo::rerun-if-changed={watch}"); + } + + run_gradle_jar(); +} + +fn run_gradle_jar() { + Command::new("/opt/gradle-8.5/bin/gradle") + .current_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/java")) + .args(["jar"]) + .spawn() + .expect("Cannot spawn gradle command") + .wait() + .expect("Cannot wait for gradle command") + .success() + .then_some(()) + .expect("gradle exited with non-success status code"); +} diff --git a/packages/node-mimimi/examples/import_imap_mail.rs b/packages/node-mimimi/examples/import_imap_mail.rs new file mode 100644 index 00000000000..06767c18071 --- /dev/null +++ b/packages/node-mimimi/examples/import_imap_mail.rs @@ -0,0 +1,92 @@ +/// How to: Import IMAP mail from Greenmail to TutaMail +/// +/// +/// 1. Start GreenMail server +/// * A greenmail version >= 2.1 is required +/// `java -Dgreenmail.setup.test.all -Dgreenmail.verbose -jar ./greenmail-standalone-2.1.0.jar` +/// +/// 2. Create default user +/// ``` +/// curl -X POST "http://localhost:8080/api/user" -H 'accept: application/json' -H 'content-type: application/json' -d '{"email":"sug@localhost","login":"sug@localhost","password":"sug"}' +/// ``` +/// +/// 3. Add account in Thunderbird with manual configuration (type in user credentials and then click on "configure manually"). +/// * IMAP: with host: .localhost, port: 3143, connection security: None, authentication method: Normal password, (imap_reader without SSL) +/// * SMTP with host: .localhost, port: 3025, connection security: None, authentication method: Normal password (smtp without SSL) +/// +/// 4. Send Some Mail (by creating another account following step 2) to sug@localhost +/// * We can use HTML and all kinds of formatting. +/// * This example will always try to import the first mail. Importing multiple mails is not implemented yet. +/// +/// 5. Start tutadb server on `http://localhost:9000` which should have this user `map-free@tutanota.de:map` +/// +/// 6. Check the list of mails in the `Draft` folder and run this example. +/// * Now the mail you wrote in step 4 should appear in draft folder. +/// * Running this example multiple times will always import first mail retrieved by the imap_reader `SEARCH` command. +/// +/// 7. Smile :) +#[cfg(not(feature = "javascript"))] +#[tokio::main] +async fn main() { + use std::sync::Arc; + use tutao_node_mimimi::importer::imap_reader::{ + import_client::ImapImport, ImapCredentials, ImapImportConfig, LoginMechanism, + }; + use tutao_node_mimimi::importer::{ImportSource, ImportState, ImporterApi}; + use tutasdk::folder_system::MailSetKind; + use tutasdk::net::native_rest_client::NativeRestClient; + use tutasdk::Sdk; + + let sdk = Sdk::new( + "http://localhost:9000".to_string(), + Arc::new(NativeRestClient::try_new().unwrap()), + ); + let logged_in_sdk = sdk + .create_session("map-free@tutanota.de", "map") + .await + .unwrap(); + let imap_import_config = ImapImportConfig { + root_import_mail_folder_name: "/".to_string(), + credentials: ImapCredentials { + host: "127.0.0.1".to_string(), + port: 3993, + login_mechanism: LoginMechanism::Plain { + username: "sug@localhost".to_string(), + password: "sug".to_string(), + }, + }, + }; + + let import_source = ImportSource::RemoteImap { + imap_import_client: ImapImport::new(imap_import_config), + }; + + let mail_facade = logged_in_sdk.mail_facade(); + let mailbox = mail_facade.load_user_mailbox().await.unwrap(); + let folders = mail_facade + .load_folders_for_mailbox(&mailbox) + .await + .unwrap(); + let inbox_folder = folders + .system_folder_by_type(MailSetKind::Inbox) + .expect("inbox should exist"); + + let mut importer = ImporterApi::new( + logged_in_sdk, + import_source, + "map-free@tutanota.de".to_string(), + inbox_folder._id.clone(), + ); + + let import_status = importer + .continue_import() + .await + .expect("Cannot complete import"); + assert!(ImportState::Finished == import_status.state,); + assert!(import_status.imported_mails > 0); +} + +#[cfg(feature = "javascript")] +fn main() { + panic!("Can not run this example in javascript environment"); +} diff --git a/packages/node-mimimi/index.d.ts b/packages/node-mimimi/index.d.ts new file mode 100644 index 00000000000..9bffef08caf --- /dev/null +++ b/packages/node-mimimi/index.d.ts @@ -0,0 +1,46 @@ +/* tslint:disable */ +/* eslint-disable */ + +/* auto-generated by NAPI-RS */ + +/** + * current state of the imap_reader import for this tuta account + * requires an initialized SDK! + */ +export const enum ImportState { + NotInitialized = 0, + Paused = 1, + Running = 2, + Postponed = 3, + Finished = 4, +} +export interface ImportStatus { + state: ImportState + importedMails: number +} +/** Passed in from js-side, will be validated before being converted to proper tuta sdk credentials. */ +export interface TutaCredentials { + apiUrl: string + clientVersion: string + login: string + userId: string + accessToken: string + encryptedPassphraseKey: Array + credentialType: TutaCredentialType +} +export const enum TutaCredentialType { + Internal = 0, + External = 1, +} +/** Wrapper for `Importer` to be used from napi-rs interface */ +export declare class ImporterApi { + continueImport(): Promise + deleteImport(): Promise + pauseImport(): Promise + static createFileImporter( + tutaCredentials: TutaCredentials, + targetOwnerGroup: string, + targetMailFolder: [string, string], + sourcePaths: Array, + ): Promise +} diff --git a/packages/node-mimimi/java/build.gradle.kts b/packages/node-mimimi/java/build.gradle.kts new file mode 100644 index 00000000000..788c346467e --- /dev/null +++ b/packages/node-mimimi/java/build.gradle.kts @@ -0,0 +1,24 @@ +repositories { + mavenLocal() + maven { + credentials(PasswordCredentials::class) + url = uri("https://next.tutao.de/nexus/content/groups/public/") + } +} + +plugins { + java +} + +dependencies { + implementation("com.icegreen:greenmail-standalone:2.1.0") +} + +tasks.jar { + val dependencies = configurations + .runtimeClasspath + .get() + .map(::zipTree) // OR .map { zipTree(it) } + from(dependencies) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} \ No newline at end of file diff --git a/packages/node-mimimi/java/settings.gradle.kts b/packages/node-mimimi/java/settings.gradle.kts new file mode 100644 index 00000000000..a9467e9f983 --- /dev/null +++ b/packages/node-mimimi/java/settings.gradle.kts @@ -0,0 +1,24 @@ +rootProject.name = "greenmail-test-server" + +pluginManagement { + repositories { + mavenLocal() + maven { + url = uri("https://next.tutao.de/nexus/content/groups/public/") + credentials(PasswordCredentials::class) + } + } +} + +buildscript { + repositories { + mavenLocal() + maven { + credentials(PasswordCredentials::class) + url = uri("https://next.tutao.de/nexus/content/groups/public/") + } + } + dependencies { + classpath(group = "de.tutao.gradle", name = "devDefaults", version = "3.6.2") + } +} \ No newline at end of file diff --git a/packages/node-mimimi/java/src/main/java/greenmailserver/GreenMailServer.java b/packages/node-mimimi/java/src/main/java/greenmailserver/GreenMailServer.java new file mode 100644 index 00000000000..25f7e93d62c --- /dev/null +++ b/packages/node-mimimi/java/src/main/java/greenmailserver/GreenMailServer.java @@ -0,0 +1,63 @@ +package greenmailserver; + +import com.icegreen.greenmail.user.GreenMailUser; +import com.icegreen.greenmail.user.UserException; +import com.icegreen.greenmail.util.GreenMail; +import com.icegreen.greenmail.util.ServerSetup; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; + +import java.io.ByteArrayInputStream; + + +public class GreenMailServer { + public static final String imapsHost = "127.0.0.1"; + + private GreenMail greenMail; + + public GreenMailUser userMap; + public GreenMailUser userSug; + + public GreenMailServer(Integer p) { + setSystemClassLoaderForCurrentThreadContext(); + + ServerSetup defaultImapsProps = new ServerSetup(p, imapsHost, "imaps"); + try { + greenMail = new GreenMail(defaultImapsProps); + } catch (Exception e) { + e.printStackTrace(); + } + + greenMail.start(); + + try { + userMap = greenMail.getUserManager().createUser("map@example.org", "map@example.org", "map"); + userSug = greenMail.getUserManager().createUser("sug@example.org", "sug@example.org", "sug"); + } catch (UserException e) { + throw new RuntimeException(e); + } + } + + public void stop() { + greenMail.stop(); + } + + public void store_mail(String recipientAddress, String mimeMsg) throws MessagingException { + var recipient = greenMail.getUserManager().getUserByEmail(recipientAddress); + var mimeMessage = new MimeMessage(null, new ByteArrayInputStream(mimeMsg.getBytes())); + recipient.deliver(mimeMessage); + } + + + // ========= configuration required for (rust) jni interface ================== + + // For the class loaded by jni, .getCurrentThread().getContextClassLoader() will be null + // set it to systemClassLoader + public static void setSystemClassLoaderForCurrentThreadContext() { + if (Thread.currentThread().getContextClassLoader() == null) { + ClassLoader cl = ClassLoader.getSystemClassLoader(); + Thread.currentThread().setContextClassLoader(cl); + } + } + +} diff --git a/packages/node-mimimi/make.js b/packages/node-mimimi/make.js new file mode 100644 index 00000000000..dc61be1e577 --- /dev/null +++ b/packages/node-mimimi/make.js @@ -0,0 +1,41 @@ +import { Argument, program } from "commander" +import { $ } from "zx" + +await program + .usage("[options] [win|linux|darwin|native]") + .addArgument(new Argument("platform").choices(["win", "linux", "darwin", "native"]).default("native").argOptional()) + .option("-c, --clean", "clean build artifacts") + .option("-r, --release", "run a release build") + .option("-t, --test", "also build the test suite") + .action(run) + .parseAsync(process.argv) + +function getTarget(platform) { + switch (platform) { + case "win": + return "--target=x86_64-pc-windows-msvc" + case "linux": + return "--target=x86_64-unknown-linux-gnu" + case "darwin": + return "--target=x86_64-apple-darwin" + case "native": + return "" + default: + throw new Error(`unknown platform ${platform}`) + } +} + +async function run(platform, { clean, release, test }) { + if (clean) { + await $`rm -r -f ./build` + await $`rm -r -f ./target` + await $`rm -r -f ./dist` + } + + const target = getTarget(platform) + const releaseFlag = release ? "--release" : "" + await $`napi build dist --platform --js binding.cjs --dts binding.d.cts ${target} ${releaseFlag} --features javascript` + if (test) { + await $`npx tsc -b test` + } +} diff --git a/packages/node-mimimi/npm/darwin-universal/README.md b/packages/node-mimimi/npm/darwin-universal/README.md new file mode 100644 index 00000000000..71296da77ca --- /dev/null +++ b/packages/node-mimimi/npm/darwin-universal/README.md @@ -0,0 +1,3 @@ +# `@tutao/node-mimimi-darwin-universal` + +This is the **universal-apple-darwin** binary for `@tutao/node-mimimi` diff --git a/packages/node-mimimi/npm/darwin-universal/package.json b/packages/node-mimimi/npm/darwin-universal/package.json new file mode 100644 index 00000000000..d40371e8c5b --- /dev/null +++ b/packages/node-mimimi/npm/darwin-universal/package.json @@ -0,0 +1,15 @@ +{ + "name": "@tutao/node-mimimi-darwin-universal", + "version": "0.0.0", + "os": [ + "darwin" + ], + "main": "node-mimimi.darwin-universal.node", + "files": [ + "node-mimimi.darwin-universal.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + } +} diff --git a/packages/node-mimimi/npm/linux-x64-gnu/README.md b/packages/node-mimimi/npm/linux-x64-gnu/README.md new file mode 100644 index 00000000000..a6364039dd4 --- /dev/null +++ b/packages/node-mimimi/npm/linux-x64-gnu/README.md @@ -0,0 +1,3 @@ +# `@tutao/node-mimimi-linux-x64-gnu` + +This is the **x86_64-unknown-linux-gnu** binary for `@tutao/node-mimimi` diff --git a/packages/node-mimimi/npm/linux-x64-gnu/package.json b/packages/node-mimimi/npm/linux-x64-gnu/package.json new file mode 100644 index 00000000000..d7245e81837 --- /dev/null +++ b/packages/node-mimimi/npm/linux-x64-gnu/package.json @@ -0,0 +1,21 @@ +{ + "name": "@tutao/node-mimimi-linux-x64-gnu", + "version": "0.0.0", + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "main": "node-mimimi.linux-x64-gnu.node", + "files": [ + "node-mimimi.linux-x64-gnu.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "libc": [ + "glibc" + ] +} diff --git a/packages/node-mimimi/npm/win32-x64-msvc/README.md b/packages/node-mimimi/npm/win32-x64-msvc/README.md new file mode 100644 index 00000000000..0961fd962af --- /dev/null +++ b/packages/node-mimimi/npm/win32-x64-msvc/README.md @@ -0,0 +1,3 @@ +# `@tutao/node-mimimi-win32-x64-msvc` + +This is the **x86_64-pc-windows-msvc** binary for `@tutao/node-mimimi` diff --git a/packages/node-mimimi/npm/win32-x64-msvc/package.json b/packages/node-mimimi/npm/win32-x64-msvc/package.json new file mode 100644 index 00000000000..6f50d754ed3 --- /dev/null +++ b/packages/node-mimimi/npm/win32-x64-msvc/package.json @@ -0,0 +1,18 @@ +{ + "name": "@tutao/node-mimimi-win32-x64-msvc", + "version": "0.0.0", + "os": [ + "win32" + ], + "cpu": [ + "x64" + ], + "main": "node-mimimi.win32-x64-msvc.node", + "files": [ + "node-mimimi.win32-x64-msvc.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + } +} diff --git a/packages/node-mimimi/package.json b/packages/node-mimimi/package.json new file mode 100644 index 00000000000..bad37aaafc2 --- /dev/null +++ b/packages/node-mimimi/package.json @@ -0,0 +1,35 @@ +{ + "name": "@tutao/node-mimimi", + "version": "250.241018.00", + "main": "./dist/binding.cjs", + "types": "./dist/binding.d.ts", + "napi": { + "name": "node-mimimi", + "triples": { + "defaults": false, + "additional": [ + "x86_64-pc-windows-msvc", + "x86_64-unknown-linux-gnu", + "universal-apple-darwin" + ] + } + }, + "license": "MIT", + "devDependencies": { + "@tutao/otest": "250.241018.0", + "@napi-rs/cli": "^2.18.4", + "typescript": "5.3.3", + "zx": "8.1.5" + }, + "engines": { + "node": ">= 20" + }, + "type": "module", + "scripts": { + "build": "node make", + "prepublishOnly": "napi prepublish -t npm", + "test": "node make -t && cd build && node Suite.js && cargo test", + "universal": "napi universal", + "version": "napi version" + } +} diff --git a/packages/node-mimimi/rust-toolchain b/packages/node-mimimi/rust-toolchain new file mode 100644 index 00000000000..870bbe4e50e --- /dev/null +++ b/packages/node-mimimi/rust-toolchain @@ -0,0 +1 @@ +stable \ No newline at end of file diff --git a/packages/node-mimimi/rustfmt.toml b/packages/node-mimimi/rustfmt.toml new file mode 100644 index 00000000000..c22683acdcd --- /dev/null +++ b/packages/node-mimimi/rustfmt.toml @@ -0,0 +1,16 @@ +edition = "2024" + +tab_spaces = 4 +hard_tabs = true +newline_style = "Unix" +max_width = 100 +match_block_trailing_comma = true +use_field_init_shorthand = true + +# TODO: Very nice-to-have, but currently nightly only +#normalize_comments = true +#normalize_doc_attributes = true +#group_imports = "StdExternalCrate" +#reorder_impl_items = true +#combine_control_expr = false +#condense_wildcard_suffixes = true diff --git a/packages/node-mimimi/src/importer.rs b/packages/node-mimimi/src/importer.rs new file mode 100644 index 00000000000..ec563b6beb2 --- /dev/null +++ b/packages/node-mimimi/src/importer.rs @@ -0,0 +1,493 @@ +use crate::importer::file_reader::import_client::{FileImport, FileIterationError}; +use crate::importer::imap_reader::import_client::{ImapImport, ImapIterationError}; +use crate::importer::imap_reader::ImapImportConfig; +use crate::importer::importable_mail::ImportableMail; +use crate::tuta::credentials::TutaCredentials; +use napi::bindgen_prelude::Error as NapiError; +use std::sync::Arc; +use tutasdk::crypto::aes::Iv; +use tutasdk::crypto::key::GenericAesKey; +use tutasdk::crypto::randomizer_facade::RandomizerFacade; +use tutasdk::entities::generated::tutanota::{ImportMailData, ImportMailPostIn, ImportMailPostOut}; +use tutasdk::login::Credentials; +use tutasdk::net::native_rest_client::NativeRestClient; +use tutasdk::services::generated::tutanota::ImportMailService; +use tutasdk::services::ExtraServiceParams; +use tutasdk::GeneratedId; +use tutasdk::{IdTupleGenerated, LoggedInSdk, Sdk}; + +pub type NapiTokioMutex = napi::tokio::sync::Mutex; + +pub mod extend_mail_parser; +pub mod file_reader; +pub mod imap_reader; +mod importable_mail; +mod plain_text_to_html_converter; + +#[derive(Clone, PartialEq)] +pub enum ImportParams { + Imap { + imap_import_config: ImapImportConfig, + }, + LocalFile { + file_path: String, + is_mbox: bool, + }, +} + +/// current state of the imap_reader import for this tuta account +/// requires an initialized SDK! +#[cfg_attr(feature = "javascript", napi_derive::napi)] +#[cfg_attr(not(feature = "javascript"), derive(Clone))] +#[derive(PartialEq, Default)] +#[cfg_attr(test, derive(Debug))] +pub enum ImportState { + #[default] + NotInitialized, + Paused, + Running, + Postponed, + Finished, +} + +#[cfg_attr(feature = "javascript", napi_derive::napi(object))] +#[derive(PartialEq, Clone, Default)] +#[cfg_attr(test, derive(Debug))] +pub struct ImportStatus { + pub state: ImportState, + pub imported_mails: u32, +} + +struct Importer { + status: ImportStatus, + logged_in_sdk: Arc, + target_owner_group: GeneratedId, + target_mail_folder: IdTupleGenerated, + import_source: ImportSource, + randomizer_facade: RandomizerFacade, +} + +pub enum ImportSource { + RemoteImap { imap_import_client: ImapImport }, + LocalFile { fs_email_client: FileImport }, +} + +/// Wrapper for `Importer` to be used from napi-rs interface +#[cfg_attr(feature = "javascript", napi_derive::napi)] +pub struct ImporterApi { + inner: Arc>, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum IterationError { + Imap(ImapIterationError), + File(FileIterationError), +} + +impl Importer { + pub async fn continue_import(&mut self) -> Result { + let mut failed_import_count = 0_u32; + let mut success_import_count = 0_u32; + + 'walk_through_source: loop { + let next_importable_mail = match &mut self.import_source { + ImportSource::RemoteImap { imap_import_client } => imap_import_client + .fetch_next_mail() + .await + .map_err(IterationError::Imap), + + ImportSource::LocalFile { fs_email_client } => fs_email_client + .get_next_importable_mail() + .map_err(IterationError::File), + }; + + let import_res = match next_importable_mail { + Ok(next_importable_mail) => self.import_one_mail(next_importable_mail).await, + + // source says, all the iteration have ended, + Err(IterationError::File(FileIterationError::SourceEnd)) + | Err(IterationError::Imap(ImapIterationError::SourceEnd)) => { + break 'walk_through_source; + } + + Err(e) => { + panic!("Cannot get next email from source: {e:?}") + } + }; + + match import_res { + // this import has been success, + Ok(_imported_mail_response) => success_import_count += 1, + + Err(_) => { + // todo: save the ImportableMail to some fail list, + // since, in this iteration the source will not give this mail again, + failed_import_count += 1; + } + } + } + + if failed_import_count > 0 { + // some mail failed to import: + self.status = ImportStatus { + state: ImportState::Postponed, + imported_mails: success_import_count, + }; + } else { + // nothing failed, + self.status = ImportStatus { + state: ImportState::Finished, + imported_mails: success_import_count, + }; + }; + + Ok(self.status.clone()) + } + + /// once we get the ImportableMail from either of source, + /// continue to the uploading counterpart + async fn import_one_mail( + &self, + importable_mail: ImportableMail, + ) -> Result { + let new_aes_256_key = GenericAesKey::from_bytes( + self.randomizer_facade + .generate_random_array::<{ tutasdk::crypto::aes::AES_256_KEY_SIZE }>() + .as_slice(), + ) + .unwrap(); + let mail_group_key = self + .logged_in_sdk + .get_current_sym_group_key(&self.target_owner_group) + .await + .map_err(|e| ())?; + let owner_enc_session_key = + mail_group_key.encrypt_key(&new_aes_256_key, Iv::generate(&self.randomizer_facade)); + + let import_mail_data = ImportMailData::from(importable_mail); + let import_mail_post_in = ImportMailPostIn { + ownerEncSessionKey: owner_enc_session_key.object, + ownerGroup: self.target_owner_group.clone(), + ownerKeyVersion: owner_enc_session_key.version, + imports: vec![import_mail_data], + targetMailFolder: self.target_mail_folder.clone(), + _format: 0, + _errors: None, + _finalIvs: Default::default(), + }; + + let service_params = ExtraServiceParams { + session_key: Some(new_aes_256_key), + ..Default::default() + }; + + let import_mail_post_out = self + .logged_in_sdk + .get_service_executor() + .post::(import_mail_post_in, service_params) + .await + .expect("Cannot execute ImportMailService"); + + Ok(import_mail_post_out) + } +} + +impl ImporterApi { + pub fn new( + logged_in_sdk: Arc, + target_owner_group: GeneratedId, + target_mail_folder: IdTupleGenerated, + import_source: ImportSource, + ) -> Self { + let import_inner = Importer { + logged_in_sdk, + target_owner_group, + target_mail_folder, + import_source, + status: ImportStatus::default(), + randomizer_facade: RandomizerFacade::from_core(rand::rngs::OsRng), + }; + Self { + inner: Arc::new(NapiTokioMutex::new(import_inner)), + } + } + + pub async fn continue_import_inner(&mut self) -> Result { + self.inner.lock().await.continue_import().await + } + + pub async fn delete_import_inner(&mut self) -> Result { + todo!() + } + + pub async fn pause_import_inner(&mut self) -> Result { + todo!() + } + + pub async fn create_file_importer_inner( + tuta_credentials: TutaCredentials, + target_owner_group: String, + target_mail_folder: (String, String), + source_paths: Vec, + ) -> napi::Result { + let logged_in_sdk_future = Self::create_sdk(tuta_credentials); + + let fs_email_client = FileImport::new(source_paths) + .map_err(|e| NapiError::from_reason("Cannot create file import"))?; + let import_source = ImportSource::LocalFile { fs_email_client }; + let logged_in_sdk = logged_in_sdk_future + .await + .map_err(|e| NapiError::from_reason("Cannot create logged in sdk"))?; + + Ok(ImporterApi::new( + logged_in_sdk, + GeneratedId(target_owner_group), + IdTupleGenerated::new( + GeneratedId(target_mail_folder.0), + GeneratedId(target_mail_folder.1), + ), + import_source, + )) + } + + async fn create_sdk(tuta_credentials: TutaCredentials) -> Result, String> { + let rest_client = Arc::new( + NativeRestClient::try_new() + .map_err(|e| format!("Cannot build native rest client: {e}"))?, + ); + + let logged_in_sdk = { + let sdk = Sdk::new(tuta_credentials.api_url.clone(), rest_client); + + let sdk_credentials: Credentials = tuta_credentials + .clone() + .try_into() + .map_err(|_| "Cannot convert to valid credentials".to_string())?; + sdk.login(sdk_credentials) + .await + .map_err(|e| format!("Cannot login to sdk. Error: {:?}", e))? + }; + + Ok(logged_in_sdk) + } +} + +// Wrapper for napi +#[cfg(feature = "javascript")] +#[napi_derive::napi] +impl ImporterApi { + // once Self::continue_import return custom error, + // do the error conversion here, or in trait + fn error_conversion(err: E) -> napi::Error { + todo!() + } + + #[napi] + pub async unsafe fn continue_import(&mut self) -> napi::Result { + self.continue_import_inner() + .await + .map_err(Self::error_conversion) + } + + #[napi] + pub async unsafe fn delete_import(&mut self) -> napi::Result { + self.delete_import_inner() + .await + .map_err(Self::error_conversion) + } + + #[napi] + pub async unsafe fn pause_import(&mut self) -> napi::Result { + self.pause_import_inner() + .await + .map_err(Self::error_conversion) + } + + #[napi] + pub async fn create_file_importer( + tuta_credentials: TutaCredentials, + target_owner_group: String, + target_mail_folder: (String, String), + source_paths: Vec, + ) -> napi::Result { + Self::create_file_importer_inner( + tuta_credentials, + target_owner_group, + target_mail_folder, + source_paths, + ) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::importer::imap_reader::{ImapCredentials, LoginMechanism}; + use crate::tuta_imap::testing::GreenMailTestServer; + use mail_builder::MessageBuilder; + use tutasdk::entities::generated::tutanota::MailFolder; + use tutasdk::folder_system::MailSetKind; + use tutasdk::net::native_rest_client::NativeRestClient; + use tutasdk::Sdk; + + fn sample_email(subject: String) -> String { + let email = MessageBuilder::new() + .from(("Matthias", "map@example.org")) + .to(("Johannes", "jhm@example.org")) + .subject(subject) + .text_body("Hello tutao! this is the first step to have email import.Want to see html πŸ˜€?

red

") + .write_to_string() + .unwrap(); + email + } + + async fn get_test_import_folder_id( + logged_in_sdk: &Arc, + kind: MailSetKind, + ) -> MailFolder { + let mail_facade = logged_in_sdk.mail_facade(); + let mailbox = mail_facade.load_user_mailbox().await.unwrap(); + let folders = mail_facade + .load_folders_for_mailbox(&mailbox) + .await + .unwrap(); + folders + .system_folder_by_type(kind) + .expect("inbox should exist") + .clone() + } + + async fn init_imap_importer() -> (Importer, GreenMailTestServer) { + let importer_mail_address = "map-free@tutanota.de".to_string(); + let logged_in_sdk = Sdk::new( + "http://localhost:9000".to_string(), + Arc::new(NativeRestClient::try_new().unwrap()), + ) + .create_session(importer_mail_address.as_str(), "map") + .await + .unwrap(); + let greenmail = GreenMailTestServer::new(); + let imap_import_config = ImapImportConfig { + root_import_mail_folder_name: "/".to_string(), + credentials: ImapCredentials { + host: "127.0.0.1".to_string(), + port: greenmail.imaps_port.try_into().unwrap(), + login_mechanism: LoginMechanism::Plain { + username: "sug@example.org".to_string(), + password: "sug".to_string(), + }, + }, + }; + + let import_source = ImportSource::RemoteImap { + imap_import_client: ImapImport::new(imap_import_config), + }; + let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng); + let target_mail_folder = get_test_import_folder_id(&logged_in_sdk, MailSetKind::Archive) + .await + ._id + .unwrap(); + + let target_owner_group = logged_in_sdk + .mail_facade() + .get_group_id_for_mail_address(importer_mail_address.as_str()) + .await + .unwrap(); + + let importer = Importer { + target_owner_group, + target_mail_folder, + logged_in_sdk, + import_source, + randomizer_facade, + status: ImportStatus::default(), + }; + + (importer, greenmail) + } + + pub async fn init_file_importer(source_paths: Vec) -> Importer { + let logged_in_sdk = Sdk::new( + "http://localhost:9000".to_string(), + Arc::new(NativeRestClient::try_new().unwrap()), + ) + .create_session("map-free@tutanota.de", "map") + .await + .unwrap(); + + let import_source = ImportSource::LocalFile { + fs_email_client: FileImport::new(source_paths).unwrap(), + }; + let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng); + let target_mail_folder = get_test_import_folder_id(&logged_in_sdk, MailSetKind::Archive) + .await + ._id + .unwrap(); + + let target_owner_group = logged_in_sdk + .mail_facade() + .get_group_id_for_mail_address("map-free@tutanota.de") + .await + .unwrap(); + + Importer { + status: ImportStatus::default(), + target_owner_group, + target_mail_folder, + logged_in_sdk, + import_source, + randomizer_facade, + } + } + + #[tokio::test] + pub async fn import_multiple_from_imap_default_folder() { + let (mut importer, greenmail) = init_imap_importer().await; + + let email_first = sample_email("Hello from imap πŸ˜€! -- Бписок.doc".to_string()); + let email_second = sample_email("Second time: hello".to_string()); + greenmail.store_mail("sug@example.org", email_first.as_str()); + greenmail.store_mail("sug@example.org", email_second.as_str()); + + let import_res = importer.continue_import().await.map_err(|_| ()); + assert_eq!( + Ok(ImportStatus { + state: ImportState::Finished, + imported_mails: 2, + }), + import_res + ); + } + + #[tokio::test] + pub async fn import_single_from_imap_default_folder() { + let (mut importer, greenmail) = init_imap_importer().await; + + let email = sample_email("Single email".to_string()); + greenmail.store_mail("sug@example.org", email.as_str()); + + let import_res = importer.continue_import().await.map_err(|_| ()); + assert_eq!( + Ok(ImportStatus { + state: ImportState::Finished, + imported_mails: 1, + }), + import_res + ); + } + + #[tokio::test] + async fn can_import_single_eml_file() { + let mut importer = init_file_importer(vec!["./test/sample.eml".to_string()]).await; + + let import_res = importer.continue_import().await.map_err(|_| ()); + assert_eq!( + Ok(ImportStatus { + state: ImportState::Finished, + imported_mails: 1, + }), + import_res + ); + } +} diff --git a/packages/node-mimimi/src/importer/extend_mail_parser.rs b/packages/node-mimimi/src/importer/extend_mail_parser.rs new file mode 100644 index 00000000000..bf4d9d988a7 --- /dev/null +++ b/packages/node-mimimi/src/importer/extend_mail_parser.rs @@ -0,0 +1,174 @@ +///! Extends the functionality of mail_parser crate +use crate::importer::importable_mail::ReplyType; +use mail_parser::HeaderName; +use std::borrow::Cow; + +pub(super) fn get_reply_type_from_headers<'a>(headers: &'a [mail_parser::Header<'a>]) -> ReplyType { + let mut is_forward = false; + let mut is_reply = false; + + for header in headers { + if header.name == HeaderName::ResentFrom { + if header.value().make_string().trim().is_empty() { + is_forward = true; + } + } else if header.name == HeaderName::References { + if header.value().make_string().trim().is_empty() { + is_reply = true; + } + } + if is_reply && is_forward { + break; + } + } + + if is_forward && is_reply { + ReplyType::ReplyForward + } else if is_forward { + ReplyType::Forward + } else if is_reply { + ReplyType::Reply + } else { + ReplyType::default() + } +} + +/// Supports converting types of external library to string that can be imported +pub(super) trait MakeString { + fn make_string(&self) -> Cow; +} + +impl<'a> MakeString for [mail_parser::Header<'a>] { + fn make_string(&self) -> Cow { + self.into_iter() + .map(MakeString::make_string) + .collect::>() + .join("\n") + .into() + } +} +impl<'a> MakeString for mail_parser::Header<'a> { + fn make_string(&self) -> Cow { + let Self { + name, + value, + offset_field: _, + offset_start: _, + offset_end: _, + } = self; + let header_line = name.to_string() + ": " + value.make_string().as_ref(); + Cow::Owned(header_line) + } +} + +impl<'a> MakeString for mail_parser::HeaderValue<'a> { + fn make_string(&self) -> Cow { + match self { + mail_parser::HeaderValue::ContentType(content_t) => MakeString::make_string(content_t), + mail_parser::HeaderValue::Received(recv) => MakeString::make_string(recv.as_ref()), + mail_parser::HeaderValue::Address(address) => MakeString::make_string(address), + mail_parser::HeaderValue::DateTime(date_time) => MakeString::make_string(date_time), + mail_parser::HeaderValue::TextList(text_list) => Cow::Owned(text_list.join(",")), + mail_parser::HeaderValue::Text(text) => Cow::Borrowed(text.as_ref()), + mail_parser::HeaderValue::Empty => Cow::Borrowed(""), + } + } +} + +impl<'a> MakeString for mail_parser::DateTime { + fn make_string(&self) -> Cow { + const DAY_OF_WEEK: [&'static str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + const MONTH_OF_YEAR: [&'static str; 12] = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec", + ]; + + let weekday = DAY_OF_WEEK[self.day_of_week() as usize - 1]; + let Self { + year, + month, + day, + hour: hh, + minute: mm, + second: ss, + tz_before_gmt: _, + tz_hour: tz_hh, + tz_minute: tz_mm, + } = self; + let month = MONTH_OF_YEAR[*month as usize - 1]; + + Cow::Owned(format!( + "{weekday}, {day} {month} {year} {hh:02}:{mm:02}:{ss:02} +{tz_hh:02}{tz_mm:02}" + )) + } +} + +impl<'a> MakeString for mail_parser::Received<'a> { + fn make_string(&self) -> Cow { + Cow::Borrowed("todo!()") + } +} + +impl<'a> MakeString for mail_parser::Address<'a> { + fn make_string(&self) -> Cow { + match self { + mail_parser::Address::List(address_list) => address_list + .into_iter() + .map(|addr| make_mail_address(addr.name(), addr.address())) + .collect::>() + .join(",") + .into(), + mail_parser::Address::Group(group_list) => { + todo!() + }, + } + } +} + +impl<'a> MakeString for mail_parser::ContentType<'a> { + fn make_string(&self) -> Cow { + let attribute_str = self.attributes.as_ref().map(|attributes| { + attributes + .into_iter() + .map(|(name, value)| { + if value.is_empty() { + name.to_string() + } else { + name.to_string() + "=\"" + value.as_ref() + "\"" + } + }) + .collect::>() + .join(";") + }); + + let mut content_type = self.c_type.as_ref().to_string(); + if let Some(subtype) = self.c_subtype.as_ref() { + content_type.push('/'); + content_type.push_str(subtype); + } + if let Some(attribute_str) = attribute_str { + if !content_type.is_empty() { + content_type.push_str("; "); + } + content_type.push_str(attribute_str.as_str()); + } + + Cow::Owned(content_type) + } +} + +pub fn make_mail_address(name: Option<&str>, address: Option<&str>) -> String { + let name = name.unwrap_or_default(); + let mut res = if name.is_empty() || name.starts_with("\"") { + name.to_string() + } else { + // always wrap name in quotes. tutadb: #417 + String::from('"') + name + "\"" + }; + + if let Some(address) = address { + res.push('<'); + res.push_str(address); + res.push('>'); + } + res +} diff --git a/packages/node-mimimi/src/importer/file_reader.rs b/packages/node-mimimi/src/importer/file_reader.rs new file mode 100644 index 00000000000..644e2c23d0f --- /dev/null +++ b/packages/node-mimimi/src/importer/file_reader.rs @@ -0,0 +1 @@ +pub mod import_client; diff --git a/packages/node-mimimi/src/importer/file_reader/import_client.rs b/packages/node-mimimi/src/importer/file_reader/import_client.rs new file mode 100644 index 00000000000..cff2f179e1c --- /dev/null +++ b/packages/node-mimimi/src/importer/file_reader/import_client.rs @@ -0,0 +1,112 @@ +use crate::importer::importable_mail::ImportableMail; +use mail_parser::mailbox::mbox::MessageIterator; +use mail_parser::MessageParser; +use std::io::{BufReader, Read}; + +pub struct FileImport { + mbox_sources: Vec>>, + eml_sources: Vec>, + + message_parser: MessageParser, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum FileIterationError { + /// We have read all contents + SourceEnd, + + /// Cannot parse next item from mbox + MboxParseError, + + /// File read error + FileReadError, + + /// Cannot parse raw content to message + MessageParseError(String), + + /// Cannot convert the read message to ImportableMail + NoImportableMail, +} + +impl FileImport { + fn next_eml_contents(&mut self) -> Result, FileIterationError> { + let mut eml_file_source = self + .eml_sources + .pop() + .ok_or(FileIterationError::SourceEnd)?; + let mut file_content = Vec::new(); + eml_file_source + .read_to_end(&mut file_content) + .map_err(|_| FileIterationError::FileReadError)?; + + Ok(file_content) + } + + fn next_mbox_item_contents(&mut self) -> Result, FileIterationError> { + let mbox_source = self + .mbox_sources + .last_mut() + .ok_or(FileIterationError::SourceEnd)?; + match mbox_source.next() { + Some(Ok(mbox_item)) => Ok(mbox_item.unwrap_contents()), + + Some(Err(e)) => Err(FileIterationError::MboxParseError), + + None => { + self.mbox_sources.pop(); + self.next_mbox_item_contents() + }, + } + } +} + +impl FileImport { + pub fn new(source_paths: Vec) -> Result { + let mut mbox_sources = Vec::new(); + let mut eml_sources = Vec::new(); + + for source_path in source_paths { + let file_buf_reader = std::fs::File::open(source_path.as_str()) + .map(BufReader::new) + .map_err(|_| FileIterationError::FileReadError)?; + + let is_mbox_file = source_path.ends_with(".mbox"); + let is_eml_file = source_path.ends_with(".eml"); + + if is_eml_file { + eml_sources.push(file_buf_reader); + } else if is_mbox_file { + mbox_sources.push(MessageIterator::new(file_buf_reader)); + } else { + Err(FileIterationError::FileReadError)? + } + } + + let message_parser = MessageParser::default(); + Ok(Self { + mbox_sources, + eml_sources, + message_parser, + }) + } + + /// Get next importable mail form sources, + /// will try to exhaust eml_sources first + pub fn get_next_importable_mail(&mut self) -> Result { + // Get next item from eml source first. once all eml sources are exhausted, + // move to next mbox sources, + let mut email_contents_res = self.next_eml_contents(); + if email_contents_res == Err(FileIterationError::SourceEnd) { + email_contents_res = self.next_mbox_item_contents(); + } + let email_contents = email_contents_res?; + + let parsed_message = self + .message_parser + .parse(email_contents.as_slice()) + .ok_or_else(|| FileIterationError::MessageParseError("todo1".to_string()))?; + let importable_mail = ImportableMail::try_from(parsed_message) + .map_err(|e| FileIterationError::MessageParseError(format!("{e:?}")))?; + Ok(importable_mail) + } +} diff --git a/packages/node-mimimi/src/importer/imap_reader.rs b/packages/node-mimimi/src/importer/imap_reader.rs new file mode 100644 index 00000000000..5f196be5720 --- /dev/null +++ b/packages/node-mimimi/src/importer/imap_reader.rs @@ -0,0 +1,24 @@ +pub mod import_client; + +#[derive(Clone, PartialEq)] +/// passed in from js before being validated and used for logging into the imap_reader server +pub struct ImapCredentials { + /// hostname of the imap_reader server to import mail from + pub host: String, + /// imap_reader port of the imap_reader server to import mail from + pub port: u16, + /// Login method + pub login_mechanism: LoginMechanism, +} + +#[derive(Clone, PartialEq)] +pub enum LoginMechanism { + Plain { username: String, password: String }, + OAuth { access_token: String }, +} + +#[derive(Clone, PartialEq)] +pub struct ImapImportConfig { + pub root_import_mail_folder_name: String, + pub credentials: ImapCredentials, +} diff --git a/packages/node-mimimi/src/importer/imap_reader/import_client.rs b/packages/node-mimimi/src/importer/imap_reader/import_client.rs new file mode 100644 index 00000000000..e8237d1690c --- /dev/null +++ b/packages/node-mimimi/src/importer/imap_reader/import_client.rs @@ -0,0 +1,158 @@ +use crate::importer::imap_reader::{ImapImportConfig, LoginMechanism}; +use crate::importer::importable_mail::{ImportableMail, MailParseError}; +use crate::tuta_imap::client::TutaImapClient; +use imap_codec::imap_types::mailbox::Mailbox; +use imap_codec::imap_types::response::StatusKind; +use std::num::NonZeroU32; + +pub struct ImapImport { + import_config: ImapImportConfig, + imap_client: TutaImapClient, + + import_state: ImapImportState, +} + +pub struct ImapImportState { + done_fetched_mailbox: Vec>, + current_mailbox: Option>, + next_target_mailbox: Vec>, + + /// List of mail id that have been fetched from current_mailbox, + fetched_from_current_mailbox: Vec, +} + +impl ImapImportState { + /// add currently selected mailbox to done, + /// and pop one from next target mailbox + pub fn finish_current_mailbox(&mut self) { + let current_mailbox = self.current_mailbox.as_mut().expect("No current mailbox"); + self.done_fetched_mailbox.push(current_mailbox.clone()); + self.current_mailbox = self.next_target_mailbox.pop(); + self.fetched_from_current_mailbox.clear(); + } + + /// this id of current mailbox was fetched + pub fn fetched_from_current_mailbox(&mut self, id: NonZeroU32) { + assert!(self.current_mailbox.is_some(), "No current mailbox"); + self.fetched_from_current_mailbox.push(id); + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum ImapIterationError { + /// All mail form remote server have been visited at least once, + SourceEnd, + + /// when executing a command, received a non-ok status, + NonOkCommandStatus, + + /// Can not convert ImapMail to ConvertableMail + MailParseError(MailParseError), + + /// Can not login to imap server + NoLogin, +} + +impl ImapImport { + pub fn new(import_config: ImapImportConfig) -> Self { + let imap_client = TutaImapClient::new( + import_config.credentials.host.as_str(), + import_config.credentials.port, + ); + + Self { + imap_client, + import_config, + + import_state: ImapImportState { + done_fetched_mailbox: vec![], + next_target_mailbox: vec![Mailbox::Inbox], + current_mailbox: None, + fetched_from_current_mailbox: vec![], + }, + } + } + + /// High level abstraction to read next mail from imap, + /// will switch to next mailbox, if everything from current mailbox is fetched, + pub async fn fetch_next_mail(&mut self) -> Result { + self.ensure_logged_in()?; + + while self.imap_client.latest_search_results.is_empty() { + if self.import_state.current_mailbox.is_some() { + self.import_state.finish_current_mailbox(); + } + + // select next mailbox + // and search for all available mails + self.ensure_mailbox_selected()?; + + self.imap_client + .search_all_uid() + .eq(&StatusKind::Ok) + .then_some(()) + .ok_or(ImapIterationError::NonOkCommandStatus)?; + } + + // search for the last ( oldest ? ) mail + let next_mail_id = self.imap_client.latest_search_results.pop().unwrap(); + self.imap_client + .fetch_mail_by_uid(next_mail_id) + .eq(&StatusKind::Ok) + .then_some(()) + .ok_or(ImapIterationError::NonOkCommandStatus)?; + let next_mail_imap = self.imap_client.latest_mails.remove(&next_mail_id).unwrap(); + + // mark this id have been fetched + self.import_state.fetched_from_current_mailbox(next_mail_id); + + ImportableMail::try_from(next_mail_imap).map_err(ImapIterationError::MailParseError) + } + + fn ensure_mailbox_selected(&mut self) -> Result<(), ImapIterationError> { + if self.import_state.current_mailbox.is_none() { + let next_mailbox_to_select = self + .import_state + .next_target_mailbox + .pop() + .ok_or(ImapIterationError::SourceEnd)?; + self.import_state.current_mailbox = Some(next_mailbox_to_select); + self.import_state.fetched_from_current_mailbox = vec![]; + } + + // if something from current mailbox is selected, it means we are already in selected state + if !self.import_state.fetched_from_current_mailbox.is_empty() { + return Ok(()); + } + + let target_mailbox = self + .import_state + .current_mailbox + .as_ref() + .ok_or(ImapIterationError::SourceEnd)?; + self.imap_client + .select_mailbox(target_mailbox.clone()) + .eq(&StatusKind::Ok) + .then_some(()) + .ok_or(ImapIterationError::NonOkCommandStatus) + } + + fn ensure_logged_in(&mut self) -> Result<(), ImapIterationError> { + if self.imap_client.is_logged_in() { + return Ok(()); + } + + match &self.import_config.credentials.login_mechanism { + LoginMechanism::Plain { username, password } => self + .imap_client + .plain_login(username.as_str(), password.as_str()) + .eq(&StatusKind::Ok) + .then_some(()) + .ok_or(ImapIterationError::NoLogin), + + LoginMechanism::OAuth { access_token: _ } => { + unimplemented!() + }, + } + } +} diff --git a/packages/node-mimimi/src/importer/importable_mail.rs b/packages/node-mimimi/src/importer/importable_mail.rs new file mode 100644 index 00000000000..1808017c9f9 --- /dev/null +++ b/packages/node-mimimi/src/importer/importable_mail.rs @@ -0,0 +1,881 @@ +use crate::importer::extend_mail_parser::{get_reply_type_from_headers, MakeString}; +use crate::importer::plain_text_to_html_converter; +use crate::tuta_imap::client::types::ImapMail; +use mail_parser::{Address, GetHeader, HeaderName, MessageParser, PartType}; +use std::borrow::Cow; +use std::collections::HashMap; +use std::hash::Hash; +use std::time::SystemTime; +use tutasdk::date::DateTime; +use tutasdk::entities::generated::tutanota::{ + EncryptedMailAddress, ImportMailData, ImportMailDataMailReference, MailAddress, Recipients, +}; +use tutasdk::CustomId; + +// todo: this is used for DataTransferType, so id really dont have to be unique, +// but have to be valid length +const FIXED_CUSTOM_ID: &str = "____"; + +#[derive(Default)] +#[cfg_attr(test, derive(PartialEq, Debug))] +enum MailState { + #[default] + Received = 2, + Sent = 1, + Draft = 0, +} + +#[repr(i64)] +#[derive(Default)] +#[cfg_attr(test, derive(PartialEq, Debug))] +enum ICalType { + #[default] + Nothing = 0, + ICalPublish = 1, + ICalRequest = 2, + ICalAdd = 3, + ICalCancel = 4, + ICalRefresh = 5, + ICalCounter = 6, + ICalDeclineCounter = 7, +} + +#[derive(Default)] +#[cfg_attr(test, derive(PartialEq, Debug))] +pub(super) enum ReplyType { + #[default] + Nothing = 0, + Reply = 1, + Forward = 2, + ReplyForward = 3, +} + +#[cfg_attr(test, derive(PartialEq, Debug))] +enum ImportableMailAttachment { + Attachment { + filename: Option, + content_type: String, + content_id: String, + content: Vec, + is_inline: bool, + }, + AttachedMessage { + message: ImportableMail, + }, +} + +#[cfg_attr(test, derive(PartialEq, Debug))] +enum BodyText { + Html(String), + Plain(String), +} + +#[derive(Default, PartialEq)] +#[cfg_attr(test, derive(Debug))] +pub struct MailContact { + pub mail_address: String, + pub name: String, +} + +impl<'a> From> for MailContact { + fn from(value: mail_parser::Addr) -> Self { + Self { + name: value.name.unwrap_or_default().to_string(), + mail_address: value.address.unwrap_or_default().to_string(), + } + } +} + +impl From for MailAddress { + fn from(value: MailContact) -> Self { + Self { + _id: None, + address: value.mail_address, + name: value.name, + contact: None, + _finalIvs: Default::default(), + } + } +} + +/// Input data for mail import service +#[cfg_attr(test, derive(PartialEq, Debug))] +pub struct ImportableMail { + pub headers_string: String, + pub subject: String, + pub html_body_text: String, + pub attachments: Vec, + + pub date: Option, + + pub different_envelope_sender: Option, + pub from_addresses: Vec, + pub to_addresses: Vec, + pub cc_addresses: Vec, + pub bcc_addresses: Vec, + pub reply_to_addresses: Vec, + + pub ical_type: ICalType, + pub reply_type: ReplyType, + + pub mail_state: MailState, + pub is_phishing: bool, // https://turbo.fish/::%3Cphising%3E + pub unread: bool, + + pub message_id: Option, + pub in_reply_to: Option, + pub references: Vec, +} + +impl ImportableMail { + /// Utility function to convert mail_parser::Address + /// to a list of tutasdk::MailAddress + /// in such a way that every address must have mail-address and optional name + /// + /// returns None, if any of the address have empty mail-address + /// + /// set the _id: of all mail address to random 4-byte long customId, + /// this will only be valid in dataTransferType context + fn map_to_tuta_mail_address(mail_parser_addresses: Cow
) -> Vec { + let address_list = match mail_parser_addresses.as_ref() { + Address::List(address_list) => Cow::Borrowed(address_list), + Address::Group(group_senders) => { + let group_addresses = group_senders + .iter() + .map(|group| group.addresses.as_slice()) + .collect::>() + .concat(); + + Cow::Owned(group_addresses) + }, + }; + + address_list + .as_ref() + .into_iter() + .map(|address| MailContact { + mail_address: address.address().unwrap_or_default().to_string(), + name: address.name().unwrap_or_default().to_string(), + }) + .collect() + } + + // from the parsed message + // return : + // .0 a single string that ca be display as email in html format + // .1 list of attachment found + fn process_all_parts( + parsed_message: &mail_parser::Message, + ) -> Result<(String, Vec), MailParseError> { + let mut email_body_as_html = String::new(); + let mut attachments = Vec::with_capacity(parsed_message.attachments.len()); + + for part in &parsed_message.parts { + match &part.body { + PartType::Text(text) => { + let plain_text_as_html = + plain_text_to_html_converter::plain_text_to_html(text.to_string()); + email_body_as_html.push_str(plain_text_as_html.as_ref()) + }, + PartType::Html(html_text) => { + email_body_as_html.push_str(html_text); + }, + PartType::Message(attached_message) => { + let importable_mail = ImportableMail::try_from(attached_message.to_owned())?; + let this_attachment = ImportableMailAttachment::AttachedMessage { + message: importable_mail, + }; + attachments.push(this_attachment); + }, + + PartType::Binary(binary_content) | PartType::InlineBinary(binary_content) => { + let is_inline = if matches!(part.body, PartType::InlineBinary(_)) { + true + } else if matches!(part.body, PartType::Binary(_)) { + false + } else { + unreachable!(); + }; + + let content_type = part.headers.header_value(&HeaderName::ContentType).map( + |content_type_header| { + content_type_header + .as_content_type() + .expect("Content-Type header should be of type content type") + }, + ); + + let filename = content_type + .map(mail_parser::ContentType::attributes) + .unwrap_or_default() + .unwrap_or_default() + .iter() + .filter(|(attribute_name, _)| attribute_name == "filename") + .map(|(_, file_name)| file_name.to_string()) + // first attribute called 'filename' + .next(); + + let content_type = content_type + .map(MakeString::make_string) + .unwrap_or_default() + .to_string(); + + let content_id = part + .headers + .header_value(&HeaderName::ContentId) + .map(|content_type_header| { + content_type_header + .as_text() + .expect("Content-Id header should be of type text") + }) + .unwrap_or("binary") + .to_string(); + let content = binary_content.to_vec(); + let this_attachment = ImportableMailAttachment::Attachment { + filename, + content_type, + content_id, + is_inline, + content, + }; + attachments.push(this_attachment); + }, + + PartType::Multipart(multi_part_msg) => { + panic!(""); + }, + } + } + + Ok((email_body_as_html, attachments)) + } +} + +impl From for ImportMailData { + fn from(importable_mail: ImportableMail) -> Self { + let ImportableMail { + headers_string: headers, + subject, + html_body_text, + different_envelope_sender, + from_addresses, + cc_addresses, + bcc_addresses, + to_addresses, + date, + reply_to_addresses, + ical_type, + reply_type, + mail_state, + is_phishing, + unread, + message_id, + in_reply_to, + references, + attachments, + } = importable_mail; + + let date = date.unwrap_or_else(|| DateTime::from_system_time(SystemTime::now())); + + let reply_tos = reply_to_addresses + .into_iter() + .map(|reply_to| EncryptedMailAddress { + _id: Some(CustomId::from_custom_string(FIXED_CUSTOM_ID)), + _finalIvs: Default::default(), + name: reply_to.name, + address: reply_to.mail_address, + }) + .collect(); + + let bcc_addresses = bcc_addresses.into_iter().map(Into::into).collect(); + let cc_addresses = cc_addresses.into_iter().map(Into::into).collect(); + let to_addresses = to_addresses.into_iter().map(Into::into).collect(); + let from_addresses: Vec = from_addresses.into_iter().map(Into::into).collect(); + + let references = references + .into_iter() + .map(|reference| ImportMailDataMailReference { + _id: Some(CustomId::from_custom_string(FIXED_CUSTOM_ID)), + reference, + }) + .collect(); + + ImportMailData { + _id: Some(CustomId::from_custom_string(FIXED_CUSTOM_ID)), + _finalIvs: HashMap::new(), + compressedHeaders: headers, + subject, + compressedBodyText: html_body_text, + differentEnvelopeSender: different_envelope_sender, + sender: from_addresses + .first() + .cloned() + .unwrap_or(MailContact::default().into()), + recipients: Recipients { + _id: Some(CustomId::from_custom_string(FIXED_CUSTOM_ID)), + bccRecipients: bcc_addresses, + ccRecipients: cc_addresses, + toRecipients: to_addresses, + }, + replyTos: reply_tos, + unread, + confidential: false, + method: ical_type as i64, + phishingStatus: if is_phishing { 1 } else { 0 }, + replyType: reply_type as i64, + date, + state: mail_state as i64, + messageId: message_id, + inReplyTo: in_reply_to, + references, + importedAttachments: vec![], + } + } +} + +impl TryFrom for ImportableMail { + type Error = MailParseError; + fn try_from(imap_mail: ImapMail) -> Result { + let ImapMail { rfc822_full } = imap_mail; + + // parse the full mime message + let imap_mail = MessageParser::new() + .parse(rfc822_full.as_slice()) + .ok_or(MailParseError::InvalidMimeMessage)?; + + let mut importable_mail = Self::try_from(imap_mail)?; + + // example: + // add more details from imap if given, + importable_mail.is_phishing = false; + importable_mail.unread = true; + + Ok(importable_mail) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum MailParseError { + InconsistentParts(&'static str), + NoSentDate, + NoRecipient, + NoFrom, + InvalidDate, + InvalidHtmlBody, + InvalidTextBody, + InvalidMimeMessage, + EmptyMailAddress, + Unknown(String), +} + +/// allow to convert from parsed message +impl<'x> TryFrom> for ImportableMail { + type Error = MailParseError; + + fn try_from(parsed_message: mail_parser::Message) -> Result { + let subject = parsed_message.subject().unwrap_or_default().to_string(); + + let (html_body_text, attachments) = ImportableMail::process_all_parts(&parsed_message)?; + + let date = parsed_message + .date() + .as_ref() + .map(|date_time| DateTime::from_millis(date_time.to_timestamp() as u64 * 1000)); + + let from_addresses = ImportableMail::map_to_tuta_mail_address( + parsed_message.from().map(Cow::Borrowed).unwrap_or_else(|| { + parsed_message + .sender() + .map(Cow::Borrowed) + .unwrap_or_else(|| Cow::Owned(mail_parser::Address::List(vec![]))) + }), + ) + .into_iter() + .map(|mut address| { + // we currently use the name as address if no address was defined on server side + if address.mail_address.is_empty() { + address.mail_address = address.name; + address.name = String::new(); + } + address + }) + .collect::>(); + + let different_envelope_sender = parsed_message + .sender() + .map(|sender| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(sender))) + // sender is allowed to be empty + .unwrap_or_default() + // there should only be one different envelope sender + .pop() + .map(|mut address| { + // we currently use the name as address if no address was defined on server side + if address.mail_address.is_empty() { + address.mail_address = address.name; + address.name = String::new(); + } + address + }) + // different envelope sender should not contain address listed in from_addresses; + .filter(|diff_sender| { + from_addresses + .iter() + .filter(|from| from.mail_address != diff_sender.mail_address) + .next() + .is_some() + }) + .map(|mail_address| mail_address.mail_address); + + let to_addresses = parsed_message + .to() + .map(|to| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(to))) + .unwrap_or_default() + .into_iter() + .filter(|address| !address.mail_address.trim().is_empty()) + .collect(); + + let cc_addresses = parsed_message + .cc() + .map(|cc| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(cc))) + .unwrap_or_default() + .into_iter() + .filter(|address| !address.mail_address.trim().is_empty()) + .collect(); + + let bcc_addresses = parsed_message + .bcc() + .map(|bcc| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(bcc))) + .unwrap_or_default() + .into_iter() + .filter(|address| !address.mail_address.trim().is_empty()) + .collect(); + + let reply_to_addresses = parsed_message + .reply_to() + .map(|reply_to| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(reply_to))) + .unwrap_or_default() + .into_iter() + .filter(|address| !address.mail_address.trim().is_empty()) + .collect(); + + let headers_string = parsed_message + .headers() + .into_iter() + .map(MakeString::make_string) + .collect::>() + .join("\n"); + + let reply_type = get_reply_type_from_headers(parsed_message.headers()); + let message_id = parsed_message.message_id().map(String::from); + let in_reply_to = parsed_message.in_reply_to().as_text().map(String::from); + let references = parsed_message + .references() + .as_text() + .map(String::from) + .into_iter() + .collect(); + + Ok(Self { + headers_string, + html_body_text, + subject, + different_envelope_sender, + from_addresses, + to_addresses, + cc_addresses, + bcc_addresses, + reply_to_addresses, + date, + reply_type, + message_id, + in_reply_to, + references, + attachments, + + ical_type: Default::default(), + unread: false, + mail_state: Default::default(), + is_phishing: false, + }) + } +} + +#[cfg(test)] +mod tests { + use crate::importer::importable_mail::{ImportableMail, MailContact}; + use mail_parser::{MessageParser, MessagePartId}; + use serde::Deserialize; + use std::borrow::Cow; + + impl From for MailContact { + fn from(value: TestMailAddress) -> Self { + let TestMailAddress { + name, mail_address, .. + } = value; + Self { mail_address, name } + } + } + + impl From for ImportableMail { + fn from(mut expected_message: ExpectedMessage) -> Self { + let mut html_body_ids: Vec = vec![]; + let mut plain_body_ids: Vec = vec![]; + let mut attachment_ids: Vec = vec![]; + let mut body_parts = vec![]; + + expected_message.mail_headers.push_str("\n"); + let parsed_headers_res = MessageParser::default() + .parse_headers(expected_message.mail_headers.as_str()) + .unwrap(); + let root_part = parsed_headers_res.part(0).unwrap().clone(); + body_parts.push(root_part); + + if let Some(plain_body_part) = expected_message.plain_body_text { + let plain_body_converted = mail_parser::MessagePart { + headers: vec![], + is_encoding_problem: false, + body: mail_parser::PartType::Text(Cow::Owned(plain_body_part)), + encoding: Default::default(), + offset_header: 0, + offset_body: 0, + offset_end: 0, + }; + plain_body_ids.push(body_parts.len()); + body_parts.push(plain_body_converted); + } + + if let Some(html_body_part) = expected_message.html_body_text { + let html_body_converted = mail_parser::MessagePart { + headers: vec![], + is_encoding_problem: false, + body: mail_parser::PartType::Html(Cow::Owned(html_body_part)), + encoding: Default::default(), + offset_header: 0, + offset_body: 0, + offset_end: 0, + }; + html_body_ids.push(body_parts.len()); + body_parts.push(html_body_converted); + } + + for attached_message in expected_message.attached_messages { + let attached_message_converted = mail_parser::MessagePart { + headers: vec![], + is_encoding_problem: false, + body: Default::default(), + encoding: Default::default(), + offset_header: 0, + offset_body: 0, + offset_end: 0, + }; + attachment_ids.push(body_parts.len()); + body_parts.push(attached_message_converted); + } + + for attached_file in expected_message.attached_files { + let attached_file_converted = mail_parser::MessagePart { + headers: vec![], + is_encoding_problem: false, + body: Default::default(), + encoding: Default::default(), + offset_header: 0, + offset_body: 0, + offset_end: 0, + }; + attachment_ids.push(body_parts.len()); + body_parts.push(attached_file_converted); + } + + let parsed_mail = mail_parser::Message { + html_body: html_body_ids, + text_body: plain_body_ids, + attachments: attachment_ids, + parts: body_parts, + raw_message: Default::default(), + }; + + ImportableMail::try_from(parsed_mail).unwrap() + } + } + + #[test] + fn headers() {} + + #[test] + fn concatenate_multiple_plain_text_parts() { + let eml_contents = r#"Message-Id: some-id +From: A +To: B +Date: Tue, 5 Nov 2024 13:18:59 +0000 +Content-Type: multipart/mixed; boundary=line + +--line +Content-type: text/plain; charset=UTF-8 + +first plain text in body + +--line +Content-Type: text/plain; charset=UTF-8 + +second plain text in body +--line-- +"#; + + let parsed_message = MessageParser::default() + .with_mime_headers() + .parse(eml_contents) + .unwrap(); + let text_contents = parsed_message + .text_bodies() + .map(|a| a.text_contents().unwrap()) + .collect::>() + .join(""); + assert_eq!( + "first plain text in body\nsecond plain text in body", + text_contents + ); + } + + #[test] + fn concatenate_multiple_html_text_parts() { + let eml_contents = r#"Message-Id: some-id +From: A +To: B +Date: Tue, 5 Nov 2024 13:18:59 +0000 +Content-Type: multipart/mixed; boundary=line + +--line +Content-type: text/html; charset=UTF-8 + +

first html text in body

+ +--line +Content-Type: text/html; charset=UTF-8 + +

second html text in body

+--line-- +"#; + + let parsed_message = MessageParser::default() + .with_mime_headers() + .parse(eml_contents) + .unwrap(); + let text_contents = parsed_message + .html_bodies() + .map(|a| a.text_contents().unwrap()) + .collect::>() + .join(""); + assert_eq!( + "

first html text in body

\n

second html in body

", + text_contents + ); + } + + #[test] + fn concatenate_alternative_html_text_parts() { + let eml_contents = r#"Message-Id: some-id +From: A +To: B +Date: Tue, 5 Nov 2024 13:18:59 +0000 +Content-Type: multipart/mixed; boundary=line + +--line +Content-type: text/plain; charset=UTF-8 + +first plain text in body + +--line +Content-Type: text/html; charset=UTF-8 + +

first html text in body

+ +--line-- +"#; + + let parsed_message = MessageParser::default() + .with_mime_headers() + .parse(eml_contents) + .unwrap(); + for body_part in parsed_message.html_bodies() { + eprintln!("====="); + eprintln!("{body_part:#?}"); + } + } + + #[test] + fn concatenate_multiple_html_and_plain_text_parts() { + let eml_contents = r#"Message-Id: some-id +From: A +To: B +Date: Tue, 5 Nov 2024 13:18:59 +0000 +Content-Type: multipart/mixed; boundary=line + +--line +Content-type: text/html; charset=UTF-8 + +

first html text in body

+ + +--line +Content-Type: img/gif; charset=UTF-8 +Content-Disposition: inline; filename=name.txt; + +first plain text in body +--line-- +"#; + + let parsed_message = MessageParser::default() + .with_mime_headers() + .parse(eml_contents) + .unwrap(); + + eprintln!("{:?}", parsed_message.text_body); + eprintln!("{:?}", parsed_message.html_body); + eprintln!("{:?}", parsed_message.attachments); + + let text_contents = parsed_message + .text_bodies() + .map(|a| a.text_contents().unwrap()) + .collect::>() + .join(""); + assert_eq!( + "

first html text in body

\nfirst plain text in body", + text_contents + ); + } + #[test] + fn can_map_to_all_header_value() { + todo!() + } + + #[derive(Debug, PartialEq, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct TestMailAddress { + name: String, + mail_address: String, + valid: bool, + } + + #[derive(Debug, PartialEq, Deserialize)] + #[serde(rename_all = "camelCase")] + struct ExpectedAttachedFile { + name: String, + data: String, + mime_type: String, + charset: Option, + content_id: String, + calender_method: Option<()>, + } + + #[derive(Debug, PartialEq, Deserialize)] + #[serde(rename_all = "camelCase")] + struct ExpectedMessage { + id: Option, + boundary: Option, + alternative_boundary: Option, + sender: TestMailAddress, + to_recipients: Vec, + cc_recipients: Vec, + bcc_recipients: Vec, + reply_to: Vec, + in_reply_to: Option, + references: Vec, + auto_submitted: Option<()>, + sent_date: Option, + subject: String, + plain_body_text: Option, + html_body_text: Option, + attached_messages: Vec<()>, + attached_files: Vec, + mail_headers: String, + spf_result: String, + list_unsubscribe: bool, + mail_authentication_result: Option<()>, + } + + #[derive(Debug, PartialEq, Deserialize)] + #[serde(rename_all = "camelCase")] + struct Exception { + clazz: String, + message: String, + } + + #[derive(Debug, PartialEq, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct FileContent { + exception: Option, + result: Option, + } + + impl FileContent { + fn read_from_file(file_path: &str) -> Result { + let file_content = std::fs::read_to_string(file_path) + .map_err(|_| format!("Cannot read content of: {file_path}"))?; + serde_json::from_str::(file_content.as_str()) + .map_err(|e| format!("Cannot read to valid ExpectedMessage struct. Error: {e:?}")) + } + } + + #[test] + fn mime_tools_test_messages() { + const DATA_DIR: &'static str = + concat!(env!("CARGO_MANIFEST_DIR"), "/test/mimetools-testmsgs"); + let source_message_paths = std::fs::read_dir(DATA_DIR) + .unwrap() + .map(Result::unwrap) + .filter(|path| path.file_name().to_str().unwrap().ends_with(".msg")); + + for message_path in source_message_paths { + eprintln!("File: {}", message_path.file_name().to_str().unwrap()); + + let message_file_content = std::fs::read_to_string(&message_path.path()).unwrap(); + let parsed_message = MessageParser::default() + .parse(message_file_content.as_str()) + .expect(format!("Cannot parse test message: {:?}", message_path.path()).as_str()); + + let expected_json_file_name = format!( + "{DATA_DIR}/{}", + message_path + .file_name() + .to_str() + .unwrap() + .replace(".msg", "-expected.json") + ); + let FileContent { + result: expected_result, + exception: expected_exception, + } = FileContent::read_from_file(expected_json_file_name.as_str()).unwrap(); + let parsed_message_result = ImportableMail::try_from(parsed_message.clone()); + + if expected_result.is_some() && expected_exception.is_none() { + let expected_importable_mail = ImportableMail::from(expected_result.unwrap()); + let mut importable_mail = parsed_message_result.unwrap(); + importable_mail.attachments = vec![]; + importable_mail.different_envelope_sender = None; + + // assert_eq!( + // importable_mail.headers_string, + // expected_importable_mail.headers_string + // ); + // assert_eq!( + // importable_mail.html_body_text, + // expected_importable_mail.html_body_text + // ); + assert_eq!(importable_mail, expected_importable_mail); + } else if expected_exception.is_some() && expected_result.is_none() { + // check that the parsing have failed, + // but we cannot check for the actual reason in `expected_exception` + // + // + // todo: should not badbound.msg fail on mail_parser::parse thing? why is it failing in ImportableMail::try_from()? + //assert!(parsed_message_result.is_err()); + } else if expected_result.is_none() && expected_exception.is_none() { + unreachable!() + } else if expected_exception.is_some() && expected_exception.is_some() { + unreachable!() + } else { + unreachable!() + } + } + } +} diff --git a/packages/node-mimimi/src/importer/plain_text_to_html_converter.rs b/packages/node-mimimi/src/importer/plain_text_to_html_converter.rs new file mode 100644 index 00000000000..8be7ad70639 --- /dev/null +++ b/packages/node-mimimi/src/importer/plain_text_to_html_converter.rs @@ -0,0 +1,164 @@ +use regex::Regex; + +/// Convert plain text to html and apply the following special formatting: +/// 1. support **line breaks**, +/// by adding **
** +/// 2. support **indented plain text blocks** (marked with '>'), +/// by adding **
** <> **
** +/// 3. **escapes** "&" with "&", "<" with "<", and ">" with ">" +/// +/// This code is ported from tutadb PlainTextToHtmlConverter +pub fn plain_text_to_html(plain_text: String) -> String { + let mut result: String = String::from(""); + let SEPARATOR: Regex = Regex::new("\r?\n").expect("invalid regex"); // todo! move to const + let lines = SEPARATOR.split(plain_text.as_str()); + let mut previous_quote_level = 0; + for (i, line) in lines.enumerate() { + let line_quote_level = get_line_quote_level(line.to_string()); + + if i > 0 && (previous_quote_level == line_quote_level) { + // only append an explicit newline (
) if the quoteLevel does not change (implicit newline in case of
) + result.push_str("
") + } + + result.push_str( + "
" + .repeat( + (previous_quote_level - line_quote_level) + .try_into() + .unwrap_or(0), + ) + .as_str(), + ); + result.push_str( + "
" + .repeat( + (line_quote_level - previous_quote_level) + .try_into() + .unwrap_or(0), + ) + .as_str(), + ); + + if line_quote_level > 0 { + if line.len() > line_quote_level as usize { + let quote_block_start_index: usize = (line_quote_level + 1) as usize; + let indented_line: &str = &line[quote_block_start_index..]; + let escaped_line = escape_plain_text_line(indented_line); + result.push_str(&*escaped_line) + } + } else { + let escaped_line = escape_plain_text_line(line); + result.push_str(&*escaped_line); // skip '> ', '>> ', ... + } + previous_quote_level = line_quote_level + } + + result.push_str( + "
" + .repeat((previous_quote_level).try_into().unwrap_or(0)) + .as_str(), + ); + + result +} + +fn escape_plain_text_line(line: &str) -> String { + let escaped_line = line.replace("&", "&"); + let escaped_line = escaped_line.replace("<", "<"); + let escaped_line = escaped_line.replace(">", ">"); + escaped_line +} + +fn get_line_quote_level(line: String) -> i32 { + let mut line_open_blockquotes = 0; + for char in line.chars() { + if char == '>' { + line_open_blockquotes += 1; + } else if char == ' ' { + break; + } else { + line_open_blockquotes = 0; + break; + } + } + line_open_blockquotes +} + +/** + * Adds and tags to the given html + */ +pub fn add_html_page_tags(html: String) -> String { + format!( + "\r\n\ + \r\n\ + \r\n\ + \r\n\ + \r\n\ +{}\ + \r\n\ +\r\n", + html + ) +} + +mod test { + use crate::importer::plain_text_to_html_converter::{add_html_page_tags, plain_text_to_html}; + + #[test] + pub fn convert_to_html() { + assert_eq!("Test-Mail im Plain-Text und mit komischen Zeichen: & \"~ΓΆΓ€β₯£Τ»Β³@
weiter gehts in der naechsten Zeile", + plain_text_to_html("Test-Mail im Plain-Text und mit komischen Zeichen: & \"~ΓΆΓ€β₯£Τ»Β³@\r\nweiter gehts in der naechsten Zeile".to_string())); + + assert_eq!( + "
simple blockquote
", + plain_text_to_html("> simple blockquote".to_string()) + ); + + assert_eq!( + "
blockquote
with line break
", + plain_text_to_html("> blockquote \r\n> with line break".to_string()) + ); + + assert_eq!( + "
blockquote
with line break
", + plain_text_to_html(">> blockquote \r\n> with line break".to_string()) + ); + + assert_eq!( + "
blockquote
with line break
", + plain_text_to_html("> blockquote \r\n>> with line break".to_string()) + ); + + assert_eq!("
blockquote
with line break", + plain_text_to_html(">>> blockquote \r\n with line break".to_string())); + + // quote without text + assert_eq!( + "
", + plain_text_to_html(">".to_string()) + ); + + // quote without text but newline + assert_eq!( + "

", + plain_text_to_html(">\r\n>".to_string()) + ); + } + + #[test] + pub fn test_add_html_page_tags() { + let expected = "\r\n\ + \r\n\ + \r\n\ + \r\n\ + \r\n\ +Test-Mail im Plain-Text\ +\r\n\ +\r\n"; + assert_eq!( + expected, + add_html_page_tags("Test-Mail im Plain-Text".to_string()) + ); + } +} diff --git a/packages/node-mimimi/src/importer/status.rs b/packages/node-mimimi/src/importer/status.rs new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/packages/node-mimimi/src/importer/status.rs @@ -0,0 +1 @@ + diff --git a/packages/node-mimimi/src/lib.rs b/packages/node-mimimi/src/lib.rs new file mode 100644 index 00000000000..a1733b1c0fa --- /dev/null +++ b/packages/node-mimimi/src/lib.rs @@ -0,0 +1,7 @@ +#![deny(clippy::all)] + +pub mod importer; +#[cfg(feature = "javascript")] +pub mod logging; +pub mod tuta; +mod tuta_imap; diff --git a/packages/node-mimimi/src/logging.rs b/packages/node-mimimi/src/logging.rs new file mode 100644 index 00000000000..5925c0fdc90 --- /dev/null +++ b/packages/node-mimimi/src/logging.rs @@ -0,0 +1,6 @@ +pub(crate) mod console; +mod logger; + +/// todo: plumb through SDK's log messages? it's currently using simple_logger when not compiled +/// todo: for ios or android. +pub use crate::logging::console::Console; diff --git a/packages/node-mimimi/src/logging/console.rs b/packages/node-mimimi/src/logging/console.rs new file mode 100644 index 00000000000..1726edc4a46 --- /dev/null +++ b/packages/node-mimimi/src/logging/console.rs @@ -0,0 +1,92 @@ +use crate::logging::logger::{LogLevel, LogMessage, Logger}; +use napi::Env; +use std::sync::OnceLock; + +const TAG: &str = file!(); + +pub static INSTANCE: OnceLock = OnceLock::new(); + +/// A way for the rust code to log messages to the main applications log files +/// without having to deal with obtaining a reference to console each time. +#[derive(Clone)] +pub struct Console { + tx: std::sync::mpsc::Sender, +} + +impl Console { + pub fn get(env: Env) -> &'static Self { + let (tx, rx) = std::sync::mpsc::channel::(); + let console = Console { tx }; + let logger = Logger::new(rx); + let Ok(()) = INSTANCE.set(console) else { + // some other thread already initialized the cell, we don't need to set up the logger. + return INSTANCE + .get() + .expect("should already have been initialized!"); + }; + + // this may be the instance set by another thread, but that's okay. + let console = INSTANCE.get().expect("not initialized"); + let maybe_async_task = env.spawn(logger); + match maybe_async_task { + Ok(_) => console.log(TAG, "spawned logger"), + Err(e) => eprintln!("failed to spawn logger: {e}"), + }; + set_panic_hook(console); + console + } + + pub fn log(&self, tag: &str, message: &str) { + // todo: this returns Err if the logger closes the channel, what to do in that case? + let _ = self.tx.send(LogMessage { + level: LogLevel::Log, + tag: tag.into(), + message: message.into(), + }); + } + pub fn warn(&self, tag: &str, message: &str) { + let _ = self.tx.send(LogMessage { + level: LogLevel::Warn, + tag: tag.into(), + message: message.into(), + }); + } + + pub fn error(&self, tag: &str, message: &str) { + let _ = self.tx.send(LogMessage { + level: LogLevel::Error, + tag: tag.into(), + message: message.into(), + }); + } +} + +/// set a panic hook that tries to log the panic to the JS side before continuing +/// a normal unwind. should work unless the panicking thread is the main thread. +fn set_panic_hook(console: &'static Console) { + let logger_thread_id = std::thread::current().id(); + let panic_console = console.clone(); + let old_panic_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |panic_info| { + let formatted_info = panic_info.to_string(); + let formatted_stack = std::backtrace::Backtrace::force_capture().to_string(); + if logger_thread_id == std::thread::current().id() { + // logger is (probably) running on the currently panicking thread, + // so we can't use it to log to JS. this at least shows up in stderr. + eprintln!("PANIC MAIN {}", formatted_info); + eprintln!("PANIC MAIN {}", formatted_stack); + } else { + panic_console.error( + "PANIC", + format!( + "thread {} {}", + std::thread::current().name().unwrap_or(""), + formatted_info + ) + .as_str(), + ); + panic_console.error("PANIC", formatted_stack.as_str()); + } + old_panic_hook(panic_info) + })); +} diff --git a/packages/node-mimimi/src/logging/logger.rs b/packages/node-mimimi/src/logging/logger.rs new file mode 100644 index 00000000000..6322127fb0b --- /dev/null +++ b/packages/node-mimimi/src/logging/logger.rs @@ -0,0 +1,113 @@ +use napi::{bindgen_prelude::*, Env, JsFunction, JsObject, JsUndefined}; + +/// The part of the logging setup that receives log messages from the rust log +/// {@link struct Console} and forwards them to the node environment to log. +pub struct Logger { + /// This is an option because we need to Option::take it from the old instance before + /// rescheduling the listen job with a new one. + rx: Option>, +} + +impl Logger { + pub fn new(rx: std::sync::mpsc::Receiver) -> Self { + Self { rx: Some(rx) } + } + fn execute_log(&self, env: Env, log_message: LogMessage) { + let globals = env.get_global().expect("no globals in env"); + let console: JsObject = globals + .get_named_property("console") + .expect("console property not found"); + + let formatted_message = format!( + "[{} {}] {}", + log_message.marker(), + log_message.tag, + log_message.message + ); + let js_string: napi::JsString = env + .create_string_from_std(formatted_message) + .expect("could not create string"); + + let js_error: JsFunction = console + .get_named_property(log_message.method()) + .expect("logging fn not found"); + js_error.call(None, &[js_string]).expect("logging failed"); + } +} + +#[cfg(feature = "javascript")] +impl napi::Task for Logger { + type Output = LogMessage; + type JsValue = JsUndefined; + + /// runs on the libuv thread pool. + fn compute(&mut self) -> Result { + if let Some(rx) = &self.rx { + Ok(rx.recv().unwrap_or_else(|_| LogMessage { + level: LogLevel::Finish, + tag: "Logger".to_string(), + message: "channel closed, logger finished".to_string(), + })) + } else { + // should not happen - each Logger instance listens for exactly one message and then + // gets dropped and reincarnated. + Ok(LogMessage { + level: LogLevel::Error, + tag: "Logger".to_string(), + message: "rx not available, already moved".to_string(), + }) + } + } + + /// runs on the main thread and receives the output produced by compute + fn resolve(&mut self, env: Env, output: Self::Output) -> Result { + let level = output.level; + self.execute_log(env, output); + if level != LogLevel::Finish { + // we only have a &mut self, so can't revive ourselves directly. + // I guess this is reincarnation. + let rx = self.rx.take(); + let _promise = env.spawn(Logger { rx }); + } + env.get_undefined() + } +} + +/// determines the urgency and some formatting of the log message +#[derive(Eq, PartialEq, Copy, Clone)] +pub enum LogLevel { + /// used if we want to log the fact that all consoles have been dropped (there will not be any more log messages) + Finish, + Log, + Warn, + Error, +} + +/// a struct containing all information necessary to print the +pub struct LogMessage { + pub level: LogLevel, + pub message: String, + pub tag: String, +} + +impl LogMessage { + /// get a prefix for labeling the log level in cases where it's + /// not obvious from terminal colors or similar + pub fn marker(&self) -> &str { + match self.level { + LogLevel::Finish | LogLevel::Log => "I", + LogLevel::Warn => "W", + LogLevel::Error => "E", + } + } + + /// the name of the logging method to use for each log level. + /// very js-specific. + pub fn method(&self) -> &str { + match self.level { + LogLevel::Finish | LogLevel::Log => "log", + LogLevel::Warn => "warn", + LogLevel::Error => "error", + } + } +} diff --git a/packages/node-mimimi/src/tuta.rs b/packages/node-mimimi/src/tuta.rs new file mode 100644 index 00000000000..9c7a932ca31 --- /dev/null +++ b/packages/node-mimimi/src/tuta.rs @@ -0,0 +1 @@ +pub mod credentials; diff --git a/packages/node-mimimi/src/tuta/credentials.rs b/packages/node-mimimi/src/tuta/credentials.rs new file mode 100644 index 00000000000..de720269b1c --- /dev/null +++ b/packages/node-mimimi/src/tuta/credentials.rs @@ -0,0 +1,49 @@ +use tutasdk::login::{CredentialType, Credentials}; +use tutasdk::GeneratedId; + +#[cfg_attr(feature = "javascript", napi_derive::napi(object))] +#[derive(Clone)] +/// Passed in from js-side, will be validated before being converted to proper tuta sdk credentials. +pub struct TutaCredentials { + pub api_url: String, + pub client_version: String, + pub login: String, + pub user_id: String, + pub access_token: String, + // FIXME Buffer type causes TutaCredentials to not being able to share between threads safely + pub encrypted_passphrase_key: Vec, + pub credential_type: TutaCredentialType, +} + +impl TryFrom for Credentials { + // todo: proper errors + type Error = (); + + fn try_from(tuta_credentials: TutaCredentials) -> Result { + // todo: validate! + Ok(Credentials { + login: tuta_credentials.login, + user_id: GeneratedId(tuta_credentials.user_id), + access_token: tuta_credentials.access_token, + encrypted_passphrase_key: tuta_credentials.encrypted_passphrase_key.clone().to_vec(), + credential_type: tuta_credentials.credential_type.into(), + }) + } +} + +#[cfg_attr(feature = "javascript", napi_derive::napi)] +#[cfg_attr(not(feature = "javascript"), derive(Clone))] +#[derive(PartialEq)] +pub enum TutaCredentialType { + Internal, + External, +} + +impl From for CredentialType { + fn from(val: TutaCredentialType) -> Self { + match val { + TutaCredentialType::Internal => CredentialType::Internal, + TutaCredentialType::External => CredentialType::External, + } + } +} diff --git a/packages/node-mimimi/src/tuta_imap.rs b/packages/node-mimimi/src/tuta_imap.rs new file mode 100644 index 00000000000..bcf2339993a --- /dev/null +++ b/packages/node-mimimi/src/tuta_imap.rs @@ -0,0 +1,4 @@ +pub mod client; +#[cfg(test)] +pub mod testing; +pub mod utils; diff --git a/packages/node-mimimi/src/tuta_imap/client.rs b/packages/node-mimimi/src/tuta_imap/client.rs new file mode 100644 index 00000000000..4913e8e8e26 --- /dev/null +++ b/packages/node-mimimi/src/tuta_imap/client.rs @@ -0,0 +1,560 @@ +use crate::tuta_imap::client::tls_stream::TlsStream; +use crate::tuta_imap::client::types::ImapMail; +use imap_codec::decode::{Decoder, ResponseDecodeError}; +use imap_codec::encode::Encoder; +use imap_codec::imap_types::core::Tag; +use imap_codec::imap_types::fetch::MessageDataItemName; +use imap_codec::imap_types::mailbox::Mailbox; +use imap_codec::imap_types::response::{ + CommandContinuationRequest, Data, Response, Status, StatusBody, StatusKind, Tagged, +}; +use imap_codec::imap_types::secret::Secret; +use imap_codec::imap_types::{response, ToStatic}; +use imap_codec::{imap_types, CommandCodec, ResponseCodec}; +use imap_types::state::State as ConnectionState; +use std::collections::HashMap; +use std::num::NonZeroU32; + +// todo: +// what is the intention for this commit: +// https://github.com/duesee/imap-codec/commit/998fa15456fb0b2006c88ba5d523b5e2e115ae86 +pub mod tls_stream; +pub mod types; + +// todo: make a PR for this type alias? +pub type CapabilitiesList<'a> = imap_types::core::Vec1>; + +/// Always return a pre-formatted/sanitised error to client and not the execution details +pub type ApiError = String; +pub type ApiResult = Result; + +pub struct TutaImapClient { + pub capabilities: Option>, + pub latest_search_results: Vec, + pub latest_mails: HashMap, + pub unreceived_status: HashMap, StatusBody<'static>>, + + connection_state: ConnectionState<'static>, + + command_codec: CommandCodec, + response_codec: ResponseCodec, + tls_stream: TlsStream, +} + +/// Implement the exposed api +impl TutaImapClient { + /// Construct a new client + /// as well as: + /// - listen ( &discard ) the greetings & SSL messages + /// - refresh capabilities once + /// - perform login + pub fn new(imaps_host: &str, imaps_port: u16) -> Self { + let tls_stream = TlsStream::new(imaps_host, imaps_port); + + let mut client = Self { + tls_stream, + capabilities: None, + latest_mails: HashMap::new(), + latest_search_results: Vec::new(), + command_codec: CommandCodec::new(), + response_codec: ResponseCodec::new(), + unreceived_status: HashMap::new(), + connection_state: ConnectionState::NotAuthenticated, + }; + + // start the tls handshake process + client.start_tls().unwrap(); + + // discard any tls & greetings messages + client.read_until_next_status(None).unwrap(); + client.connection_state = ConnectionState::Greeting; + + // refresh the capabilities + client.refresh_capabilities(); + + // return the updated client + client + } + + pub fn is_logged_in(&self) -> bool { + matches!( + self.connection_state, + ConnectionState::Selected(_) + | ConnectionState::Authenticated + | ConnectionState::IdleAuthenticated(_) + | ConnectionState::IdleSelected(_, _) + ) + } + + /// try to refresh the capability from server by executing CAPABILITIES command + pub fn refresh_capabilities(&mut self) -> response::StatusKind { + let capability_command = imap_types::command::Command { + tag: self.create_tag(), + body: imap_types::command::CommandBody::Capability, + }; + let capability_response = self.execute_command_directly(capability_command).unwrap(); + + capability_response.kind + } + + pub fn plain_login(&mut self, username: &str, password: &str) -> response::StatusKind { + let login_command = imap_types::command::Command { + tag: self.create_tag(), + body: imap_types::command::CommandBody::Login { + username: username.try_into().unwrap(), + password: Secret::new(password.try_into().unwrap()), + }, + }; + let login_response = self.execute_command_directly(login_command).unwrap(); + let status_kind = login_response.kind; + + if status_kind == response::StatusKind::Ok { + self.connection_state = ConnectionState::Authenticated; + } + + status_kind + } + + /// List all the mailboxes (i.e. folders) available inside the imap account. + pub fn list_mailboxes(&mut self) -> StatusKind { + assert_eq!( + ConnectionState::Authenticated, + self.connection_state, + "must be in authenticated state to list mailboxes. Current state:" + ); + // TODO check values for mailbox_wildcard and reference + /* let list_command = imap_types::command::Command { + tag: self.create_tag(), + body: imap_types::command::CommandBody::List { + mailbox_wildcard: ListMailbox::String("*".into()), + reference: Mailbox::Other("".into()), + }, + }; + let list_response = self.execute_command_directly(list_command).unwrap(); + let status_kind = list_response.kind; + if status_kind == response::StatusKind::Ok { + self.connection_state = ConnectionState::Authenticated; + } + status_kind*/ + StatusKind::Ok + } + + /// Select a single mailbox (i.e. folder) + /// + /// Caller should already invoke `list_mailboxes` function before calling this function. + pub fn select_mailbox(&mut self, mailbox: Mailbox) -> StatusKind { + assert!( + matches!( + self.connection_state, + ConnectionState::Authenticated | ConnectionState::Selected(_) + ), + "must be in authenticated/selected state to select mailbox" + ); + let select_command = imap_types::command::Command { + tag: self.create_tag(), + body: imap_types::command::CommandBody::Select { + mailbox: mailbox.clone(), + }, + }; + + let select_response = self.execute_command_directly(select_command).unwrap(); + let status_kind = select_response.kind; + if status_kind == StatusKind::Ok { + self.connection_state = ConnectionState::Selected(mailbox.to_static()); + } + status_kind + } + + // TODO implement imap commands exposing the complete syntax? + pub fn fetch(&mut self, command_body: imap_types::command::CommandBody) -> StatusKind { + assert_eq!( + ConnectionState::Selected(Mailbox::Inbox), + self.connection_state, + "must be in selected state to fetch mailbox UID" + ); + let fetch_command = imap_types::command::Command { + tag: self.create_tag(), + body: command_body, + }; + + let search_all_command = self.execute_command_directly(fetch_command).unwrap(); + + search_all_command.kind + } + + /// perform a UID search command + pub fn search_all_uid(&mut self) -> StatusKind { + assert_eq!( + ConnectionState::Selected(Mailbox::Inbox), + self.connection_state, + "must be in selected state to search mailbox UIDs" + ); + let search_all_command = imap_types::command::Command { + tag: self.create_tag(), + body: imap_types::command::CommandBody::Search { + charset: None, + uid: true, + criteria: [imap_types::search::SearchKey::All].into(), + }, + }; + + let search_all_command = self.execute_command_directly(search_all_command).unwrap(); + + search_all_command.kind + } + + /// fetch mail with given uid + // todo: + /// & given uidValidity + pub fn fetch_mail_by_uid(&mut self, uid: NonZeroU32) -> StatusKind { + assert_eq!( + ConnectionState::Selected(Mailbox::Inbox), + self.connection_state, + "must be in selected state to fetch mailbox UID" + ); + let fetch_command = imap_types::command::Command { + tag: self.create_tag(), + body: imap_types::command::CommandBody::Fetch { + uid: true, + sequence_set: imap_types::sequence::Sequence::Single(uid.into()).into(), + macro_or_item_names: vec![MessageDataItemName::Rfc822].into(), + }, + }; + + let search_all_command = self.execute_command_directly(fetch_command).unwrap(); + + search_all_command.kind + } +} + +/// Implement direct helper function divisions + +impl TutaImapClient { + fn create_tag(&mut self) -> Tag<'static> { + Tag::try_from("tag").unwrap() + } + + fn start_tls(&mut self) -> Result<(), ()> { + Ok(()) + } + + // Use any untagged data response to update the state + fn process_data_response(&mut self, data_response: Data) { + match data_response { + Data::Capability(list) => self.capabilities = Some(list.to_static()), + Data::Search(list) => self.latest_search_results = list.to_static(), + Data::Fetch { seq, items } => { + self.latest_mails.insert(seq, ImapMail::new(items)); + }, + + anything_else => { + log::warn!("Do not know yet how to handle: {anything_else:?}") + }, + } + } + + // command continuation request + fn process_cmd_continutation_response( + &self, + cmd_continutation_response: CommandContinuationRequest, + ) -> Result<(), ()> { + Ok(()) + } + + /// Process any response parsed. + fn process_response(&mut self, response: Response) { + match response { + Response::Data(untagged_data) => { + self.process_data_response(untagged_data); + }, + Response::Status(status) => match status { + Status::Untagged(untagged_status) => { + log::warn!("Received untagged status: {:?}", untagged_status); + }, + Status::Tagged(response::Tagged { tag, body }) => { + self.unreceived_status + .insert(tag.to_static(), body.to_static()); + }, + Status::Bye(response_bye) => { + log::warn!("Received bye from server. byeeeee."); + self.connection_state = ConnectionState::Logout; + }, + }, + Response::CommandContinuationRequest(cmd_continuation) => { + self.process_cmd_continutation_response(cmd_continuation) + .unwrap(); + }, + } + } + + /// returns if response bytes is incomplete + fn parse_response( + &mut self, + response_bytes: &mut Vec, + ) -> Result { + if response_bytes.is_empty() { + Err(ResponseDecodeError::Failed)?; + } + + let response = self.response_codec.decode(response_bytes.as_ref()); + match response { + Ok((_left_over, response)) => { + log::info!("Got response to be: `{:?}`", response); + Ok(response.to_static()) + }, + + Err(ResponseDecodeError::LiteralFound { length }) => { + log::warn!( + "Literal found for response: {}", + String::from_utf8(response_bytes.to_vec()).unwrap() + ); + + // read everything remaining + let mut remaining_literal = vec![0u8; length.try_into().unwrap()]; + self.tls_stream.read_exact(&mut remaining_literal).unwrap(); + response_bytes.append(&mut remaining_literal); + + // try again + self.parse_response(response_bytes) + }, + + // if this is incomplete/Failed , + // save this might have to re-read again once we get remaining of response + Err(ResponseDecodeError::Incomplete) | Err(ResponseDecodeError::Failed) => { + log::warn!( + "Got an {response:?} response from server. Saving it: `{}`", + String::from_utf8(response_bytes.to_vec()).unwrap() + ); + Err(response.unwrap_err()) + }, + } + } + + // execute a command in imap + // + // this api is allowed to cache or delay the execution given command: + // example: some other non-overridden-able command is in progress + // example: LOGIN command is in progress. it's better to wait for such command to finish + // so that we can execute following command in correct state context + // + // this api is allowed to block the execution for so reason. If the waiting is not desired, + // todo: + // call another async function which will received the command and put it to queue, + // once the command is executed and the response with tag of this command is received, + // the response ( only the tagged one ) will be passed to the receiving channel + + fn execute_command_directly( + &mut self, + command: imap_types::command::Command, + ) -> Result { + // only check for logout state, + // calling function should make sure to check for other state + // if that action expects client to be in certain state + assert_ne!( + ConnectionState::Logout, + self.connection_state, + "Cannot execute command after being logged out" + ); + log::info!("Start Executing command: {command:?}"); + + // write the command + let encoded_command = self.command_codec.encode(&command); + self.tls_stream + .write_imap_command(encoded_command.dump().as_slice()) + .unwrap(); + + log::info!("Command written..."); + + self.read_until_next_status(Some(&command.tag)) + } + + /// Read until we get next StatusCode ( Ok, Bye, Bad, PreAuth ) + /// + /// If expected_tag is Some(), we always make sure the status match this tag, + /// if not, we return on first tagged/untagged status + fn read_until_next_status(&mut self, expected_tag: Option<&Tag>) -> Result { + let mut next_line: Vec = Vec::new(); + loop { + // we assume we get at least one line of response with every command + // otherwise we will wait here forever, + // unless we get any other response ( which is still not ok because we check for this tag later on) + next_line.append(&mut self.tls_stream.read_until_crlf().unwrap()); + + let maybe_cmd_status = self.parse_response(&mut next_line).map(|r| r.to_static()); + return match maybe_cmd_status { + // if it's the tagged response + // with the same tag as of command + Ok(Response::Status(some_status)) => match some_status { + Status::Tagged(Tagged { tag, body }) + if Some(&tag) == expected_tag || expected_tag.is_none() => + { + Ok(body.to_static()) + }, + Status::Untagged(body) if expected_tag.is_none() => Ok(body.to_static()), + + Status::Tagged(_) | Status::Untagged(_) | Status::Bye(_) => { + next_line.clear(); + self.process_response(Response::Status(some_status)); + continue; + }, + }, + + Ok(response) => { + next_line.clear(); + self.process_response(response); + continue; + }, + + Err(ResponseDecodeError::Incomplete) => { + // read one more line and try again + continue; + }, + + Err(ResponseDecodeError::LiteralFound { .. }) => { + unreachable!("Expect literal found to be handled inside parse response") + }, + + // response was literalFound? Incomplete? + Err(ResponseDecodeError::Failed) => { + log::warn!( + "Cannot get the tagged response from server for {:?} What to do now?", + expected_tag + ); + Err(()) + }, + }; + } + } +} + +/// Credentials mechanism to use for authenticating the client +/// +/// LOGIN command will be available in all imap server, +/// but this is the least secure way to authenticate. and simplest. +/// +/// According to server capabilities, we can choose to perform login via any +/// SASL* (RFC 4422) authentication mechanism. +/// +/// Example: +/// Gmail IMAP server support OAUTH2, +/// and provides a custom `Authenticate` Command to do so. +/// this will require CommandContinuationRequests and hence is less +/// simple than LOGIN but this mechanism will be more "secured" +/// +/// todo: +/// For now only care for PLAIN mechanism. +pub enum CredentialsMechanism { + Plain, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tuta_imap::testing::utils::to_non_zero_u32; + use crate::tuta_imap::testing::GreenMailTestServer; + use imap_codec::imap_types::response::{Capability, StatusKind}; + + #[test] + fn can_refresh_capabilities() { + let greenmail = GreenMailTestServer::new(); + let mut import_client = TutaImapClient::new("127.0.0.1", greenmail.imaps_port as u16); + + // refreshing multiple times should still result in same + for _ in 0..3 { + assert_eq!(StatusKind::Ok, import_client.refresh_capabilities()); + assert_eq!( + Some(vec![ + Capability::Imap4Rev1, + Capability::LiteralPlus, + Capability::UidPlus, + Capability::Sort(None), + Capability::Idle, + Capability::Move, + Capability::Quota, + ]), + import_client + .capabilities + .clone() + .map(CapabilitiesList::into_inner) + ); + } + } + + #[test] + fn can_login() { + let greenmail = GreenMailTestServer::new(); + let mut import_client = TutaImapClient::new("127.0.0.1", greenmail.imaps_port as u16); + + // refreshing multiple times should still result in same + assert_eq!( + StatusKind::Ok, + import_client.plain_login("sug@example.org", "sug") + ); + assert_eq!( + import_client.connection_state, + ConnectionState::Authenticated + ); + } + + #[test] + fn select_inbox() { + let greenmail = GreenMailTestServer::new(); + let mut import_client = TutaImapClient::new("127.0.0.1", greenmail.imaps_port as u16); + + import_client.plain_login("sug@example.org", "sug"); + + // refreshing multiple times should still result in same + assert_eq!(StatusKind::Ok, import_client.select_mailbox(Mailbox::Inbox)); + assert_eq!( + import_client.connection_state, + ConnectionState::Selected(Mailbox::Inbox) + ); + } + + #[test] + fn search_all_mail() { + let greenmail = GreenMailTestServer::new(); + let mut import_client = TutaImapClient::new("127.0.0.1", greenmail.imaps_port as u16); + + // should find these two `sug` mails + greenmail.store_mail("sug@example.org", ""); + greenmail.store_mail("sug@example.org", ""); + // should not find this `map` mail + greenmail.store_mail("map@example.org", ""); + + import_client.plain_login("sug@example.org", "sug"); + import_client.select_mailbox(Mailbox::Inbox); + assert_eq!(StatusKind::Ok, import_client.search_all_uid()); + assert_eq!( + to_non_zero_u32(&[1, 2]), + import_client.latest_search_results + ); + } + + #[test] + fn fetch_mail() { + let greenmail = GreenMailTestServer::new(); + let mut import_client = TutaImapClient::new("127.0.0.1", greenmail.imaps_port as u16); + + greenmail.store_mail("map@example.org", "Subject: =?UTF-8?B?bWEgdXRmLTgg4oKs?="); + greenmail.store_mail("map@example.org", "Subject: Find me if you can"); + + import_client.plain_login("map@example.org", "map"); + import_client.select_mailbox(Mailbox::Inbox); + import_client.search_all_uid(); + + let message_id = NonZeroU32::new(1).unwrap(); + assert_eq!(StatusKind::Ok, import_client.fetch_mail_by_uid(message_id)); + + let imap_mail = import_client.latest_mails.get(&message_id).unwrap(); + let parsed_mail = mail_parser::MessageParser::new() + .parse(imap_mail.rfc822_full.as_slice()) + .unwrap(); + // assert_eq!( + // &ImapMail { + // subject: "=?UTF-8?B?bWEgdXRmLTgg4oKs?=".to_string() + // }, + // import_client.latest_mails.get(&message_id).unwrap(), + // ); + } +} diff --git a/packages/node-mimimi/src/tuta_imap/client/tls_stream.rs b/packages/node-mimimi/src/tuta_imap/client/tls_stream.rs new file mode 100644 index 00000000000..da00b4cf44f --- /dev/null +++ b/packages/node-mimimi/src/tuta_imap/client/tls_stream.rs @@ -0,0 +1,109 @@ +use crate::tuta_imap::utils::BufReadExtension; +use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; +use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; +use rustls::{ClientConfig, ClientConnection, DigitallySignedStruct, Error, SignatureScheme}; +use std::io::{BufReader, Read, Write}; +use std::net::{SocketAddr, SocketAddrV4, TcpStream}; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; + +pub type SecuredStream = rustls::StreamOwned; + +pub struct TlsStream { + buffer_controller: BufReader, +} + +impl TlsStream { + pub fn new(address: &str, port: u16) -> Self { + let tcp_address = SocketAddr::V4(SocketAddrV4::new( + std::net::Ipv4Addr::from_str(address).unwrap(), + port, + )); + let tcp_stream = TcpStream::connect_timeout(&tcp_address, Duration::from_secs(10)).unwrap(); + + let dangerous_config = ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(MockSsl)) + .with_no_client_auth(); + let client_connection = rustls::ClientConnection::new( + Arc::new(dangerous_config), + address.to_string().try_into().unwrap(), + ) + .unwrap(); + + let buffer_controller = BufReader::new(SecuredStream::new(client_connection, tcp_stream)); + TlsStream { buffer_controller } + } + + pub fn write_imap_command(&mut self, encoded_command: &[u8]) -> std::io::Result { + let writer = self.buffer_controller.get_mut(); + let written = writer.write(encoded_command)?; + writer.flush()?; + Ok(written) + } + + pub fn read_until_crlf(&mut self) -> std::io::Result> { + let mut line_until_crlf = Vec::new(); + self.buffer_controller + .read_until_slice(b"\r\n", &mut line_until_crlf)?; + + Ok(line_until_crlf) + } + + pub fn read_exact(&mut self, target: &mut Vec) -> std::io::Result<()> { + self.buffer_controller.read_exact(target) + } +} + +#[derive(Debug)] +pub struct MockSsl; + +impl ServerCertVerifier for MockSsl { + fn verify_server_cert( + &self, + end_entity: &CertificateDer<'_>, + intermediates: &[CertificateDer<'_>], + server_name: &ServerName<'_>, + ocsp_response: &[u8], + now: UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + SignatureScheme::RSA_PKCS1_SHA1, + SignatureScheme::ECDSA_SHA1_Legacy, + SignatureScheme::RSA_PKCS1_SHA256, + SignatureScheme::ECDSA_NISTP256_SHA256, + SignatureScheme::RSA_PKCS1_SHA384, + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::RSA_PKCS1_SHA512, + SignatureScheme::ECDSA_NISTP521_SHA512, + SignatureScheme::RSA_PSS_SHA256, + SignatureScheme::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA512, + SignatureScheme::ED25519, + SignatureScheme::ED448, + ] + } +} diff --git a/packages/node-mimimi/src/tuta_imap/client/types.rs b/packages/node-mimimi/src/tuta_imap/client/types.rs new file mode 100644 index 00000000000..e435134d1b0 --- /dev/null +++ b/packages/node-mimimi/src/tuta_imap/client/types.rs @@ -0,0 +1,37 @@ +use imap_codec::imap_types::core::Vec1; +use imap_codec::imap_types::fetch::MessageDataItem; + +#[derive(Eq, PartialEq, Debug)] +pub struct ImapMail { + pub rfc822_full: Vec, +} + +impl ImapMail { + pub fn new(items: Vec1) -> Self { + let mut imap_mail = ImapMail { + rfc822_full: Vec::new(), + }; + + for item in items { + match item { + MessageDataItem::Rfc822(rfc822_text) => { + imap_mail.rfc822_full = rfc822_text.0.unwrap().into_inner().to_vec(); + }, + + MessageDataItem::Envelope(envelope) => {}, + MessageDataItem::Body(_) => {}, + MessageDataItem::BodyExt { .. } => {}, + MessageDataItem::BodyStructure(_) => {}, + MessageDataItem::Flags(_) => {}, + MessageDataItem::InternalDate(_) => {}, + MessageDataItem::Rfc822Text(_) => {}, + MessageDataItem::Rfc822Header(_) => {}, + MessageDataItem::Rfc822Size(_) => {}, + MessageDataItem::Uid(_) => {}, + MessageDataItem::Binary { .. } => {}, + MessageDataItem::BinarySize { .. } => {}, + } + } + imap_mail + } +} diff --git a/packages/node-mimimi/src/tuta_imap/testing.rs b/packages/node-mimimi/src/tuta_imap/testing.rs new file mode 100644 index 00000000000..3fd5214a593 --- /dev/null +++ b/packages/node-mimimi/src/tuta_imap/testing.rs @@ -0,0 +1,161 @@ +use j4rs::{Instance, InvocationArg, Jvm}; +use std::collections::HashMap; + +pub mod jvm_singeleton; +pub mod utils; + +pub const GREENMAIL_TEST_SERVER_JAR: &str = env!("GREENMAIL_TEST_SERVER_JAR"); +pub const IMAPS_STARTING_PORT: i32 = 3993; + +pub struct GreenMailTestServer { + pub jvm: Jvm, + pub server: Instance, + + pub imaps_address: (String, u32), + + pub users: HashMap<&'static str, Instance>, + pub imaps_port: i32, +} + +impl GreenMailTestServer { + pub fn new() -> Self { + let this_jvm_id = jvm_singeleton::start_or_attach_to_jvm(); + let imaps_port = this_jvm_id + IMAPS_STARTING_PORT; + let jvm = Jvm::attach_thread().unwrap(); + + let imaps_host = jvm + .static_class_field("greenmailserver.GreenMailServer", "imapsHost") + .map(|v| jvm.to_rust(v)) + .unwrap() + .unwrap(); + let imaps_address = (imaps_host, imaps_port as u32); + + let server = jvm + .create_instance( + "greenmailserver.GreenMailServer", + &[InvocationArg::try_from(imaps_port).unwrap()], + ) + .unwrap(); + + let mut users = HashMap::new(); + users.insert("map", jvm.field(&server, "userMap").unwrap()); + users.insert("sug", jvm.field(&server, "userSug").unwrap()); + + Self { + users, + jvm, + server, + imaps_address, + imaps_port, + } + } + + pub fn stop(self) { + self.stop_greenmail_server(); + } + + fn stop_greenmail_server(&self) { + self.jvm + .invoke(&self.server, "stop", InvocationArg::empty()) + .unwrap(); + } + + pub fn store_mail(&self, receiver: &str, mime_message: &str) { + self.jvm + .invoke( + &self.server, + "store_mail", + &[ + &InvocationArg::try_from(receiver).unwrap(), + &mime_message.try_into().unwrap(), + ], + ) + .unwrap(); + } +} + +impl Drop for GreenMailTestServer { + fn drop(&mut self) { + self.stop_greenmail_server() + } +} + +#[cfg(test)] +pub mod greenmail_interaction { + use super::*; + use std::process::Command; + + #[test] + pub fn ensure_imap_server_running() { + let test_server = GreenMailTestServer::new(); + let (imaps_host, imaps_port) = &test_server.imaps_address; + + let output = Command::new("curl") + .args([ + format!("imaps://{imaps_host}:{imaps_port}").as_str(), + "--request", + "CAPABILITY", + "-k", + ]) + .output() + .unwrap(); + + assert!(output.status.success()); + assert_eq!( + b"* CAPABILITY IMAP4rev1 LITERAL+ UIDPLUS SORT IDLE MOVE QUOTA\r\n", + output.stdout.as_slice() + ); + } + + #[test] + pub fn ensure_can_store_mail() { + let test_server = GreenMailTestServer::new(); + let (imaps_host, imaps_port) = &test_server.imaps_address; + + test_server.store_mail( + "sug@example.org", + r#"From: Some One +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="XXXXboundary text" + +This is a multipart message in MIME format. + +--XXXXboundary text +Content-Type: text/plain + +this is the body text + +--XXXXboundary text +Content-Type: text/plain; +Content-Disposition: attachment; + filename="test.txt" + +this is the attachment text + +--XXXXboundary text--"#, + ); + + let output = Command::new("curl") + .args([ + format!("imaps://{imaps_host}:{imaps_port}/INBOX").as_str(), + "--request", + "LIST \"\" *", + "--user", + "sug@example.org:sug", + "--request", + "FETCH 1 BODY[HEADER]", + "-k", + ]) + .output() + .unwrap(); + + assert!(output.status.success()); + assert_eq!( + b"* 1 FETCH (FLAGS (\\Seen) BODY[HEADER] {127}\r\n", + output.stdout.as_slice(), + "{}", + String::from_utf8(output.stdout.to_vec()).unwrap() + ); + } +} diff --git a/packages/node-mimimi/src/tuta_imap/testing/jvm_singeleton.rs b/packages/node-mimimi/src/tuta_imap/testing/jvm_singeleton.rs new file mode 100644 index 00000000000..cd9e4cb9877 --- /dev/null +++ b/packages/node-mimimi/src/tuta_imap/testing/jvm_singeleton.rs @@ -0,0 +1,21 @@ +use super::GREENMAIL_TEST_SERVER_JAR; +use j4rs::{ClasspathEntry, JvmBuilder}; + +static mut START_JVM_INVOCATION_COUNTER: i32 = 0; + +pub fn start_or_attach_to_jvm() -> i32 { + /// todo: SAFETY??? + unsafe { + if START_JVM_INVOCATION_COUNTER == 0 { + // create exactly one jvm and attach to it whenever we create a new IMAP test server + + JvmBuilder::new() + .classpath_entry(ClasspathEntry::new(GREENMAIL_TEST_SERVER_JAR)) + .with_default_classloader() + .build() + .expect("Cannot start jvm"); + } + START_JVM_INVOCATION_COUNTER += 1; + START_JVM_INVOCATION_COUNTER + } +} diff --git a/packages/node-mimimi/src/tuta_imap/testing/utils.rs b/packages/node-mimimi/src/tuta_imap/testing/utils.rs new file mode 100644 index 00000000000..954c8ce2761 --- /dev/null +++ b/packages/node-mimimi/src/tuta_imap/testing/utils.rs @@ -0,0 +1,5 @@ +use std::num::NonZeroU32; + +pub fn to_non_zero_u32(slice: &[u32]) -> Vec { + slice.iter().map(|s| NonZeroU32::new(*s).unwrap()).collect() +} diff --git a/packages/node-mimimi/src/tuta_imap/utils.rs b/packages/node-mimimi/src/tuta_imap/utils.rs new file mode 100644 index 00000000000..7905ba9bcb1 --- /dev/null +++ b/packages/node-mimimi/src/tuta_imap/utils.rs @@ -0,0 +1,28 @@ +use std::io::BufRead; + +pub trait BufReadExtension { + /// Same as `std::io::BufRead::read_until` + /// instead of accepting a single byte, accept a slice + fn read_until_slice(&mut self, delimiter: &[u8], buf: &mut Vec) -> std::io::Result; +} + +// implement for everything that have BufRead +impl BufReadExtension for T +where + T: BufRead, +{ + fn read_until_slice(&mut self, delimeter: &[u8], buf: &mut Vec) -> std::io::Result { + let mut read_count = 0; + + loop { + let mut one_byte = [0]; + self.read_exact(&mut one_byte)?; + buf.push(one_byte[0]); + + read_count += 1; + if buf.ends_with(delimeter) { + break Ok(read_count); + } + } + } +} diff --git a/packages/node-mimimi/test/Suite.ts b/packages/node-mimimi/test/Suite.ts new file mode 100644 index 00000000000..ac4aebaa8c9 --- /dev/null +++ b/packages/node-mimimi/test/Suite.ts @@ -0,0 +1,16 @@ +import { createFileImporter, TutaCredentials, TutaCredentialType } from "../dist/binding.cjs" + +// we still need to figure out where we can get the encryptedPassphraseKey from +let tutaCredential: TutaCredentials = { + apiUrl: "http://localhost:9000", + clientVersion: "246", + login: "map-free@tutanota.de", + userId: "O9xate2----0", + accessToken: "ZK9m5qGAB0ABSsmXJFz6b7m16rhSJ6y6aA", + encryptedPassphraseKey: [], + credentialType: TutaCredentialType.Internal, +} +let importer = await createFileImporter(tutaCredential.login, tutaCredential, "../test/sample.eml", false) + +let importStatus = await importer.continueImportNapi() +console.log("Import status is: ", importStatus) diff --git a/packages/node-mimimi/test/mimetools-testmsgs/2002_06_12_doublebound-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/2002_06_12_doublebound-expected.json new file mode 100644 index 00000000000..1c133c6245f --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/2002_06_12_doublebound-expected.json @@ -0,0 +1,45 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "ReaqsoxgOBHFXBhH", + "alternativeBoundary": null, + "sender": { + "name": "Lars Hecking", + "mailAddress": "lhecking@nmrc.ie", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "mutt-dev@mutt.org", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "MIME handling bug demo", + "plainBodyText": "\r\n-- \r\nThe plot was designed in a light vein that somehow became varicose.\r\n -- David Lardner\r\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "The Mutt E-Mail Client.html", + "data": "PCFET0NUWVBFIEhUTUwgUFVCTElDICItLy9XM0MvL0RURCBIVE1MIDQuMCBUcmFuc2l0aW9uYWwvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvVFIvUkVDLWh0bWw0MC9sb29zZS5kdGQiPjxodG1sPjxoZWFkPjx0aXRsZT5UaGUgTXV0dCBFLU1haWwgQ2xpZW50PC90aXRsZT48L2hlYWQ+DQoNCg0KDQo8Ym9keT4NCg0KPGNlbnRlcj4NCjxoMT5UaGUgTXV0dCBFLU1haWwgQ2xpZW50PC9oMT4NCjxwPg0KPGNpdGU+DQoiQWxsIG1haWwgY2xpZW50cyBzdWNrLiAgVGhpcyBvbmUganVzdCBzdWNrcyBsZXNzLiIgIC1tZSwgY2lyY2EgMTk5NQ0KPC9jaXRlPg0KPC9wPjwvY2VudGVyPg0KPHA+DQo8YSBocmVmPSJodHRwOi8vd3d3Lm11dHQub3JnL2xpbmtzLmh0bWwjbWlycm9ycyI+bWlycm9yczwvYT4NCjwvcD48aHI+PHA+DQo8L3A+PGgyPkxhdGVzdCBOZXdzPC9oMj4NCjwhLS0gU3RpY2sgaW1wb3J0YW50IG5ld3MgKHNlY3VyaXR5LCBldGMuKSBoZXJlLCBjeWNsZWQgb3V0IHRvIG5ld3MuaHRtbC4gLS0+DQo8cD4NCk11dHQgMS4zLjI4IHdhcyByZWxlYXNlZCBvbiBNYXJjaCAxMywgMjAwMi4gIFRoaXMgaXMgYSA8c3Ryb25nPnJlbGVhc2UNCmNhbmRpZGF0ZTwvc3Ryb25nPiBmb3IgMS40Lg0KPC9wPjxwPg0KTXV0dCAxLjIuNS4xIGFuZCAxLjMuMjUgd2VyZSByZWxlYXNlZCBvbiBKYW51YXJ5IDEsIDIwMDIuICBUaGVzZSByZWxlYXNlcyBib3RoDQpmaXggYSA8c3Ryb25nPnNlY3VyaXR5IGhvbGU8L3N0cm9uZz4gd2hpY2ggY2FuIGJlIHJlbW90ZWx5IGV4cGxvaXRlZC4gIEZvciBtb3JlDQppbmZvcm1hdGlvbiwgc2VlIHRoZQ0KPGEgaHJlZj0iaHR0cDovL3d3dy5tdXR0Lm9yZy9hbm5vdW5jZS9tdXR0LTEuMi41LjEtMS4zLjI1Lmh0bWwiPnJlbGVhc2UgYW5ub3VuY2VtZW50PC9hPi4NCjwvcD48cD4NCk11dHQgMS4zLjI0IHdhcyByZWxlYXNlZCBvbiBOb3ZlbWJlciAzMCwgMjAwMS4gIFRoaXMgaXMgYSA8c3Ryb25nPmJldGE8L3N0cm9uZz4NCmRldmVsb3BtZW50IHJlbGVhc2UgdG93YXJkIHRoZSBuZXh0IHN0YWJsZSBwdWJsaWMgcmVsZWFzZSB2ZXJzaW9uLiAgVGhlcmUgaGF2ZQ0KYmVlbiBzZXZlcmFsIGxhcmdlIGNoYW5nZXMgc2luY2UgMS4yLngsIHNvIHBsZWFzZSBjaGVjayB0aGUNCjxhIGhyZWY9Imh0dHA6Ly93d3cubXV0dC5vcmcvY2hhbmdlcy5odG1sIj5yZWNlbnQgY2hhbmdlcyBwYWdlPC9hPi4NCjwvcD48cD4NClRoZSBNdXR0IENWUyBzZXJ2ZXIgaGFzIDxzdHJvbmc+bW92ZWQ8L3N0cm9uZz4gZnJvbSBmdHAuZ3V1Zy5kZSB0byBmdHAubXV0dC5vcmcuDQo8L3A+PHA+DQo8YSBocmVmPSJodHRwOi8vd3d3Lm11dHQub3JnL25ld3MuaHRtbCI+bW9yZSBuZXdzPC9hPg0KPC9wPjxwPjwvcD48aHI+PHA+DQo8L3A+PGgyPkdlbmVyYWwgSW5mbzwvaDI+DQo8cD4NCk11dHQgaXMgYSBzbWFsbCBidXQgdmVyeSBwb3dlcmZ1bCB0ZXh0LWJhc2VkIG1haWwgY2xpZW50IGZvciBVbml4IG9wZXJhdGluZw0Kc3lzdGVtcy4gIFRoZSBsYXRlc3QgcHVibGljIHJlbGVhc2UgdmVyc2lvbiBpcyAxLjMuMjgsIHdoaWNoIGlzIGENCjxzdHJvbmc+cmVsZWFzZSBjYW5kaWRhdGU8L3N0cm9uZz4gZm9yIDEuNC4gIFRoZSBjdXJyZW50IHN0YWJsZSBwdWJsaWMgcmVsZWFzZQ0KdmVyc2lvbiBpcyAxLjIuNS4xLiBGb3IgbW9yZSBpbmZvcm1hdGlvbiwgc2VlIHRoZSBmb2xsb3dpbmc6DQo8L3A+PHA+DQo8L3A+PHVsPjxsaT48YSBocmVmPSIjZmVhdHVyZXMiPkZlYXR1cmVzPC9hPg0KICAgIDwvbGk+PGxpPjxhIGhyZWY9Imh0dHA6Ly93d3cubXV0dC5vcmcvc2NyZWVuc2hvdHMvIj5TY3JlZW5zaG90czwvYT4NCiAgICA8L2xpPjxsaT5Eb2N1bWVudGF0aW9uDQogICAgICAgIDx1bD48bGk+PGEgaHJlZj0iaHR0cDovL3d3dy5mZWZlLmRlL211dHRmYXEvZmFxIj5GQVE8L2E+DQogICAgICAgIChtYWludGFpbmVkIGJ5IEZlbGl4IHZvbiBMZWl0bmVyKQ0KICAgICAgICAgICAgPC9saT48bGk+PGEgaHJlZj0iaHR0cDovL3d3dy5tdXR0Lm9yZy9kb2MvbWFuX3BhZ2UuaHRtbCI+TWFuIFBhZ2U8L2E+DQogICAgICAgICAgICA8L2xpPjxsaT5NYW51YWwNCiAgICAgICAgICAgICAgICA8dWw+PGxpPjxhIGhyZWY9Imh0dHA6Ly93d3cubXV0dC5vcmcvZG9jL21hbnVhbC8iPkhUTUw8L2E+DQogICAgICAgICAgICAgICAgICAgICAgICAoPGEgaHJlZj0iaHR0cDovL3d3dy5tdXR0Lm9yZy9kb2MvbWFudWFsLmh0bWwudGFyLmd6Ij5nemlwcGVkPC9hPikNCiAgICAgICAgICAgICAgICAgICAgPC9saT48bGk+PGEgaHJlZj0iaHR0cDovL3d3dy5tdXR0Lm9yZy9kb2MvbWFudWFsLnR4dCI+VGV4dDwvYT4NCiAgICAgICAgICAgICAgICAgICAgICAgICg8YSBocmVmPSJodHRwOi8vd3d3Lm11dHQub3JnL2RvYy9tYW51YWwudHh0Lmd6Ij5nemlwcGVkPC9hPikNCiAgICAgICAgICAgICAgICA8L2xpPjwvdWw+DQogICAgICAgICAgICA8L2xpPjxsaT48YSBocmVmPSJodHRwOi8vd3d3Lm11dHQub3JnL2NoYW5nZXMuaHRtbCI+UmVjZW50IENoYW5nZXMgdG8gTXV0dDwvYT4NCiAgICAgICAgICAgICAgICAocGxlYXNlIHJlYWQgaWYgdXBncmFkaW5nKQ0KICAgICAgICAgICAgPC9saT48bGk+PGEgaHJlZj0iaHR0cDovL3d3dy5tdXR0Lm9yZy9saW5rcy5odG1sI2NvbmZpZyI+U2FtcGxlIENvbmZpZ3VyYXRpb24gKC5tdXR0cmMsIGV0Yy4pIEZpbGVzPC9hPg0KICAgICAgICAgICAgPC9saT48bGk+PGEgaHJlZj0iaHR0cDovL211dHQuc291cmNlZm9yZ2UubmV0L2ltYXAvIj5NdXR0IGFuZCBJTUFQPC9hPg0KICAgICAgICAgICAgICAgIChtYWludGFpbmVkIGJ5IEJyZW5kYW4gQ3VsbHkpDQogICAgICAgICAgICA8L2xpPjxsaT5Vc2luZyBNdXR0IHdpdGggR1BHL1BHUA0KICAgICAgICAgICAgICAgIDx1bD48bGk+PGEgaHJlZj0iaHR0cDovL3d3dy5tdXR0Lm9yZy9kb2MvUEdQLU5vdGVzLnR4dCI+T2ZmaWNpYWwgTXV0dCBkb2M8L2E+DQogICAgICAgICAgICAgICAgICAgICAgICAoaW5jbHVkZWQgaW4gdGhlIHJlbGVhc2UpDQogICAgICAgICAgICAgICAgICAgIDwvbGk+PGxpPjxhIGhyZWY9Imh0dHA6Ly9jb2Rlc29yY2VyeS5uZXQvbXV0dC9tdXR0LWdudXBnLWhvd3RvIj5BbHRlcm5hdGUgdmVyc2lvbjwvYT4NCiAgICAgICAgICAgICAgICAgICAgICAgIChmb3IgUEdQL0dQRyBuZXdiaWVzLCBieSBKdXN0aW4gUi4gTWlsbGVyKQ0KICAgICAgICAgICAgICAgICAgICA8L2xpPjxsaT48YSBocmVmPSJodHRwOi8vd3d3LmxpbnV4ZG9jLm9yZy9IT1dUTy9NdXR0LUdudVBHLVBHUC1IT1dUTy5odG1sIj5BbHRlcm5hdGUgdmVyc2lvbjwvYT4NCiAgICAgICAgICAgICAgICAgICAgICAgIChmcm9tIHRoZSBMRFApDQogICAgICAgICAgICAgICAgPC9saT48L3VsPg0KICAgICAgICAgICAgPC9saT48bGk+PGEgaHJlZj0iaHR0cDovL211dHQuYmxhY2tmaXNoLm9yZy51ay8iPk11dHQgb3ZlcnZpZXcgZm9yIG5ld2JpZXM8L2E+DQogICAgICAgICAgICAgICAgKG1haW50YWluZWQgYnkgQnJ1bm8gUG9zdGxlKQ0KICAgICAgICA8L2xpPjwvdWw+DQogICAgPC9saT48bGk+PGEgaHJlZj0iaHR0cDovL3d3dy5tdXR0Lm9yZy9kb3dubG9hZC5odG1sIj5Eb3dubG9hZGluZzwvYT4NCiAgICA8L2xpPjxsaT48YSBocmVmPSJodHRwOi8vd3d3Lm11dHQub3JnL25ld3MuaHRtbCI+TmV3czwvYT4gKHJlbGVhc2VzLCBzZWN1cml0eSBhbGVydHMsIGV0Yy4pDQogICAgPC9saT48bGk+PGEgaHJlZj0iaHR0cDovL2J1Z3MuZ3V1Zy5kZS9kYi9wYS9sbXV0dC5odG1sIj5DdXJyZW50IFJlcG9ydGVkIEJ1Z3M8L2E+DQogICAgPC9saT48bGk+PGEgaHJlZj0iI2Rpc2N1c3MiPlVzZXIgRGlzY3Vzc2lvbjwvYT4NCiAgICAgICAgKG1haWxpbmcgbGlzdHMsIG5ld3Nncm91cHMsIElSQywgZXRjLikNCiAgICA8L2xpPjxsaT48YSBocmVmPSJodHRwOi8vd3d3Lm11dHQub3JnL2xpbmtzLmh0bWwiPkxpbmtzPC9hPiAodXNlciBhZHZvY2FjeSwgaW50ZXJuYXRpb25hbCBwYWdlcywNCiAgICAgICAgdXNlciBjb250cmlidXRlZCBkb2NzLCBwYXRjaGVzLCBzY3JpcHRzLCBhZGQtb25zLCBvdGhlciByZWNvbW1lbmRlZA0KICAgICAgICBwcm9ncmFtcywgZXRjLikNCiAgICA8L2xpPjxsaT48YSBocmVmPSIjcHJlc3MiPldoYXQgT3RoZXIgUGVvcGxlIEFyZSBTYXlpbmcgQWJvdXQgTXV0dDwvYT4gKHByZXNzKQ0KPC9saT48L3VsPg0KPHA+DQo8aW1nIHNyYz0iVGhlJTIwTXV0dCUyMEUtTWFpbCUyMENsaWVudF9maWxlcy9tdXR0X2J1dHRvbi5naWYiIGFsdD0iW011dHQgTWFpbCBBZ2VudCBCdXR0b25dIiBib3JkZXI9IjAiIHdpZHRoPSI4OCIgaGVpZ2h0PSIzMSI+DQo8IS0tDQo8aW1nIHNyYz0iaW1hZ2UvbXV0dF9iYXIuZ2lmIiBhbHQ9IltNdXR0IFJ1bm5pbmcgRG9nIEJhcl0iIGJvcmRlcj0wDQp3aWR0aD0yMDAgaGVpZ2h0PTM1Pg0KLS0+DQo8L3A+PHA+PC9wPjxocj48cD4NCjwvcD48aDI+PGEgbmFtZT0iZmVhdHVyZXMiPkZlYXR1cmVzPC9hPjwvaDI+DQo8cD4NClNvbWUgb2YgTXV0dCdzIGZlYXR1cmVzIGluY2x1ZGU6DQo8L3A+PHA+DQo8L3A+PHVsPjxsaT5jb2xvciBzdXBwb3J0DQogICAgPC9saT48bGk+bWVzc2FnZSB0aHJlYWRpbmcNCiAgICA8L2xpPjxsaT5NSU1FIHN1cHBvcnQgKGluY2x1ZGluZyBSRkMyMDQ3IHN1cHBvcnQgZm9yIGVuY29kZWQgaGVhZGVycykNCiAgICA8L2xpPjxsaT5QR1AvTUlNRSAoUkZDMjAxNSkNCiAgICA8L2xpPjxsaT52YXJpb3VzIGZlYXR1cmVzIHRvIHN1cHBvcnQgbWFpbGluZyBsaXN0cywgaW5jbHVkaW5nIGxpc3QtcmVwbHkNCiAgICA8L2xpPjxsaT5hY3RpdmUgZGV2ZWxvcG1lbnQgY29tbXVuaXR5DQogICAgPC9saT48bGk+UE9QMyBzdXBwb3J0DQogICAgPC9saT48bGk+SU1BUCBzdXBwb3J0DQogICAgPC9saT48bGk+ZnVsbCBjb250cm9sIG9mIG1lc3NhZ2UgaGVhZGVycyB3aGVuIGNvbXBvc2luZw0KICAgIDwvbGk+PGxpPnN1cHBvcnQgZm9yIG11bHRpcGxlIG1haWxib3ggZm9ybWF0cyAobWJveCwgTU1ERiwgTUgsIG1haWxkaXIpDQogICAgPC9saT48bGk+PHN0cm9uZz5oaWdobHk8L3N0cm9uZz4gY3VzdG9taXphYmxlLCBpbmNsdWRpbmcga2V5YmluZGluZ3MgYW5kIG1hY3Jvcw0KICAgIDwvbGk+PGxpPmNoYW5nZSBjb25maWd1cmF0aW9uIGF1dG9tYXRpY2FsbHkgYmFzZWQgb24gcmVjaXBpZW50cywgY3VycmVudA0KICAgICAgICBmb2xkZXIsIGV0Yy4NCiAgICA8L2xpPjxsaT5zZWFyY2hlcyB1c2luZyByZWd1bGFyIGV4cHJlc3Npb25zLCBpbmNsdWRpbmcgYW4gaW50ZXJuYWwgcGF0dGVybg0KICAgICAgICBtYXRjaGluZyBsYW5ndWFnZSANCiAgICA8L2xpPjxsaT5EZWxpdmVyeSBTdGF0dXMgTm90aWZpY2F0aW9uIChEU04pIHN1cHBvcnQNCiAgICA8L2xpPjxsaT5wb3N0cG9uZSBtZXNzYWdlIGNvbXBvc2l0aW9uIGluZGVmaW5ldGx5IGZvciBsYXRlciByZWNhbGwNCiAgICA8L2xpPjxsaT5lYXNpbHkgaW5jbHVkZSBhdHRhY2htZW50cyB3aGVuIGNvbXBvc2luZywgZXZlbiBmcm9tIHRoZSBjb21tYW5kIGxpbmUNCiAgICA8L2xpPjxsaT5hYmlsaXR5IHRvIHNwZWNpZnkgYWx0ZXJuYXRlIGFkZHJlc3NlcyBmb3IgcmVjb2duaXRpb24gb2YgbWFpbCANCiAgICAgICAgZm9yd2FyZGVkIGZyb20gb3RoZXIgYWNjb3VudHMsIHdpdGggYWJpbGl0eSB0byBzZXQgdGhlIEZyb206IGhlYWRlcnMNCiAgICAgICAgb24gcmVwbGllcy9ldGMuIGFjY29yZGluZ2x5DQogICAgPC9saT48bGk+bXVsdGlwbGUgbWVzc2FnZSB0YWdnaW5nDQogICAgPC9saT48bGk+cmVwbHkgdG8gb3IgZm9yd2FyZCBtdWx0aXBsZSBtZXNzYWdlcyBhdCBvbmNlDQogICAgPC9saT48bGk+PGk+Lm1haWxyYzwvaT4gc3R5bGUgY29uZmlndXJhdGlvbiBmaWxlcw0KICAgIDwvbGk+PGxpPmVhc3kgdG8gaW5zdGFsbCAodXNlcyBHTlUgYXV0b2NvbmYpDQogICAgPC9saT48bGk+Y29tcGlsZXMgYWdhaW5zdCBlaXRoZXIgY3Vyc2VzL25jdXJzZXMgb3IgUy1sYW5nDQogICAgPC9saT48bGk+dHJhbnNsYXRpb24gaW50byBhdCBsZWFzdCAyMCBsYW5ndWFnZXMNCiAgICA8L2xpPjxsaT5zbWFsbCBhbmQgZWZmaWNpZW50DQogICAgPC9saT48bGk+PGVtPkl0J3MgZnJlZSE8L2VtPiAobm8gY29zdCBhbmQgR1BMJ2VkKQ0KPC9saT48L3VsPg0KPHA+DQo8YSBocmVmPSJodHRwOi8vd3d3Lm11dHQub3JnL3NjcmVlbnNob3RzLyI+U2NyZWVuc2hvdHM8L2E+IGRlbW9uc3RyYXRpbmcgc29tZSBvZiBNdXR0J3MNCmNhcGFiaWxpdGllcyBhcmUgYXZhaWxhYmxlLg0KPC9wPjxwPg0KVGhvdWdoIHdyaXR0ZW4gZnJvbSBzY3JhdGNoLCBNdXR0J3MgaW5pdGlhbCBpbnRlcmZhY2Ugd2FzIGJhc2VkIGxhcmdlbHkgb24gdGhlDQo8YSBocmVmPSJodHRwOi8vd3d3Lm15eGEuY29tL2VsbS5odG1sIj5FTE08L2E+IG1haWwgY2xpZW50LiAgVG8gYSBsYXJnZSBleHRlbnQsDQpNdXR0IGlzIHN0aWxsIHZlcnkgRUxNLWxpa2UgaW4gcHJlc2VudGF0aW9uIG9mIGluZm9ybWF0aW9uIGluIG1lbnVzIChhbmQgaW4NCmZhY3QsIEVMTSB1c2VycyB3aWxsIGZpbmQgaXQgcXVpdGUgcGFpbmxlc3MgdG8gc3dpdGNoIGFzIHRoZSBkZWZhdWx0IGtleQ0KYmluZGluZ3MgYXJlIGlkZW50aWNhbCkuICBBcyBkZXZlbG9wbWVudCBwcm9ncmVzc2VkLCBmZWF0dXJlcyBmb3VuZCBpbiBvdGhlcg0KcG9wdWxhciBjbGllbnRzIHN1Y2ggYXMgUElORSBhbmQgTVVTSCBoYXZlIGJlZW4gYWRkZWQsIHRoZSByZXN1bHQgYmVpbmcgYQ0KaHlicmlkLCBvciAibXV0dC4iIEF0IHByZXNlbnQsIGl0IG1vc3QgY2xvc2VseSByZXNlbWJsZXMgdGhlIDxhIGhyZWY9Imh0dHA6Ly9zcGFjZS5taXQuZWR1LyU3RWRhdmlzL3Nscm4uaHRtbCI+U0xSTjwvYT4gbmV3cyBjbGllbnQuICBNdXR0IHdhcw0Kb3JpZ2luYWxseSB3cml0dGVuIGJ5IDxhIGhyZWY9Imh0dHA6Ly93d3cuY3MuaG1jLmVkdS8lN0VtZS8iPk1pY2hhZWwgRWxraW5zPC9hPg0KYnV0IGlzIG5vdyBkZXZlbG9wZWQgYW5kIG1haW50YWluZWQgYnkgdGhlIG1lbWJlcnMgb2YgdGhlIE11dHQgZGV2ZWxvcG1lbnQNCjxhIGhyZWY9IiNkaXNjdXNzIj5tYWlsaW5nIGxpc3Q8L2E+Lg0KPC9wPjxwPg0KPGEgaHJlZj0iIyI+dG9wPC9hPg0KPC9wPjxwPjwvcD48aHI+PHA+DQo8L3A+PGgyPjxhIG5hbWU9ImRpc2N1c3MiPk11dHQgVXNlciBEaXNjdXNzaW9uPC9hPjwvaDI+DQo8cD4NCjwvcD48dWw+PGxpPk1haWxpbmcgTGlzdHMNCiAgICAgICAgPHVsPjxsaT5tdXR0LWFubm91bmNlQG11dHQub3JnIC0tIEFubm91bmNlbWVudHMuDQogICAgICAgICAgICA8L2xpPjxsaT48YSBocmVmPSJtYWlsdG86bXV0dC11c2Vyc0BtdXR0Lm9yZyI+bXV0dC11c2Vyc0BtdXR0Lm9yZzwvYT4NCiAgICAgICAgICAgICAgICAtLSBHZW5lcmFsIERpc2N1c3Npb24uDQogICAgICAgICAgICA8L2xpPjxsaT48YSBocmVmPSJodHRwOi8vbWFyYy50aGVhaW1zZ3JvdXAuY29tLz9sPW11dHQtdXNlcnMiPm11dHQtdXNlcnMgQXJjaGl2ZTwvYT4NCiAgICAgICAgICAgICAgICAoQUlNUykNCiAgICAgICAgICAgIDwvbGk+PGxpPjxhIGhyZWY9Imh0dHA6Ly9tYWlsLWFyY2hpdmUuY29tL211dHQtdXNlcnMlNDBtdXR0Lm9yZy8iPm11dHQtdXNlcnMgQXJjaGl2ZTwvYT4NCiAgICAgICAgICAgICAgICAobWFpbC1hcmNoaXZlLmNvbSkNCiAgICAgICAgICAgIDwvbGk+PGxpPjxhIGhyZWY9Imh0dHA6Ly93d3cuZWdyb3Vwcy5jb20vZ3JvdXAvbXV0dC11c2Vycy8iPm11dHQtdXNlcnMgQXJjaGl2ZTwvYT4NCiAgICAgICAgICAgICAgICAoRWdyb3VwcykNCiAgICAgICAgICAgIDwvbGk+PGxpPjxhIGhyZWY9Im1haWx0bzptdXR0LWRldkBtdXR0Lm9yZyI+bXV0dC1kZXZAbXV0dC5vcmc8L2E+IC0tDQogICAgICAgICAgICAgICAgRGV2ZWxvcG1lbnQgQ29tbXVuaXR5Lg0KICAgICAgICAgICAgPC9saT48bGk+PGEgaHJlZj0iaHR0cDovL21hcmMudGhlYWltc2dyb3VwLmNvbS8/bD1tdXR0LWRldiI+bXV0dC1kZXYgQXJjaGl2ZTwvYT4NCiAgICAgICAgICAgICAgICAoQUlNUykNCiAgICAgICAgICAgIDwvbGk+PGxpPjxhIGhyZWY9Imh0dHA6Ly93d3cuZWdyb3Vwcy5jb20vZ3JvdXAvbXV0dC1kZXYvIj5tdXR0LWRldiBBcmNoaXZlPC9hPg0KICAgICAgICAgICAgICAgIChFZ3JvdXBzKQ0KICAgICAgICAgICAgPC9saT48bGk+PGEgaHJlZj0ibWFpbHRvOm11dHQtcG9AbXV0dC5vcmciPm11dHQtcG9AbXV0dC5vcmc8L2E+IC0tDQogICAgICAgICAgICAgICAgVHJhbnNsYXRpb24gSXNzdWVzLg0KICAgICAgICAgICAgPC9saT48bGk+PGEgaHJlZj0iaHR0cDovL3d3dy5tdXR0Lm9yZy9tYWlsLWxpc3RzLmh0bWwiPlN1YnNjcmliZSB0byB0aGUgbGlzdHMgdGhyb3VnaCB0aGlzIHdlYiBzaXRlPC9hPi4NCiAgICAgICAgICAgICAgICAoWW91IG5lZWQgdG8gYmUgc3Vic2NyaWJlZCB0byB0aGUgbGlzdHMgdG8gcG9zdCB0byB0aGVtLikNCiAgICAgICAgPC9saT48L3VsPg0KICAgICAgICA8cD4NCiAgICA8L3A+PC9saT48bGk+TmV3c2dyb3VwDQogICAgICAgIDx1bD48bGk+PGEgaHJlZj0ibmV3czovL2NvbXAubWFpbC5tdXR0LyI+Y29tcC5tYWlsLm11dHQ8L2E+DQogICAgICAgICAgICA8L2xpPjxsaT48YSBocmVmPSJodHRwOi8vd3d3LmRlamEuY29tL2RucXVlcnkueHA/cXVlcnk9JTdFZyUyMGNvbXAubWFpbC5tdXR0Ij5jb21wLm1haWwubXV0dCBBcmNoaXZlPC9hPg0KICAgICAgICAgICAgICAgIChEZWphLmNvbSkNCiAgICAgICAgPC9saT48L3VsPg0KICAgICAgICA8cD4NCiAgICA8L3A+PC9saT48bGk+SVJDIC0tIENoYW5uZWwgI211dHQgb24NCiAgICAgICAgPGEgaHJlZj0iaHR0cDovL3d3dy5vcGVucHJvamVjdHMubmV0L2lyY19zZXJ2ZXJzLnNodG1sLyI+aXJjLm9wZW5wcm9qZWN0cy5uZXQ8L2E+DQo8L2xpPjwvdWw+DQo8cD4NCjxhIGhyZWY9IiMiPnRvcDwvYT4NCjwvcD48cD48L3A+PGhyPjxwPg0KPC9wPjxoMj48YSBuYW1lPSJwcmVzcyI+UHJlc3MgQWJvdXQgTXV0dDwvYT48L2gyPg0KPHA+DQo8L3A+PHVsPjxsaT48YSBocmVmPSJodHRwOi8vd3d3LmRldnNoZWQuY29tL1NlcnZlcl9TaWRlL0FkbWluaXN0cmF0aW9uL011dHQvcGFnZTEuaHRtbCI+QSBNYW4gQW5kIEhpcyBNdXR0PC9hPg0KICAgICAgICAtLSA8YSBocmVmPSJodHRwOi8vd3d3LmRldnNoZWQuY29tLyI+RGV2ZWxvcGVyIFNoZWQ8L2E+DQogICAgPC9saT48bGk+PGEgaHJlZj0iaHR0cDovL3d3dy5saW51eG5vdmljZS5vcmcvbWFpbl9zb2Z0d2FyZS5waHAzP1ZJRVc9VklFVyZhbXA7dF9pZD0xNDYiPk11dHQ6IEFuIGUtbWFpbCB1c2VyJ3MgYmVzdCBmcmllbmQgLS0gUGFydCBPbmU8L2E+DQogICAgICAgIC0tIDxhIGhyZWY9Imh0dHA6Ly93d3cubGludXhub3ZpY2Uub3JnLyI+TGludXhOb3ZpY2Uub3JnPC9hPi4NCiAgICAgICAgPGEgaHJlZj0iaHR0cDovL3d3dy5saW51eG5vdmljZS5vcmcvbWFpbl9zb2Z0d2FyZS5waHAzP1ZJRVc9VklFVyZhbXA7dF9pZD0xNjQiPlBhcnQgVHdvPC9hPg0KICAgIDwvbGk+PGxpPjxhIGhyZWY9Imh0dHA6Ly93d3cubGludXhjYXJlLmNvbS92aWV3cG9pbnRzL2FwLW9mLXRoZS13ay8wMy0zMS0wMC5lcGwiPk11dHQ8L2E+IC0tIE11dHQgd2FzIHRoZSBhcHAgb2YgdGhlIHdlZWsgYXQNCiAgICAgICAgPGEgaHJlZj0iaHR0cDovL3d3dy5saW51eGNhcmUuY29tLyI+TGludXhjYXJlPC9hPi4NCiAgICA8L2xpPjxsaT48YSBocmVmPSJodHRwOi8vd3d3LmxpbnV4d29ybGQuY29tL2xpbnV4d29ybGQvbHctMTk5OS0xMi9sdy0xMi1tdXR0Lmh0bWwiPk1hbidzIEJlc3QgRnJpZW5kPC9hPg0KICAgICAgICAtLSA8YSBocmVmPSJodHRwOi8vd3d3LmxpbnV4d29ybGQuY29tLyI+TGludXggV29ybGQ8L2E+Lg0KICAgIDwvbGk+PGxpPjxhIGhyZWY9Imh0dHA6Ly93d3cuMzJiaXRzb25saW5lLmNvbS9hcnRpY2xlLnBocDM/ZmlsZT1pc3N1ZXMvMTk5ODEyL211dHQmYW1wO3BhZ2U9MSI+TXV0dDogQSBVbml4IE1haWxlciBmb3IgRXhwZXJ0czwvYT4NCiAgICAgICAgLS0gYW4gYXJ0aWNsZSBmcm9tDQogICAgICAgIDxhIGhyZWY9Imh0dHA6Ly93d3cuMzJiaXRzb25saW5lLmNvbS8iPjMyQml0c09ubGluZTwvYT4uDQogICAgPC9saT48bGk+PGEgaHJlZj0iaHR0cDovL3d3dy5zc2MuY29tL2xnL2lzc3VlMTQvbXV0dC5odG1sIj5UaGUgTXV0dCBNYWlsZXI8L2E+DQogICAgICAgIC0tIGFuIChvbGQpIGFydGljbGUgZnJvbSB0aGUNCiAgICAgICAgPGEgaHJlZj0iaHR0cDovL3d3dy5zc2MuY29tL2xnLyI+TGludXggR2F6ZXR0ZTwvYT4uDQo8L2xpPjwvdWw+DQo8cD4NCjxhIGhyZWY9IiMiPnRvcDwvYT4NCjwvcD48cD48L3A+PGhyPg0KPGFkZHJlc3M+TGFzdCB1cGRhdGVkIG9uIE1hcmNoIDEzLCAyMDAyIGJ5DQogIDxhIGhyZWY9Imh0dHA6Ly9qYmxvc3Nlci5maXJpbm4ub3JnLyI+SmVyZW15IEJsb3NzZXI8L2E+Lg0KPC9hZGRyZXNzPg0KVVJMOiZsdDtodHRwOi8vd3d3Lm11dHQub3JnL2luZGV4Lmh0bWwmZ3Q7PGJyPg0KQ29weXJpZ2h0IKkgMTk5Ni05IE1pY2hhZWwgUi4gRWxraW5zLiAgQWxsIHJpZ2h0cyByZXNlcnZlZC48YnI+DQpDb3B5cmlnaHQgqSAxOTk5LTIwMDIgSmVyZW15IEJsb3NzZXIuICBBbGwgcmlnaHRzIHJlc2VydmVkLg0KPGhyPg0KPCEtLSBCRUdJTiBIT1NURUQgQlkgLS0+DQo8YSBocmVmPSJodHRwOi8vd3d3LmdibmV0Lm5ldC8iPjxpbWcgc3JjPSJUaGUlMjBNdXR0JTIwRS1NYWlsJTIwQ2xpZW50X2ZpbGVzL2dibmV0dGVrX3cxMDBfdHJhbnNiLmdpZiIgd2lkdGg9IjEwMCIgaGVpZ2h0PSI2NCIgYWx0PSJHQk5ldC9OZXRUZWsiIGJvcmRlcj0iMCIgdnNwYWNlPSIxMCI+PC9hPg0KPGJyPjxzbWFsbD5ob3N0ZWQgYnk8YnI+DQo8YSBocmVmPSJodHRwOi8vd3d3LmdibmV0Lm5ldC8iPkdCTmV0L05ldFRlazwvYT48L3NtYWxsPg0KPCEtLSBFTkQgSE9TVEVEIEJZIC0tPg0KDQo8L2JvZHk+PC9odG1sPg==", + "mimeType": "text/html", + "charset": "iso-8859-15", + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "From: Lars Hecking \nTo: mutt-dev@mutt.org\nSubject: MIME handling bug demo\nMime-Version: 1.0\nContent-Type: multipart/mixed; boundary=\"ReaqsoxgOBHFXBhH\"\nContent-Disposition: inline\nContent-Transfer-Encoding: 8bit\nX-Mutt-Fcc: \nStatus: RO\nContent-Length: 11138\nLines: 226", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/2002_06_12_doublebound.msg b/packages/node-mimimi/test/mimetools-testmsgs/2002_06_12_doublebound.msg new file mode 100644 index 00000000000..c435713d166 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/2002_06_12_doublebound.msg @@ -0,0 +1,240 @@ +From: Lars Hecking +To: mutt-dev@mutt.org +Subject: MIME handling bug demo +Mime-Version: 1.0 +Content-Type: multipart/mixed; boundary="ReaqsoxgOBHFXBhH" +Content-Disposition: inline +Content-Transfer-Encoding: 8bit +X-Mutt-Fcc: +Status: RO +Content-Length: 11138 +Lines: 226 + + +--ReaqsoxgOBHFXBhH +Content-Type: text/plain; charset=us-ascii +Content-Disposition: inline + + +-- +The plot was designed in a light vein that somehow became varicose. + -- David Lardner + +--ReaqsoxgOBHFXBhH +--ReaqsoxgOBHFXBhH +Content-Type: text/html; charset=iso-8859-15 +Content-Disposition: attachment; filename="The Mutt E-Mail Client.html" +Content-Transfer-Encoding: 8bit + +The Mutt E-Mail Client + + + + + +
+

The Mutt E-Mail Client

+

+ +"All mail clients suck. This one just sucks less." -me, circa 1995 + +

+

+mirrors +


+

Latest News

+ +

+Mutt 1.3.28 was released on March 13, 2002. This is a release +candidate for 1.4. +

+Mutt 1.2.5.1 and 1.3.25 were released on January 1, 2002. These releases both +fix a security hole which can be remotely exploited. For more +information, see the +release announcement. +

+Mutt 1.3.24 was released on November 30, 2001. This is a beta +development release toward the next stable public release version. There have +been several large changes since 1.2.x, so please check the +recent changes page. +

+The Mutt CVS server has moved from ftp.guug.de to ftp.mutt.org. +

+more news +


+

General Info

+

+Mutt is a small but very powerful text-based mail client for Unix operating +systems. The latest public release version is 1.3.28, which is a +release candidate for 1.4. The current stable public release +version is 1.2.5.1. For more information, see the following: +

+

+

+[Mutt Mail Agent Button] + +


+

Features

+

+Some of Mutt's features include: +

+

  • color support +
  • message threading +
  • MIME support (including RFC2047 support for encoded headers) +
  • PGP/MIME (RFC2015) +
  • various features to support mailing lists, including list-reply +
  • active development community +
  • POP3 support +
  • IMAP support +
  • full control of message headers when composing +
  • support for multiple mailbox formats (mbox, MMDF, MH, maildir) +
  • highly customizable, including keybindings and macros +
  • change configuration automatically based on recipients, current + folder, etc. +
  • searches using regular expressions, including an internal pattern + matching language +
  • Delivery Status Notification (DSN) support +
  • postpone message composition indefinetly for later recall +
  • easily include attachments when composing, even from the command line +
  • ability to specify alternate addresses for recognition of mail + forwarded from other accounts, with ability to set the From: headers + on replies/etc. accordingly +
  • multiple message tagging +
  • reply to or forward multiple messages at once +
  • .mailrc style configuration files +
  • easy to install (uses GNU autoconf) +
  • compiles against either curses/ncurses or S-lang +
  • translation into at least 20 languages +
  • small and efficient +
  • It's free! (no cost and GPL'ed) +
+

+Screenshots demonstrating some of Mutt's +capabilities are available. +

+Though written from scratch, Mutt's initial interface was based largely on the +ELM mail client. To a large extent, +Mutt is still very ELM-like in presentation of information in menus (and in +fact, ELM users will find it quite painless to switch as the default key +bindings are identical). As development progressed, features found in other +popular clients such as PINE and MUSH have been added, the result being a +hybrid, or "mutt." At present, it most closely resembles the SLRN news client. Mutt was +originally written by Michael Elkins +but is now developed and maintained by the members of the Mutt development +mailing list. +

+top +


+

Mutt User Discussion

+

+

+

+top +


+

Press About Mutt

+

+

+

+top +


+
Last updated on March 13, 2002 by + Jeremy Blosser. +
+URL:<http://www.mutt.org/index.html>
+Copyright © 1996-9 Michael R. Elkins. All rights reserved.
+Copyright © 1999-2002 Jeremy Blosser. All rights reserved. +
+ +GBNet/NetTek +
hosted by
+GBNet/NetTek
+ + + +--ReaqsoxgOBHFXBhH-- + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/README.md b/packages/node-mimimi/test/mimetools-testmsgs/README.md new file mode 100644 index 00000000000..9ae3324fc65 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/README.md @@ -0,0 +1 @@ +MIME test messages copied from tutadb mimetools-testmsgs. \ No newline at end of file diff --git a/packages/node-mimimi/test/mimetools-testmsgs/ak-0696-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/ak-0696-expected.json new file mode 100644 index 00000000000..ef80c798f28 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/ak-0696-expected.json @@ -0,0 +1,55 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "------------70522FC73543", + "alternativeBoundary": null, + "sender": { + "name": "Juergen Specht", + "mailAddress": "specht@kulturbox.de", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "andreas.koenig@mind.de", + "valid": true + }, + { + "name": "", + "mailAddress": "kun@pop.combox.de", + "valid": true + }, + { + "name": "", + "mailAddress": "101762.2307@compuserve.com", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 835252517000, + "subject": "[Fwd: Re: 34Mbit/s Netz]", + "plainBodyText": "-- \r\nJuergen Specht - KULTURBOX\r\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "Re_ 34Mbit_s Netz.eml", + "data": "WC1QT1AzLVJjcHQ6IHNwZWNodEB0cmFjaGVhDQpSZXR1cm4tUGF0aDogaGVybWVzDQpSZWNlaXZlZDogKGZyb20gaGVybWVzQGxvY2FsaG9zdCkgYnkga3VsdHVyYm94Lm5ldG1ieC5kZSAoOC43LjEvOC43LjEpIGlkIFNBQTA0NTEzIGZvciBzcGVjaHQ7IFdlZCwgMTkgSnVuIDE5OTYgMTg6MzA6MTIgKzAyMDANClJlY2VpdmVkOiBieSBuZXRtYngubmV0bWJ4LmRlICgvXD09L1wgU21haWwzLjEuMjguMSkNCgkgIGZyb20gbWFpbC5jcy50dS1iZXJsaW4uZGUgd2l0aCBzbXRwDQoJICBpZCA8bTB1V1ByTy0wMDA0d3BDPjsgV2VkLCAxOSBKdW4gOTYgMTg6MTIgTUVTDQpSZWNlaXZlZDogKGZyb20gbm9ib2R5QGxvY2FsaG9zdCkgYnkgbWFpbC5jcy50dS1iZXJsaW4uZGUgKDguNi4xMi84LjYuMTIpIGlkIFNBQTEyNDEzOyBXZWQsIDE5IEp1biAxOTk2IDE4OjI2OjI4ICswMjAwDQpSZXNlbnQtRGF0ZTogV2VkLCAxOSBKdW4gMTk5NiAxODoyNjoyOCArMDIwMA0KUmVzZW50LU1lc3NhZ2UtSWQ6IDwxOTk2MDYxOTE2MjYuU0FBMTI0MTNAbWFpbC5jcy50dS1iZXJsaW4uZGU+DQpSZXNlbnQtRnJvbTogbm9ib2R5QGNzLnR1LWJlcmxpbi5kZQ0KUmVzZW50LVRvOiBrdWx0dXJAa3VsdHVyYm94Lm5ldG1ieC5kZQ0KUmVjZWl2ZWQ6IGZyb20gZ2F0ZWtlZXBlci50ZWxla29tLmRlIChbMTk0LjI1LjE1LjExXSkgYnkgbWFpbC5jcy50dS1iZXJsaW4uZGUgKDguNi4xMi84LjYuMTIpIHdpdGggU01UUCBpZCBTQUExMTY3OCBmb3IgPHNwZWNodEBrdWx0dXJib3guZGU+OyBXZWQsIDE5IEp1biAxOTk2IDE4OjExOjI5ICswMjAwDQpSZWNlaXZlZDogZnJvbSBVTE0wMi5tbmgudGVsZWtvbS5kZSBieSBnYXRla2VlcGVyLnRlbGVrb20uZGU7ICg1LjY1djMuMC8xLjEuOC4yLzAyQXVnOTUtMDEzMlBNKQ0KCWlkIEFBMDEzNzY7IFdlZCwgMTkgSnVuIDE5OTYgMTg6MTE6MjcgKzAyMDANClJlY2VpdmVkOiBmcm9tIHVsbTAyLm1uaC50ZWxla29tLmRlIChkZXVzY2hsZUBtbmgudGVsZWtvbS5kZSkgYnkgVUxNMDIubW5oLnRlbGVrb20uZGUgKDguNi4xMC8zKSB3aXRoIFNNVFAgaWQgU0FBMzA2ODAgZm9yIDxzcGVjaHRAa3VsdHVyYm94LmRlPjsgV2VkLCAxOSBKdW4gMTk5NiAxODoxNDo0MCBHTVQNCk1lc3NhZ2UtSWQ6IDwxOTk2MDYxOTE4MTQuU0FBMzA2ODBAVUxNMDIubW5oLnRlbGVrb20uZGU+DQpYLVNlbmRlcjogZGV1c2NobGVAdWxtMDIubW5oLnRlbGVrb20uZGUNClgtTWFpbGVyOiBXaW5kb3dzIEV1ZG9yYSBWZXJzaW9uIDEuNC40DQpNaW1lLVZlcnNpb246IDEuMA0KQ29udGVudC1UeXBlOiB0ZXh0L3BsYWluOyBjaGFyc2V0PSJpc28tODg1OS0xIg0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogcXVvdGVkLXByaW50YWJsZQ0KRGF0ZTogV2VkLCAxOSBKdW4gMTk5NiAxODoxMjowMiArMDIwMA0KVG86IEp1ZXJnZW4gU3BlY2h0IDxzcGVjaHRAa3VsdHVyYm94LmRlPg0KRnJvbTogZGV1c2NobGVAbW5oLnRlbGVrb20uZGUgKEd1ZW50ZXIgRGV1c2NobGUpDQpTdWJqZWN0OiBSZTogMzRNYml0L3MgTmV0eg0KWC1Nb3ppbGxhLVN0YXR1czogMDAxMQ0KDQpIYWxsbyBIZXJyIFNwZWNodCwNCmVudHNjaHVsZGlnZW4gU2llIHZvcmFiLCBkYXNzIGljaCBJaG5lbiBuaWNodCB0ZWxlZm9uaXNjaCB6dXIgVmVyZnVlZ3VuZw0Kc3RlaGUsIGljaCBQcmFlc2VudGF0aW9uZW4gZ2VoYWx0ZW4vIG5vY2ggenUgaGFsdGVuIHVuZCB2aWVsZQ0KS3VuZGVubmFjaGZyYWdlbiB6dSBwcm9qZWt0aWVyZW4uIE5hY2ggSW5mb3JtYXRpb25lbiBkZXMgUHJvZHVrdC1NYW5hZ2Vycw0KVGVtbWUgc3RlaHQgZGVyIFBPUCBzY2hvbiB6dXIgVmVyZj1GQ2d1bmchIFN0YW5kb3J0OiB2b3JhdXNzaWNodGxpY2g6DQpXaW50ZXJmZWxkc3RyLiAyMSwgMTA3ODEgQmVybGluLg0KRGVyIFBPUCBoYXQgenVyIFplaXQgZGlyZWt0ZSAzNE0tQW5iaW5kdW5nZW4genUgZm9sZ2VuZGVuIE9ydGVuOiBSb3N0b2NrLA0KSGFtYnVyZywgSGFubm92ZXIgJiBMZWlwemlnLiA0IHdlaXRlcmUgd2VyZGVuIGluIGt1ZXJ6ZSBpbiBCZXRyaWViIGdlaGVuLg0KRGFtaXQgaGFiZW4gU2llIGVpbmVuIEJlc29uZGVyZW4gU2ljaGVyaGVpdHNzdGFuZGFyZCB2ZXJmdWVnYmFyIQ0KS29udGFrdCBtdWVzc2VuIFNpZSB1ZWJlciBJaHJlIG9lcmx0bGljaGUgVmVydHJpZWJzZWluaGVpdCBhdWZuZWhtZW46DQplbnR3ZWRlciBkZW4gR2VzY2hhZWZ0cy1LdW5kZW4tVmVydHJpZWIgb2RlciBkYXMgR3Jvc3NLdW5kZW5NYW5hZ2VtZW50Lg0KRGllc2UgVmVydHJpZWJzZWluaGVpdGVuIGdyZWlmZW4gYXVmIGRlbiBvZXJ0bGljaGVuIFRlY2huaXNjaGVuDQpWZXJ0cmllYnMtU3VwcG9ydCB6dS4gRGllIEluZm9ybWF0aW9uZW4gd2VyZGVuIHVlYmVyIFRWUyB6dXIgVmVydHJpZWJzZWloZWl0DQpnZWdlYmVuIHVuZCBkYW5uIHp1IElobmVuLg0KIFNpZSBiZW5vZXRpZ2VuIGVpbmUgU3RhbmRsZWl0dW5nIHZvbiBJaHJlciBMb2thdGlvbiB6dW0gSW50ZXJuZXQtUE9QDQpVZWJlcmdhYmVwdW5rdCB6dSBJaHJlbSBJbmZvLVNlcnZlciBpc3QgZWluIENJU0NPIDEwMDAtUm91dGVyLiBEYW5uIHphaGxlbg0KU2llIG5lYmVuIGRlbiBtb25hdGxpY2hlbiBLb3N0ZW4gZnVlciBkaWUgU3RhbmRsZWl0dW5nIGRpZSBLb3N0ZW4gZnVlciBkZW4NCkludGVybmV0LVp1Z2FuZzogekIgYmVpIDY0azogMTUwMERNIGJlaSAyR0J5dGUgRnJlaXZvbHVtZW4uIDEyOEs6IDMwMDAgRE0NCmJlaSA1IEdCIEZyZWl2b2x1bWVuICYgMk06IDMwLjAwMCBETSBiZWkgNTBHQiBGcmVpdm9sdW1lbi4NCkZyZXVuZGxpY2hlIEdydWVzc2U9MjANCkd1ZW50ZXIgRGV1c2NobGUNCg0KDQo+U2VociBnZWVocnRlciBIZXJyIERldXNjaGxlLA0KPlNpZSBzaW5kIG1pciB2b24gSGVycm4gTWV5ZW5kcmllc2NoIGVtcGZvaGxlbiB3b3JkZW4uDQo+SWNoIHZlcnN1Y2hlIEluZm9ybWF0aW9uZW4gdWViZXIgZGFzIFQtZWlnZW5lIDM0TWJpdC9zIE5ldHogdW5kIGRlbj0yMA0KPmxva2FsZW4gUG9wLUJlcmxpbiByYXVzenVmaW5kZW4sIGJ6dy4gd2FzIGVpbiBBbnNjaGx1c3Mga29zdGV0IHVuZD0yMA0KPndvIG1hbiBpaG4gaGVyYmVrb21tdC4gTGF1dCBIZXJybiBTY2huaWNrIGluIEJlcmxpbiBnaWJ0IGVzIGRlbj0yMA0KPlQtUG9wIG5pY2h0LCBsYXV0IFRyYWNlcm91dGUgdm9uIEhlcnJuIE1leWVuZHJpZXNjaCBzZWhyd29obC4gQXVjaD0yMA0KPmlzdCBkaWVzIE5ldHogaW4gZGVyIElYIHZvbSBNYWkgOTYgZXJ3YWVobnQuDQo+S29lbm5lbiBTaWUgbWlyIGhlbGZlbj8NCj4NCj5NZkcNCj4tLT0yMA0KPkp1ZXJnZW4gU3BlY2h0IC0gS1VMVFVSQk9YDQo+DQo+DQoNCj0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0NCj0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0NCj0zRA0KRGlwbC4tSW5nLiAgR3VlbnRlciAgICAgRCBFIFUgUyBDIEggTCBFDQpEZXV0c2NoZSBUZWxla29tIEFHICAgICBOaWVkZXJsYXNzdW5nIDMgSGFubm92ZXINCkdyb3NzS3VuZGVuTWFuYWdlbWVudCAtIFRlY2huLiBWZXJ0cmllYnMtU3VwcG9ydDoNClRlYW0tTGVpdGVyICAgICAgICAgICAgIEludGVybmV0IE9ubGluZS1EaWVuc3RlDQotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NCkdyS00tVFZTLUlPRCAgICAgICAgICAgVGVsOiArNDktNTExLTMzMy0yNzcyDQpWYWhyZW53YWxkZXItU3RyLiAyNDUgIEZBWDogKzQ5LTUxMS0zMzMtMjc1MQ0KMzAxNzkgSGFubm92ZXIgICAgICAgZU1haWw6IGRldXNjaGxlQG1uaC50ZWxla29tLmRlPTIwDQo9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9DQo9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9DQo9M0QNCg0KDQoNCg==", + "mimeType": "message/rfc822", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "Date: Thu, 20 Jun 1996 08:35:17 +0200\nFrom: Juergen Specht \nOrganization: KULTURBOX\nX-Mailer: Mozilla 2.02 (WinNT; I)\nMIME-Version: 1.0\nTo: andreas.koenig@mind.de, kun@pop.combox.de, 101762.2307@compuserve.com\nSubject: [Fwd: Re: 34Mbit/s Netz]\nContent-Type: MULTIPART/MIXED; boundary=\"------------70522FC73543\"\nX-Filter: mailagent [version 3.0 PL44] for k@.in-berlin.de", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/ak-0696.msg b/packages/node-mimimi/test/mimetools-testmsgs/ak-0696.msg new file mode 100644 index 00000000000..e6c791353fb --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/ak-0696.msg @@ -0,0 +1,111 @@ +Date: Thu, 20 Jun 1996 08:35:17 +0200 +From: Juergen Specht +Organization: KULTURBOX +X-Mailer: Mozilla 2.02 (WinNT; I) +MIME-Version: 1.0 +To: andreas.koenig@mind.de, kun@pop.combox.de, 101762.2307@compuserve.com +Subject: [Fwd: Re: 34Mbit/s Netz] +Content-Type: MULTIPART/MIXED; boundary="------------70522FC73543" +X-Filter: mailagent [version 3.0 PL44] for k@.in-berlin.de + +This is a multi-part message in MIME format. + +--------------70522FC73543 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +-- +Juergen Specht - KULTURBOX + +--------------70522FC73543 +Content-Type: message/rfc822 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +X-POP3-Rcpt: specht@trachea +Return-Path: hermes +Received: (from hermes@localhost) by kulturbox.netmbx.de (8.7.1/8.7.1) id SAA04513 for specht; Wed, 19 Jun 1996 18:30:12 +0200 +Received: by netmbx.netmbx.de (/\==/\ Smail3.1.28.1) + from mail.cs.tu-berlin.de with smtp + id ; Wed, 19 Jun 96 18:12 MES +Received: (from nobody@localhost) by mail.cs.tu-berlin.de (8.6.12/8.6.12) id SAA12413; Wed, 19 Jun 1996 18:26:28 +0200 +Resent-Date: Wed, 19 Jun 1996 18:26:28 +0200 +Resent-Message-Id: <199606191626.SAA12413@mail.cs.tu-berlin.de> +Resent-From: nobody@cs.tu-berlin.de +Resent-To: kultur@kulturbox.netmbx.de +Received: from gatekeeper.telekom.de ([194.25.15.11]) by mail.cs.tu-berlin.de (8.6.12/8.6.12) with SMTP id SAA11678 for ; Wed, 19 Jun 1996 18:11:29 +0200 +Received: from ULM02.mnh.telekom.de by gatekeeper.telekom.de; (5.65v3.0/1.1.8.2/02Aug95-0132PM) + id AA01376; Wed, 19 Jun 1996 18:11:27 +0200 +Received: from ulm02.mnh.telekom.de (deuschle@mnh.telekom.de) by ULM02.mnh.telekom.de (8.6.10/3) with SMTP id SAA30680 for ; Wed, 19 Jun 1996 18:14:40 GMT +Message-Id: <199606191814.SAA30680@ULM02.mnh.telekom.de> +X-Sender: deuschle@ulm02.mnh.telekom.de +X-Mailer: Windows Eudora Version 1.4.4 +Mime-Version: 1.0 +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable +Date: Wed, 19 Jun 1996 18:12:02 +0200 +To: Juergen Specht +From: deuschle@mnh.telekom.de (Guenter Deuschle) +Subject: Re: 34Mbit/s Netz +X-Mozilla-Status: 0011 + +Hallo Herr Specht, +entschuldigen Sie vorab, dass ich Ihnen nicht telefonisch zur Verfuegung +stehe, ich Praesentationen gehalten/ noch zu halten und viele +Kundennachfragen zu projektieren. Nach Informationen des Produkt-Managers +Temme steht der POP schon zur Verf=FCgung! Standort: voraussichtlich: +Winterfeldstr. 21, 10781 Berlin. +Der POP hat zur Zeit direkte 34M-Anbindungen zu folgenden Orten: Rostock, +Hamburg, Hannover & Leipzig. 4 weitere werden in kuerze in Betrieb gehen. +Damit haben Sie einen Besonderen Sicherheitsstandard verfuegbar! +Kontakt muessen Sie ueber Ihre oerltliche Vertriebseinheit aufnehmen: +entweder den Geschaefts-Kunden-Vertrieb oder das GrossKundenManagement. +Diese Vertriebseinheiten greifen auf den oertlichen Technischen +Vertriebs-Support zu. Die Informationen werden ueber TVS zur Vertriebseiheit +gegeben und dann zu Ihnen. + Sie benoetigen eine Standleitung von Ihrer Lokation zum Internet-POP +Uebergabepunkt zu Ihrem Info-Server ist ein CISCO 1000-Router. Dann zahlen +Sie neben den monatlichen Kosten fuer die Standleitung die Kosten fuer den +Internet-Zugang: zB bei 64k: 1500DM bei 2GByte Freivolumen. 128K: 3000 DM +bei 5 GB Freivolumen & 2M: 30.000 DM bei 50GB Freivolumen. +Freundliche Gruesse=20 +Guenter Deuschle + + +>Sehr geehrter Herr Deuschle, +>Sie sind mir von Herrn Meyendriesch empfohlen worden. +>Ich versuche Informationen ueber das T-eigene 34Mbit/s Netz und den=20 +>lokalen Pop-Berlin rauszufinden, bzw. was ein Anschluss kostet und=20 +>wo man ihn herbekommt. Laut Herrn Schnick in Berlin gibt es den=20 +>T-Pop nicht, laut Traceroute von Herrn Meyendriesch sehrwohl. Auch=20 +>ist dies Netz in der IX vom Mai 96 erwaehnt. +>Koennen Sie mir helfen? +> +>MfG +>--=20 +>Juergen Specht - KULTURBOX +> +> + +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= +=3D +Dipl.-Ing. Guenter D E U S C H L E +Deutsche Telekom AG Niederlassung 3 Hannover +GrossKundenManagement - Techn. Vertriebs-Support: +Team-Leiter Internet Online-Dienste +--------------------------------------------------- +GrKM-TVS-IOD Tel: +49-511-333-2772 +Vahrenwalder-Str. 245 FAX: +49-511-333-2751 +30179 Hannover eMail: deuschle@mnh.telekom.de=20 +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= +=3D + + + + +--------------70522FC73543-- + + + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/attachment-filename-encoding-Latin1-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/attachment-filename-encoding-Latin1-expected.json new file mode 100644 index 00000000000..8f22a356640 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/attachment-filename-encoding-Latin1-expected.json @@ -0,0 +1,39 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "------------050706070100080203090004", + "alternativeBoundary": null, + "sender": { + "name": "", + "mailAddress": "", + "valid": false + }, + "toRecipients": [], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "", + "plainBodyText": "Attachment Test\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "attachment.Àâü", + "data": "VGVzdAo=", + "mimeType": "text/plain", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "MIME-Version: 1.0\nContent-Type: multipart/mixed;\r\n boundary=\"------------050706070100080203090004\"", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/attachment-filename-encoding-Latin1.msg b/packages/node-mimimi/test/mimetools-testmsgs/attachment-filename-encoding-Latin1.msg new file mode 100644 index 00000000000..eb8dc7ee001 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/attachment-filename-encoding-Latin1.msg @@ -0,0 +1,20 @@ +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------050706070100080203090004" + +This is a multi-part message in MIME format. +--------------050706070100080203090004 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 7bit + +Attachment Test + +--------------050706070100080203090004 +Content-Type: text/plain; + name="=?ISO-8859-1?B?YXR0YWNobWVudC7k9vw=?=" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename*=ISO-8859-1''%61%74%74%61%63%68%6D%65%6E%74%2E%E4%F6%FC + +VGVzdAo= +--------------050706070100080203090004-- diff --git a/packages/node-mimimi/test/mimetools-testmsgs/attachment-filename-encoding-UTF8-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/attachment-filename-encoding-UTF8-expected.json new file mode 100644 index 00000000000..8f22a356640 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/attachment-filename-encoding-UTF8-expected.json @@ -0,0 +1,39 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "------------050706070100080203090004", + "alternativeBoundary": null, + "sender": { + "name": "", + "mailAddress": "", + "valid": false + }, + "toRecipients": [], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "", + "plainBodyText": "Attachment Test\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "attachment.Àâü", + "data": "VGVzdAo=", + "mimeType": "text/plain", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "MIME-Version: 1.0\nContent-Type: multipart/mixed;\r\n boundary=\"------------050706070100080203090004\"", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/attachment-filename-encoding-UTF8.msg b/packages/node-mimimi/test/mimetools-testmsgs/attachment-filename-encoding-UTF8.msg new file mode 100644 index 00000000000..53f8d397307 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/attachment-filename-encoding-UTF8.msg @@ -0,0 +1,20 @@ +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------050706070100080203090004" + +This is a multi-part message in MIME format. +--------------050706070100080203090004 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 7bit + +Attachment Test + +--------------050706070100080203090004 +Content-Type: text/plain; + name="=?UTF-8?B?YXR0YWNobWVudC7DpMO2w7w=?=" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename*=UTF-8''%61%74%74%61%63%68%6D%65%6E%74%2E%C3%A4%C3%B6%C3%BC + +VGVzdAo= +--------------050706070100080203090004-- diff --git a/packages/node-mimimi/test/mimetools-testmsgs/badbound-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/badbound-expected.json new file mode 100644 index 00000000000..bcf7a0a33eb --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/badbound-expected.json @@ -0,0 +1,7 @@ +{ + "exception": { + "clazz": "javax.mail.internet.ParseException", + "message": "Missing start boundary" + }, + "result": null +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/badbound.msg b/packages/node-mimimi/test/mimetools-testmsgs/badbound.msg new file mode 100644 index 00000000000..0ac46cad54e --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/badbound.msg @@ -0,0 +1,160 @@ +Received: from uriela.in-berlin.de by anna.in-berlin.de via SMTP (940816.SGI.8.6.9/940406.SGI) + for id AAA04436; Mon, 23 Dec 1996 00:08:02 +0100 +Resent-From: koenig@franz.ww.TU-Berlin.DE +Received: by uriela.in-berlin.de (/\oo/\ Smail3.1.29.1 #29.8) + id ; Mon, 23 Dec 96 00:08 MET +Received: by methan.chemie.fu-berlin.de (Smail3.1.29.1) + from franz.ww.TU-Berlin.DE (130.149.200.51) with smtp + id ; Mon, 23 Dec 96 00:07 MET +Received: (from koenig@localhost) by franz.ww.TU-Berlin.DE (8.7.3/8.7.3) id XAA01761 for k@anna.in-berlin.de; Sun, 22 Dec 1996 23:25:10 +0100 (CET) +Resent-Date: Sun, 22 Dec 1996 23:25:10 +0100 (CET) +Resent-Message-Id: <199612222225.XAA01761@franz.ww.TU-Berlin.DE> +Resent-To: k +Received: from mailgzrz.TU-Berlin.DE (mailgzrz.TU-Berlin.DE [130.149.4.10]) by franz.ww.TU-Berlin.DE (8.7.3/8.7.3) with ESMTP id XAA01754 for ; Sun, 22 Dec 1996 23:24:32 +0100 (CET) +Received: from challenge.uscom.com (actually mail.uscom.com) + by mailgzrz.TU-Berlin.DE with SMTP (PP); + Sun, 22 Dec 1996 23:19:35 +0100 +To: koenig@franz.ww.tu-berlin.de +From: Mail Administrator +Reply-To: Mail Administrator +Subject: Mail System Error - Returned Mail +Date: Sun, 22 Dec 1996 17:21:12 -0500 +Message-ID: <19961222222112.AAE16235@challenge.uscom.com> +MIME-Version: 1.0 +Content-Type: multipart/mixed; Boundary="===========================_ _= + 2283630(16235)" +Content-Transfer-Encoding: 7BIT +X-Filter: mailagent [version 3.0 PL44] for koenig@franz.ww.tu-berlin.de +X-Filter: mailagent [version 3.0 PL44] for k@.in-berlin.de + +--===========================_ _= + 2283630(16235) +Content-type: text/plain; charset="us-ascii" + +This Message was undeliverable due to the following reason: + +The Program-Deliver module couldn't deliver the message to one or more +of the intended recipients because their delivery program(s) failed. +The following error messages provide the details about each failure: + + The delivery program "/pages/pnet/admin/mail_proc.pl" produced + the following output while delivering the message to + pnet@uscom.com + + Can't exec "/usr/sbin/sendmail -t": No such file or directory at /pages/pnet/admin/mail_proc.pl line 96, <> line 93. + Broken pipe + + The program "/pages/pnet/admin/mail_proc.pl" exited with an + unknown value of 141 while delivering the message to + pnet@uscom.com + +The message was not delivered to: + + pnet@uscom.com + +Please reply to Postmaster@challenge.uscom.com +if you feel this message to be in error. + +--===========================_ _= + 2283630(16235) +Content-type: message/rfc822 +Content-Disposition: attachment +Date: Sun, 22 Dec 1996 22:24:21 +0000 +Message-ID: <"mailgzrz.T.061:22.12.96.22.24.21"@TU-Berlin.DE> + +Received: from franz.ww.TU-Berlin.DE ([130.149.200.51]) + by challenge.uscom.com (Netscape Mail Server v2.02) with ESMTP + id AAA685 for ; Wed, 18 Dec 1996 16:58:30 -0500 +Received: from mailgzrz.TU-Berlin.DE (mailgzrz.TU-Berlin.DE [130.149.4.10]) + by franz.ww.TU-Berlin.DE (8.7.3/8.7.3) with ESMTP id SAA23400 + for ; + Wed, 18 Dec 1996 18:59:21 +0100 (CET) +Received: from anna.in-berlin.de by mailgzrz.TU-Berlin.DE with SMTP (PP); + Wed, 18 Dec 1996 18:59:11 +0100 +Received: by anna.in-berlin.de (940816.SGI.8.6.9/940406.SGI) id SAA19491; + Wed, 18 Dec 1996 18:55:21 +0100 +Date: Wed, 18 Dec 1996 18:55:21 +0100 +Message-Id: <199612181755.SAA19491@anna.in-berlin.de> +From: Andreas Koenig +To: michelle@eugene.net +CC: msqlperl@franz.ww.tu-berlin.de +In-reply-to: (message from Michelle Brownsworth on Wed, 18 Dec 1996 09:32:02 -0700) +Subject: HOWTO (Was: unsubscribe) + +>>>>> On Wed, 18 Dec 1996 09:32:02 -0700, +>>>>> Michelle Brownsworth +>>>>> who can be reached at: michelle@eugene.net +>>>>> (whose comments are cited below with " michelle> "), +>>>>> sent the cited message concerning the subject of "unsubscribe" +>>>>> twice to the whole list of subscribers + + michelle> unsubscribe + + michelle> ************************************************************ + michelle> Michelle Brownsworth + michelle> System Administrator + + michelle> Internet Marketing Services michelle@eugene.net + michelle> 2300 Oakmont Way, #209 541-431-3374 + michelle> Eugene, OR 97402 FAX 431-7345 + michelle> ************************************************************ + +Welcome new subscriber! + +You've joined the mailing list of unsubscribers' collected wisdom of +unsubscribe messages. Relax! + +You won't have to subscribe to any mailing list for the rest of your +life. Better yet, you can't even unsubscribe! So just lean back and +enjoy to watch your IO stream of millions of unsubscribe messages +daily. + +Isn't that far more than everything you ever dared to dream of? + +andreas + +P.S. This was posted 12 days ago: + +Date: Fri, 6 Dec 1996 15:47:51 +0100 +To: msqlperl@franz.ww.tu-berlin.de +Subject: How To Unsubscribe (semi-regular posting) + +To get off this list, send mail to +-------------------- + + majordomo@franz.ww.tu-berlin.de + +with the following words in the body of the message (subject line will +be ignored): + + unsubscribe msqlperl <_insert_your_subscription_address_here_> + + +To find out who you are subscribed as, send mail to +------------------------------------- + + majordomo@franz.ww.tu-berlin.de + +with only nothing but + + who msqlperl + +in the body of the message: + + +If you encounter problems, +------------------------- + +please try sending the message "help" to majordomo@franz.ww.tu-berlin.de. + + +Hope that help, +andreas + + +NOTE: if the above recipe does not work for you, ask me for assistance +and do not spam the list with the request. Thank you! + + +--===========================_ _= + 2283630(16235)-- diff --git a/packages/node-mimimi/test/mimetools-testmsgs/badfile-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/badfile-expected.json new file mode 100644 index 00000000000..8e5093a0c62 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/badfile-expected.json @@ -0,0 +1,36 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": null, + "alternativeBoundary": null, + "sender": { + "name": "Michelle Holm", + "mailAddress": "holm@sitka.colorado.edu", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "imswww@rhine.gsfc.nasa.gov", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "note the bogus filename", + "plainBodyText": "This had better not end up in /tmp!\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [], + "mailHeaders": "From: Michelle Holm \nTo: imswww@rhine.gsfc.nasa.gov\nMime-Version: 1.0\nSubject: note the bogus filename\nContent-Type: text/plain; charset=iso-8859-1; name=\"/tmp/whoa\"\nContent-Transfer-Encoding: 8bit", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/badfile.msg b/packages/node-mimimi/test/mimetools-testmsgs/badfile.msg new file mode 100644 index 00000000000..a141c27b02f --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/badfile.msg @@ -0,0 +1,8 @@ +From: Michelle Holm +To: imswww@rhine.gsfc.nasa.gov +Mime-Version: 1.0 +Subject: note the bogus filename +Content-Type: text/plain; charset=iso-8859-1; name="/tmp/whoa" +Content-Transfer-Encoding: 8bit + +This had better not end up in /tmp! diff --git a/packages/node-mimimi/test/mimetools-testmsgs/bluedot-postcard-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/bluedot-postcard-expected.json new file mode 100644 index 00000000000..33fd6b16fd3 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/bluedot-postcard-expected.json @@ -0,0 +1,39 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "----------=_961872013-1436-1", + "alternativeBoundary": "----------=_961872013-1436-0", + "sender": { + "name": "", + "mailAddress": "", + "valid": false + }, + "toRecipients": [], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "A postcard for you", + "plainBodyText": "Having a wonderful time... \r\nwish you were looking at HTML \r\ninstead of this boring text!\r\n", + "htmlBodyText": "

Hey there!

\r\n Having a wonderful time... \r\n take a look!\r\n
\"Snapshot\"
", + "attachedMessages": [], + "attachedFiles": [ + { + "name": "bluedot.jpg", + "data": "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KOGCJAkccahVRQMAADgADjFeFjM2veFD7/8AL/M+qy7h+zVXFf8AgP8An/l/wxDp2nWmk6fDY2MCw20IwiAk9Tkkk8kkkkk5JJJJJNWqKK8I+r2CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigDlNY+H2iarcPdQefpd1I5eWawKp5pJJJZGVkLEnJfbvOAN2OK5O88AeJ7PJt5dN1ONV3kqXtZCf7iod6k8cEyKCTg4AyfV6K6aOLr0dIS0OLEZdhcRrVgm++z+9HiU2j+IrWJprrwzqccK/edDDORngfJFI7nn0U46nABNVP9L/6A2uf+Ce6/wDjde8UV2RzjELdJ/L/AIJ5suHMG3o5L5r9UeEJHfzSLHFomttI5CoraZPGCT0BZ0Cr9WIA7kCtGHwx4ruZViXw7JbFv+Wt3dwLEvf5jG7t7DCnkjOBkj2ailLN8Q9rL5f5lQ4dwcd7v1f+SR5xYfDK7lkR9Z1lTCQGe2sITGc8ZQzMxJXGRlVRjwQV6V2ukeH9I0GN00rTra0MgUSvHGA8u3ODI/3nPJ5Yk5JOeTWlRXBVr1KrvUdz1aGFo4dWpRSCiiisjoCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//Z", + "mimeType": "image/jpeg", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "Content-Type: multipart/alternative; boundary=\"----------=_961872013-1436-0\"\nContent-Transfer-Encoding: binary\nMime-Version: 1.0\nX-Mailer: MIME-tools 5.211 (Entity 5.205)\nTo: noone\nSubject: A postcard for you", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/bluedot-postcard.msg b/packages/node-mimimi/test/mimetools-testmsgs/bluedot-postcard.msg new file mode 100644 index 00000000000..89c2516d13b --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/bluedot-postcard.msg @@ -0,0 +1,138 @@ +Content-Type: multipart/alternative; boundary="----------=_961872013-1436-0" +Content-Transfer-Encoding: binary +Mime-Version: 1.0 +X-Mailer: MIME-tools 5.211 (Entity 5.205) +To: noone +Subject: A postcard for you + +This is a multi-part message in MIME format... + +------------=_961872013-1436-0 +Content-Type: text/plain +Content-Disposition: inline +Content-Transfer-Encoding: binary + +Having a wonderful time... +wish you were looking at HTML +instead of this boring text! + +------------=_961872013-1436-0 +Content-Type: multipart/related; boundary="----------=_961872013-1436-1" +Content-Transfer-Encoding: binary + +This is a multi-part message in MIME format... + +------------=_961872013-1436-1 +Content-Type: text/html +Content-Disposition: inline +Content-Transfer-Encoding: binary + +

Hey there!

+ Having a wonderful time... + take a look! +
Snapshot
+------------=_961872013-1436-1 +Content-Type: image/jpeg; name="bluedot.jpg" +Content-Disposition: inline; filename="bluedot.jpg" +Content-Transfer-Encoding: base64 +Content-Id: my-graphic + +/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsL +DBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/ +2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy +MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAEAAQADASIAAhEBAxEB/8QA +HwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF +BAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkK +FhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1 +dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXG +x8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEB +AQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAEC +AxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRom +JygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOE +hYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU +1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigAoo +ooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiii +gAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKA +CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAK +KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoo +ooAKKKKACiiigAooooAKKKKACiiigAooooAKKKjnnhtbeW4uJY4YIkLySSMF +VFAySSeAAOc0ASUVw+p/E3TY1MeiW0uqzgkb8NBbjB6+ay/MCM4MauDgZIBB +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 +KOGCJAkccahVRQMAADgADjFeFjM2veFD7/8AL/M+qy7h+zVXFf8AgP8An/l/ +wxDp2nWmk6fDY2MCw20IwiAk9Tkkk8kkkkk5JJJJJNWqKK8I+r2CiiigAooo +oAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigDlNY+H2iarcPdQef +pd1I5eWawKp5pJJJZGVkLEnJfbvOAN2OK5O88AeJ7PJt5dN1ONV3kqXtZCf7 +iod6k8cEyKCTg4AyfV6K6aOLr0dIS0OLEZdhcRrVgm++z+9HiU2j+IrWJprr +wzqccK/edDDORngfJFI7nn0U46nABNVP9L/6A2uf+Ce6/wDjde8UV2RzjELd +J/L/AIJ5suHMG3o5L5r9UeEJHfzSLHFomttI5CoraZPGCT0BZ0Cr9WIA7kCt +GHwx4ruZViXw7JbFv+Wt3dwLEvf5jG7t7DCnkjOBkj2ailLN8Q9rL5f5lQ4d +wcd7v1f+SR5xYfDK7lkR9Z1lTCQGe2sITGc8ZQzMxJXGRlVRjwQV6V2ukeH9 +I0GN00rTra0MgUSvHGA8u3ODI/3nPJ5Yk5JOeTWlRXBVr1KrvUdz1aGFo4dW +pRSCiiisjoCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACi +iigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKK +KACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoooo +AKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigA +ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//Z + +------------=_961872013-1436-1-- + +------------=_961872013-1436-0-- diff --git a/packages/node-mimimi/test/mimetools-testmsgs/bluedot-simple-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/bluedot-simple-expected.json new file mode 100644 index 00000000000..97534fe9dfa --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/bluedot-simple-expected.json @@ -0,0 +1,39 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": null, + "alternativeBoundary": null, + "sender": { + "name": "", + "mailAddress": "", + "valid": false + }, + "toRecipients": [], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "", + "plainBodyText": null, + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "bluedot.jpg", + "data": "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KOGCJAkccahVRQMAADgADjFeFjM2veFD7/8AL/M+qy7h+zVXFf8AgP8An/l/wxDp2nWmk6fDY2MCw20IwiAk9Tkkk8kkkkk5JJJJJNWqKK8I+r2CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigDlNY+H2iarcPdQefpd1I5eWawKp5pJJJZGVkLEnJfbvOAN2OK5O88AeJ7PJt5dN1ONV3kqXtZCf7iod6k8cEyKCTg4AyfV6K6aOLr0dIS0OLEZdhcRrVgm++z+9HiU2j+IrWJprrwzqccK/edDDORngfJFI7nn0U46nABNVP9L/6A2uf+Ce6/wDjde8UV2RzjELdJ/L/AIJ5suHMG3o5L5r9UeEJHfzSLHFomttI5CoraZPGCT0BZ0Cr9WIA7kCtGHwx4ruZViXw7JbFv+Wt3dwLEvf5jG7t7DCnkjOBkj2ailLN8Q9rL5f5lQ4dwcd7v1f+SR5xYfDK7lkR9Z1lTCQGe2sITGc8ZQzMxJXGRlVRjwQV6V2ukeH9I0GN00rTra0MgUSvHGA8u3ODI/3nPJ5Yk5JOeTWlRXBVr1KrvUdz1aGFo4dWpRSCiiisjoCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//Z", + "mimeType": "image/jpeg", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "Content-Type: image/jpeg; name=\"bluedot.jpg\"\nContent-Disposition: inline; filename=\"bluedot.jpg\"\nContent-Transfer-Encoding: base64\nContent-Id: my-graphic", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/bluedot-simple.msg b/packages/node-mimimi/test/mimetools-testmsgs/bluedot-simple.msg new file mode 100644 index 00000000000..8d9e4a7a390 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/bluedot-simple.msg @@ -0,0 +1,100 @@ +Content-Type: image/jpeg; name="bluedot.jpg" +Content-Disposition: inline; filename="bluedot.jpg" +Content-Transfer-Encoding: base64 +Content-Id: my-graphic + +/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsL +DBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/ +2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy +MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAEAAQADASIAAhEBAxEB/8QA +HwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF +BAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkK +FhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1 +dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXG +x8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEB +AQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAEC +AxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRom +JygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOE +hYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU +1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigAoo +ooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiii +gAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKA +CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAK +KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoo +ooAKKKKACiiigAooooAKKKKACiiigAooooAKKKjnnhtbeW4uJY4YIkLySSMF +VFAySSeAAOc0ASUVw+p/E3TY1MeiW0uqzgkb8NBbjB6+ay/MCM4MauDgZIBB +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 +KOGCJAkccahVRQMAADgADjFeFjM2veFD7/8AL/M+qy7h+zVXFf8AgP8An/l/ +wxDp2nWmk6fDY2MCw20IwiAk9Tkkk8kkkkk5JJJJJNWqKK8I+r2CiiigAooo +oAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigDlNY+H2iarcPdQef +pd1I5eWawKp5pJJJZGVkLEnJfbvOAN2OK5O88AeJ7PJt5dN1ONV3kqXtZCf7 +iod6k8cEyKCTg4AyfV6K6aOLr0dIS0OLEZdhcRrVgm++z+9HiU2j+IrWJprr +wzqccK/edDDORngfJFI7nn0U46nABNVP9L/6A2uf+Ce6/wDjde8UV2RzjELd +J/L/AIJ5suHMG3o5L5r9UeEJHfzSLHFomttI5CoraZPGCT0BZ0Cr9WIA7kCt +GHwx4ruZViXw7JbFv+Wt3dwLEvf5jG7t7DCnkjOBkj2ailLN8Q9rL5f5lQ4d +wcd7v1f+SR5xYfDK7lkR9Z1lTCQGe2sITGc8ZQzMxJXGRlVRjwQV6V2ukeH9 +I0GN00rTra0MgUSvHGA8u3ODI/3nPJ5Yk5JOeTWlRXBVr1KrvUdz1aGFo4dW +pRSCiiisjoCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACi +iigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKK +KACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoooo +AKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigA +ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//Z diff --git a/packages/node-mimimi/test/mimetools-testmsgs/double-bound-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/double-bound-expected.json new file mode 100644 index 00000000000..e0a6f6e42e1 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/double-bound-expected.json @@ -0,0 +1,45 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "------------299A70B339B65A93542D2AE", + "alternativeBoundary": null, + "sender": { + "name": "Eryq", + "mailAddress": "eryq@rhine.gsfc.nasa.gov", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "john-bigboote@eryq.pr.mcs.net", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 829203030000, + "subject": "test of double-boundary behavior", + "plainBodyText": null, + "htmlBodyText": "

This message contains double boundaries all over the\r\nplace. We want to make sure that bad things don't happen.\r\n\r\n

One bad thing is that the doubled-boundary above can\r\nbe mistaken for a single boundary plus a bogus premature\r\nend of headers.\r\n

Hello? Am I here?\r\n

Hello? Am I here?\r\n", + "attachedMessages": [], + "attachedFiles": [ + { + "name": "3d-eye.gif", + "data": "R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7+3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatLrU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJvs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjwE0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "Return-Path: eryq@rhine.gsfc.nasa.gov\nSender: john-bigboote\nDate: Thu, 11 Apr 1996 01:10:30 -0500\nFrom: Eryq \nOrganization: Yoyodyne Propulsion Systems\nX-Mailer: Mozilla 2.0 (X11; I; Linux 1.1.18 i486)\nMIME-Version: 1.0\nTo: john-bigboote@eryq.pr.mcs.net\nSubject: test of double-boundary behavior\nContent-Type: multipart/mixed; boundary=\"------------299A70B339B65A93542D2AE\"", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/double-bound.msg b/packages/node-mimimi/test/mimetools-testmsgs/double-bound.msg new file mode 100644 index 00000000000..9650d416a5b --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/double-bound.msg @@ -0,0 +1,67 @@ +Return-Path: eryq@rhine.gsfc.nasa.gov +Sender: john-bigboote +Date: Thu, 11 Apr 1996 01:10:30 -0500 +From: Eryq +Organization: Yoyodyne Propulsion Systems +X-Mailer: Mozilla 2.0 (X11; I; Linux 1.1.18 i486) +MIME-Version: 1.0 +To: john-bigboote@eryq.pr.mcs.net +Subject: test of double-boundary behavior +Content-Type: multipart/mixed; boundary="------------299A70B339B65A93542D2AE" + +This is a multi-part message in MIME format. + +--------------299A70B339B65A93542D2AE +--------------299A70B339B65A93542D2AE +Content-Type: text/html; charset=us-ascii +Content-Transfer-Encoding: 7bit +Subject: [2] this should be text/html, but double-bound may mess it up + +

This message contains double boundaries all over the +place. We want to make sure that bad things don't happen. + +

One bad thing is that the doubled-boundary above can +be mistaken for a single boundary plus a bogus premature +end of headers. + +--------------299A70B339B65A93542D2AE +--------------299A70B339B65A93542D2AE +Content-Type: text/html; charset=us-ascii +Subject: [4] this should be text/html, but double-bound may mess it up + +

Hello? Am I here? + +--------------299A70B339B65A93542D2AE + +--------------299A70B339B65A93542D2AE +Content-Type: text/html; charset=us-ascii +Subject: [6] this should be text/html, but double-bound may mess it up + +

Hello? Am I here? + +--------------299A70B339B65A93542D2AE +Content-Type: text/html; charset=us-ascii +Subject: [7] this header is improperly terminated +--------------299A70B339B65A93542D2AE +Content-Type: text/html; charset=us-ascii +Subject: [8] this body is empty + +--------------299A70B339B65A93542D2AE +Content-Type: text/html; charset=us-ascii +Subject: [9] this body also empty + +--------------299A70B339B65A93542D2AE +Content-Type: image/gif; name="3d-eye.gif" +Content-Transfer-Encoding: base64 +Subject: [10] just an image + +R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAA +AAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7 +VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7 ++3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatL +rU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJ +vs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjw +E0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7 +--------------299A70B339B65A93542D2AE-- + + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/double-semicolon-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/double-semicolon-expected.json new file mode 100644 index 00000000000..39a4783371c --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/double-semicolon-expected.json @@ -0,0 +1,7 @@ +{ + "exception": { + "clazz": "javax.mail.internet.ParseException", + "message": "In parameter list <;; boundary=\"foo\">, expected parameter name, got \";\"" + }, + "result": null +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/double-semicolon.msg b/packages/node-mimimi/test/mimetools-testmsgs/double-semicolon.msg new file mode 100644 index 00000000000..8075ed26bfc --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/double-semicolon.msg @@ -0,0 +1,17 @@ +Mime-Version: 1.0 +Content-Type: multipart/alternative;; boundary="foo" + +Preamble + +--foo +Content-Type: text/plain; charset=us-ascii + +The better part + +--foo +Content-Type: text/plain; charset=us-ascii + +The worse part + +--foo-- + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/double-semicolon2-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/double-semicolon2-expected.json new file mode 100644 index 00000000000..9d02c7fe98d --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/double-semicolon2-expected.json @@ -0,0 +1,7 @@ +{ + "exception": { + "clazz": "javax.mail.internet.ParseException", + "message": "In parameter list < ; ; ; ;; ;;;;;;;; boundary=\"foo\">, expected parameter name, got \";\"" + }, + "result": null +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/double-semicolon2.msg b/packages/node-mimimi/test/mimetools-testmsgs/double-semicolon2.msg new file mode 100644 index 00000000000..7ce3717b8c4 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/double-semicolon2.msg @@ -0,0 +1,17 @@ +Mime-Version: 1.0 +Content-Type: multipart/alternative ; ; ; ;; ;;;;;;;; boundary="foo" + +Preamble + +--foo +Content-Type: text/plain; charset=us-ascii + +The better part + +--foo +Content-Type: text/plain; charset=us-ascii + +The worse part + +--foo-- + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/dup-names-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/dup-names-expected.json new file mode 100644 index 00000000000..2d007a494c1 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/dup-names-expected.json @@ -0,0 +1,77 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "------------299A70B339B65A93542D2AE", + "alternativeBoundary": null, + "sender": { + "name": "Eryq", + "mailAddress": "eryq@rhine.gsfc.nasa.gov", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "john-bigboote@eryq.pr.mcs.net", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 829203030000, + "subject": "Two images for you...", + "plainBodyText": null, + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "one.gif", + "data": "R0lGODdhKAAoAOMAAAAAAAAAgB6Q/y9PT25ubnCAkKBSLb6+vufn5/Xes/+lAP/6zQAAAAAAAAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJLMOyYbcoxkaZ5oCkoH6L5wLMfiWqd4btZhmxbAoFCY47EIqMJgyWw2ATjj7aRkAq5YwDMl9VGtKO0SiuoiTVlscsxt9c4HgXxUIA0EAVOVfDKT8Hl1B3kDAYYle202XnGGgoMHhYckiWVuR3+OTgCGeZRslotwgJ2lnYigfZdTjQULr7ALBZN0qTurjHgLKAu0B5Wqopm7J72etQN8t8Ijury+wMtvw8/Hv7Ylfs0BxCbGqMmK0yOOQ0GTCgrR2bhwJGlXJQYG6mMKoeNoWSbzCWIACe5JwxQm3AkDAbUAQCiQhDZEBeBl6afgCsOBrD45edIvQceGWSMevpOYhl6CkydBHhBZQmGKjihVshypjB9ClAHZMTugzOU7mzhBPiSZ5uDNnA7b/aTZ0mhMnfl0pDBFa6bUElSPWb0qtYuHrxlwcR17YsWMs2jTql3LFkQEADs=", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + }, + { + "name": "one.gif", + "data": "R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7+3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatLrU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJvs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjwE0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + }, + { + "name": "two.nice.gif", + "data": "R0lGODdhKAAoAOMAAAAAAAAAgB6Q/y9PT25ubnCAkKBSLb6+vufn5/Xes/+lAP/6zQAAAAAAAAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJLMOyYbcoxkaZ5oCkoH6L5wLMfiWqd4btZhmxbAoFCY47EIqMJgyWw2ATjj7aRkAq5YwDMl9VGtKO0SiuoiTVlscsxt9c4HgXxUIA0EAVOVfDKT8Hl1B3kDAYYle202XnGGgoMHhYckiWVuR3+OTgCGeZRslotwgJ2lnYigfZdTjQULr7ALBZN0qTurjHgLKAu0B5Wqopm7J72etQN8t8Ijury+wMtvw8/Hv7Ylfs0BxCbGqMmK0yOOQ0GTCgrR2bhwJGlXJQYG6mMKoeNoWSbzCWIACe5JwxQm3AkDAbUAQCiQhDZEBeBl6afgCsOBrD45edIvQceGWSMevpOYhl6CkydBHhBZQmGKjihVshypjB9ClAHZMTugzOU7mzhBPiSZ5uDNnA7b/aTZ0mhMnfl0pDBFa6bUElSPWb0qtYuHrxlwcR17YsWMs2jTql3LFkQEADs=", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + }, + { + "name": "two.nice.gif", + "data": "R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7+3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatLrU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJvs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjwE0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + }, + { + "name": "two.nice.gif", + "data": "R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7+3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatLrU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJvs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjwE0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "Return-Path: eryq@rhine.gsfc.nasa.gov\nSender: john-bigboote\nDate: Thu, 11 Apr 1996 01:10:30 -0500\nFrom: Eryq \nOrganization: Yoyodyne Propulsion Systems\nX-Mailer: Mozilla 2.0 (X11; I; Linux 1.1.18 i486)\nMIME-Version: 1.0\nTo: john-bigboote@eryq.pr.mcs.net\nSubject: Two images for you...\nContent-Type: multipart/mixed; boundary=\"------------299A70B339B65A93542D2AE\"", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/dup-names.msg b/packages/node-mimimi/test/mimetools-testmsgs/dup-names.msg new file mode 100644 index 00000000000..02329251d54 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/dup-names.msg @@ -0,0 +1,78 @@ +Return-Path: eryq@rhine.gsfc.nasa.gov +Sender: john-bigboote +Date: Thu, 11 Apr 1996 01:10:30 -0500 +From: Eryq +Organization: Yoyodyne Propulsion Systems +X-Mailer: Mozilla 2.0 (X11; I; Linux 1.1.18 i486) +MIME-Version: 1.0 +To: john-bigboote@eryq.pr.mcs.net +Subject: Two images for you... +Content-Type: multipart/mixed; boundary="------------299A70B339B65A93542D2AE" + +This is a multi-part message in MIME format. + +--------------299A70B339B65A93542D2AE +Content-Type: image/gif +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="one.gif" + +R0lGODdhKAAoAOMAAAAAAAAAgB6Q/y9PT25ubnCAkKBSLb6+vufn5/Xes/+lAP/6zQAAAAAA +AAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJLMOyYbcoxkaZ5oCkoH6L5wLMfiWqd4btZhmxbA +oFCY47EIqMJgyWw2ATjj7aRkAq5YwDMl9VGtKO0SiuoiTVlscsxt9c4HgXxUIA0EAVOVfDKT +8Hl1B3kDAYYle202XnGGgoMHhYckiWVuR3+OTgCGeZRslotwgJ2lnYigfZdTjQULr7ALBZN0 +qTurjHgLKAu0B5Wqopm7J72etQN8t8Ijury+wMtvw8/Hv7Ylfs0BxCbGqMmK0yOOQ0GTCgrR +2bhwJGlXJQYG6mMKoeNoWSbzCWIACe5JwxQm3AkDAbUAQCiQhDZEBeBl6afgCsOBrD45edIv +QceGWSMevpOYhl6CkydBHhBZQmGKjihVshypjB9ClAHZMTugzOU7mzhBPiSZ5uDNnA7b/aTZ +0mhMnfl0pDBFa6bUElSPWb0qtYuHrxlwcR17YsWMs2jTql3LFkQEADs= +--------------299A70B339B65A93542D2AE +Content-Type: image/gif +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="one.gif" + +R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAA +AAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7 +VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7 ++3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatL +rU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJ +vs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjw +E0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7 +--------------299A70B339B65A93542D2AE +Content-Type: image/gif +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="two.nice.gif" + +R0lGODdhKAAoAOMAAAAAAAAAgB6Q/y9PT25ubnCAkKBSLb6+vufn5/Xes/+lAP/6zQAAAAAA +AAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJLMOyYbcoxkaZ5oCkoH6L5wLMfiWqd4btZhmxbA +oFCY47EIqMJgyWw2ATjj7aRkAq5YwDMl9VGtKO0SiuoiTVlscsxt9c4HgXxUIA0EAVOVfDKT +8Hl1B3kDAYYle202XnGGgoMHhYckiWVuR3+OTgCGeZRslotwgJ2lnYigfZdTjQULr7ALBZN0 +qTurjHgLKAu0B5Wqopm7J72etQN8t8Ijury+wMtvw8/Hv7Ylfs0BxCbGqMmK0yOOQ0GTCgrR +2bhwJGlXJQYG6mMKoeNoWSbzCWIACe5JwxQm3AkDAbUAQCiQhDZEBeBl6afgCsOBrD45edIv +QceGWSMevpOYhl6CkydBHhBZQmGKjihVshypjB9ClAHZMTugzOU7mzhBPiSZ5uDNnA7b/aTZ +0mhMnfl0pDBFa6bUElSPWb0qtYuHrxlwcR17YsWMs2jTql3LFkQEADs= +--------------299A70B339B65A93542D2AE +Content-Type: image/gif +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="two.nice.gif" + +R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAA +AAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7 +VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7 ++3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatL +rU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJ +vs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjw +E0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7 +--------------299A70B339B65A93542D2AE +Content-Type: image/gif +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="two.nice.gif" + +R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAA +AAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7 +VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7 ++3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatL +rU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJ +vs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjw +E0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7 +--------------299A70B339B65A93542D2AE-- + + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/empty-preamble-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/empty-preamble-expected.json new file mode 100644 index 00000000000..e2510787846 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/empty-preamble-expected.json @@ -0,0 +1,39 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "t0UkRYy7tHLRMCai", + "alternativeBoundary": null, + "sender": { + "name": "", + "mailAddress": "", + "valid": false + }, + "toRecipients": [], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "", + "plainBodyText": "Das ist ein Test.\n-- \nsub i($){print$_[0]}*j=*ENV;sub w($){sleep$_[0]}sub _($){i\"$p:$c> \",w+01\n,$_=$_[0],tr;i-za-h,;a-hi-z ;,i$_,w+01,i\"\\n\"}$|=1;$f='HO';($c=$j{PWD})=~\ns+$j{$f.\"ME\"}+~+;$p.=\"$j{USER}\\@\".`hostname`;chop$p;_\"kl\",$c='~',_\"zu,\".\n\"-zn,*\",_\"#,epg,lw,gwc,mfmkcbm,cvsvwev,uiqt,kwvbmvb?\",i\"$p:$c> \";w+1<<07\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "dot.png", + "data": "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH1AUbFQQ0Vbb7XQAAAB10RVh0Q29tbWVudABDcmVhdGVkIHdpdGggVGhlIEdJTVDvZCVuAAAAFklEQVR42mP8//8/AwMDEwMDAwMDAwAkBgMB/umWrAAAAABJRU5ErkJggg==", + "mimeType": "image/png", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "Content-Type: multipart/mixed; boundary=\"t0UkRYy7tHLRMCai\"\nContent-Disposition: inline", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/empty-preamble.msg b/packages/node-mimimi/test/mimetools-testmsgs/empty-preamble.msg new file mode 100644 index 00000000000..38ad42562cd --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/empty-preamble.msg @@ -0,0 +1,27 @@ +Content-Type: multipart/mixed; boundary="t0UkRYy7tHLRMCai" +Content-Disposition: inline + + +--t0UkRYy7tHLRMCai +Content-Type: text/plain; charset=us-ascii +Content-Disposition: inline +Content-Transfer-Encoding: quoted-printable + +Das ist ein Test. +--=20 +sub i($){print$_[0]}*j=3D*ENV;sub w($){sleep$_[0]}sub _($){i"$p:$c> ",w+01 +,$_=3D$_[0],tr;i-za-h,;a-hi-z ;,i$_,w+01,i"\n"}$|=3D1;$f=3D'HO';($c=3D$j{PW= +D})=3D~ +s+$j{$f."ME"}+~+;$p.=3D"$j{USER}\@".`hostname`;chop$p;_"kl",$c=3D'~',_"zu,". +"-zn,*",_"#,epg,lw,gwc,mfmkcbm,cvsvwev,uiqt,kwvbmvb?",i"$p:$c> ";w+1<<07 + +--t0UkRYy7tHLRMCai +Content-Type: image/png +Content-Disposition: attachment; filename="dot.png" +Content-Transfer-Encoding: base64 + +iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAAAsTAAALEwEAmpwY +AAAAB3RJTUUH1AUbFQQ0Vbb7XQAAAB10RVh0Q29tbWVudABDcmVhdGVkIHdpdGggVGhlIEdJ +TVDvZCVuAAAAFklEQVR42mP8//8/AwMDEwMDAwMDAwAkBgMB/umWrAAAAABJRU5ErkJggg== + +--t0UkRYy7tHLRMCai-- diff --git a/packages/node-mimimi/test/mimetools-testmsgs/frag-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/frag-expected.json new file mode 100644 index 00000000000..4812822e5ca --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/frag-expected.json @@ -0,0 +1,51 @@ +{ + "exception": null, + "result": { + "id": "392C2385.4C402C55@mailandnews.com", + "boundary": "------------ABE49921AF9E83E8F9A7667E", + "alternativeBoundary": null, + "sender": { + "name": "Sven", + "mailAddress": "omrec@mailandnews.com", + "valid": true + }, + "toRecipients": [ + { + "name": "Eryq", + "mailAddress": "eryq@zeegee.com", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [ + { + "name": "", + "mailAddress": "omrec@mailandnews.com", + "valid": true + } + ], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 959193989000, + "subject": "[Fwd: [Fwd: [Fwd: FW: Another Priceless Moment]]]", + "plainBodyText": "\r\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "[Fwd_ [Fwd_ FW_ Another Priceless Moment]].eml", + "data": "UmVjZWl2ZWQ6IGZyb20gbWFpbC52eW5jZS5vcmcgWzYzLjE5OC40My4xM10gKHZ5bmNlQHZ5bmNlLm9yZyk7IFR1ZSwgMjMgTWF5IDIwMDAgMjI6MDA6MTYgLTA0MDANClgtRW52ZWxvcGUtVG86IG9tcmVjDQpSZWNlaXZlZDogZnJvbSB2eW5jZS5vcmcgKDE2Ni45MC4xMjguMjQzKSBieSBtYWlsLnZ5bmNlLm9yZw0KIHdpdGggRVNNVFAgKEV1ZG9yYSBJbnRlcm5ldCBNYWlsIFNlcnZlciAxLjMuMSk7IFR1ZSwgMjMgTWF5IDIwMDAgMTk6MDU6NTIgLTA3MDANCk1lc3NhZ2UtSUQ6IDwzOTJCMzg5QS4xOTY4OTk4QkB2eW5jZS5vcmc+DQpEYXRlOiBUdWUsIDIzIE1heSAyMDAwIDE5OjA0OjEwIC0wNzAwDQpGcm9tOiBWeW5jZSA8dnluY2VAdnluY2Uub3JnPg0KT3JnYW5pemF0aW9uOiBEZXNrdG9wLmNvbQ0KWC1NYWlsZXI6IE1vemlsbGEgNC42MSBbZW5dIChXaW45ODsgVSkNClgtQWNjZXB0LUxhbmd1YWdlOiBlbg0KTUlNRS1WZXJzaW9uOiAxLjANClRvOiBvbXJlY0BtYWlsYW5kbmV3cy5jb20NClN1YmplY3Q6IFtGd2Q6IFtGd2Q6IEZXOiBBbm90aGVyIFByaWNlbGVzcyAgTW9tZW50XV0NCkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L21peGVkOw0KIGJvdW5kYXJ5PSItLS0tLS0tLS0tLS00Q0VCNUU0NDhEQzA3N0YzNTA1MEM0QkUiDQpYLU1vemlsbGEtU3RhdHVzMjogMDAwMDAwMDANCg0KVGhpcyBpcyBhIG11bHRpLXBhcnQgbWVzc2FnZSBpbiBNSU1FIGZvcm1hdC4NCi0tLS0tLS0tLS0tLS0tNENFQjVFNDQ4REMwNzdGMzUwNTBDNEJFDQpDb250ZW50LVR5cGU6IHRleHQvcGxhaW47IGNoYXJzZXQ9dXMtYXNjaWkNCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IDdiaXQNCg0KanVzdCB0byBhZGQgdG8geW91ciBwZXJzb25hbCBoZWxsLg0KDQoNCi0tLS0tLS0tLS0tLS0tNENFQjVFNDQ4REMwNzdGMzUwNTBDNEJFDQpDb250ZW50LVR5cGU6IG1lc3NhZ2UvcmZjODIyDQpDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiA3Yml0DQpDb250ZW50LURpc3Bvc2l0aW9uOiBpbmxpbmUNCg0KUmV0dXJuLVBhdGg6IDxqYXNvbmNAc25lc3lzdGVtcy5jb20+DQpSZWNlaXZlZDogZnJvbSBpZ2xvdS5jb20gKDE5Mi4xMDcuNDEuMykgYnkgbWFpbC52eW5jZS5vcmcNCiB3aXRoIFNNVFAgKEV1ZG9yYSBJbnRlcm5ldCBNYWlsIFNlcnZlciAxLjMuMSk7IFRodSwgMTggTWF5IDIwMDAgMTY6MTA6MDIgLTA3MDANClJlY2VpdmVkOiBmcm9tIFsyMDQuMjU1LjIzNC4xOV0gKGhlbG89bnRzZXJ2ZXIyLnNuZXN5c3RlbXMuY29tKSANCglieSBpZ2xvdS5jb20gd2l0aCBlc210cCAoOC45LjMvOC45LjMpDQoJaWQgMTJzWkt3LTAwMDdKSy0wMDsgVGh1LCAxOCBNYXkgMjAwMCAxOTowNDoxNSAtMDQwMA0KUmVjZWl2ZWQ6IGZyb20gc25lc3lzdGVtcy5jb20gKHNuZS0zMC5zbmVzeXN0ZW1zLmNvbSBbMjA0LjI1NS4yMzQuMzBdKSBieSBudHNlcnZlcjIuc25lc3lzdGVtcy5jb20gd2l0aCBTTVRQIChNaWNyb3NvZnQgRXhjaGFuZ2UgSW50ZXJuZXQgTWFpbCBTZXJ2aWNlIFZlcnNpb24gNS41LjI2NTAuMjEpDQoJaWQgTEdKSDhBWVE7IFRodSwgMTggTWF5IDIwMDAgMTk6MDM6NDAgLTA0MDANClNlbmRlcjogcm9vdEBtYWlsLnZ5bmNlLm9yZw0KTWVzc2FnZS1JRDogPDM5MjQ3NzI0LkFGMjVFRjgzQHNuZXN5c3RlbXMuY29tPg0KRGF0ZTogVGh1LCAxOCBNYXkgMjAwMCAxOTowNTowOCAtMDQwMA0KRnJvbTogcm9vdCA8amFzb25jQHNuZXN5c3RlbXMuY29tPg0KUmVwbHktVG86IGphc29uY0BzbmVzeXN0ZW1zLmNvbQ0KT3JnYW5pemF0aW9uOiBTTkUgU3lzdGVtcywgSW5jLg0KWC1NYWlsZXI6IE1vemlsbGEgNC43MiBbZW5dIChYMTE7IEk7IExpbnV4IDIuMi4xMi0yMCBpNjg2KQ0KWC1BY2NlcHQtTGFuZ3VhZ2U6IGphLCBlbg0KTUlNRS1WZXJzaW9uOiAxLjANClRvOiB2eW5jZUB2eW5jZS5vcmcNClN1YmplY3Q6IFtGd2Q6IEZXOiBBbm90aGVyIFByaWNlbGVzcyAgTW9tZW50XQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvbWl4ZWQ7DQogYm91bmRhcnk9Ii0tLS0tLS0tLS0tLThCNTMzQTgyOTIyNDA3RDdDM0QzNUE5OSINClgtTW96aWxsYS1TdGF0dXMyOiAwMDAwMDAwMA0KDQpUaGlzIGlzIGEgbXVsdGktcGFydCBtZXNzYWdlIGluIE1JTUUgZm9ybWF0Lg0KLS0tLS0tLS0tLS0tLS04QjUzM0E4MjkyMjQwN0Q3QzNEMzVBOTkNCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD11cy1hc2NpaQ0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogN2JpdA0KDQoNCg0KLS0tLS0tLS0tLS0tLS04QjUzM0E4MjkyMjQwN0Q3QzNEMzVBOTkNCkNvbnRlbnQtVHlwZTogbWVzc2FnZS9yZmM4MjINCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IDdiaXQNCkNvbnRlbnQtRGlzcG9zaXRpb246IGlubGluZQ0KDQpSZWNlaXZlZDogYnkgbnRzZXJ2ZXIyIA0KCWlkIDwwMUJGQzBDQS5DMzFGN0ExMEBudHNlcnZlcjI+OyBUaHUsIDE4IE1heSAyMDAwIDA5OjEyOjQ3IC0wNDAwDQpNZXNzYWdlLUlEOiA8MDFENDc2MzQxQkRCRDIxMUI3QzUwMEEwQ0MyMDlCQTAzREY1QzZAbnRzZXJ2ZXIyPg0KRnJvbTogU2hhd24gTW9yZ2FuIDxTaGF3bk1Ac25lc3lzdGVtcy5jb20+DQpUbzogV2F5bmUgUHJpY2UgPFdheW5lUEBzbmVzeXN0ZW1zLmNvbT4sIFRpbSBTcGF5bmVyIDxUaW1TQHNuZXN5c3RlbXMuY29tPiwgDQoJR2FyeSBKb25lcyA8R2FyeWpAc25lc3lzdGVtcy5jb20+LCBKYXNvbiBDaGVsbGlhaCA8SmFzb25DQHNuZXN5c3RlbXMuY29tPg0KU3ViamVjdDogRlc6IEFub3RoZXIgUHJpY2VsZXNzICBNb21lbnQNCkRhdGU6IFRodSwgMTggTWF5IDIwMDAgMDk6MTI6NDcgLTA0MDANCk1JTUUtVmVyc2lvbjogMS4wDQpDb250ZW50LVR5cGU6IG11bHRpcGFydC9taXhlZDsNCglib3VuZGFyeT0iLS0tLV89X05leHRQYXJ0XzAwMF8wMUJGQzBDQS5DMzJBNDQ1MCINCg0KVGhpcyBtZXNzYWdlIGlzIGluIE1JTUUgZm9ybWF0LiBTaW5jZSB5b3VyIG1haWwgcmVhZGVyIGRvZXMgbm90IHVuZGVyc3RhbmQNCnRoaXMgZm9ybWF0LCBzb21lIG9yIGFsbCBvZiB0aGlzIG1lc3NhZ2UgbWF5IG5vdCBiZSBsZWdpYmxlLg0KDQotLS0tLS1fPV9OZXh0UGFydF8wMDBfMDFCRkMwQ0EuQzMyQTQ0NTANCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsNCgljaGFyc2V0PSJpc28tODg1OS0xIg0KDQoNCg0KLS0tLS1PcmlnaW5hbCBNZXNzYWdlLS0tLS0NCkZyb206IFNoYXduIE1vcmdhbiBbbWFpbHRvOmNlcGhhbG9zQGhvbWUuY29tXQ0KU2VudDogV2VkbmVzZGF5LCBNYXkgMTcsIDIwMDAgODoxOCBQTQ0KVG86IFNoYXduIE1vcmdhbg0KU3ViamVjdDogRnc6IEFub3RoZXIgUHJpY2VsZXNzIE1vbWVudA0KDQoNCg0KLS0tLS0gT3JpZ2luYWwgTWVzc2FnZSAtLS0tLSANCkZyb206IE1pY2hlbGUgTW9yZ2FuIDxBaWxpbm5AYmVsbHNvdXRoLm5ldD4NClRvOiA8bWFpbHRvOlVuZGlzY2xvc2VkLVJlY2lwaWVudDpAbWFpbDAubWNvLmJlbGxzb3V0aC5uZXQ+DQpTZW50OiBUdWVzZGF5LCBNYXkgMTYsIDIwMDAgMTA6MzEgUE0NClN1YmplY3Q6IEZ3OiBBbm90aGVyIFByaWNlbGVzcyBNb21lbnQNCg0KDQo+IA0KPiANCg0KDQotLS0tLS1fPV9OZXh0UGFydF8wMDBfMDFCRkMwQ0EuQzMyQTQ0NTANCkNvbnRlbnQtVHlwZTogaW1hZ2UvanBlZzsNCgluYW1lPSJhcHJpbGZvb2xzLmpwZyINCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NA0KQ29udGVudC1EaXNwb3NpdGlvbjogYXR0YWNobWVudDsNCglmaWxlbmFtZT0iYXByaWxmb29scy5qcGciDQoNCi85ai80QUFRU2taSlJnQUJBZ0VBU0FCSUFBRC83UTR1VUdodmRHOXphRzl3SURNdU1BQTRRa2xOQSswQUFBQUFBQkFBU0FBQUFBRUENCkFRQklBQUFBQVFBQk9FSkpUUVFOQUFBQUFBQUVBQUFBZURoQ1NVMEQ4d0FBQUFBQUNBQUFBQUFBQUFBQU9FSkpUUVFLQUFBQUFBQUINCkFBQTRRa2xOSnhBQUFBQUFBQW9BQVFBQUFBQUFBQUFDT0VKSlRRUDFBQUFBQUFCSUFDOW1aZ0FCQUd4bVpnQUdBQUFBQUFBQkFDOW0NClpnQUJBS0dabWdBR0FBQUFBQUFCQURJQUFBQUJBRm9BQUFBR0FBQUFBQUFCQURVQUFBQUJBQzBBQUFBR0FBQUFBQUFCT0VKSlRRUDQNCkFBQUFBQUJ3QUFELy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy9BK2dBQUFBQS8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8NCi93UG9BQUFBQVAvLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vOEQ2QUFBQUFELy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8NCkErZ0FBRGhDU1UwRUFBQUFBQUFBQWdBQ09FSkpUUVFDQUFBQUFBQUdBQUFBQUFBQU9FSkpUUVFJQUFBQUFBQVFBQUFBQVFBQUFrQUENCkFBSkFBQUFBQURoQ1NVMEVGQUFBQUFBQUJBQUFBQVE0UWtsTkJBd0FBQUFBREg0QUFBQUJBQUFBY0FBQUFGUUFBQUZRQUFCdVFBQUENCkRHSUFHQUFCLzlqLzRBQVFTa1pKUmdBQkFnRUFTQUJJQUFELzdnQU9RV1J2WW1VQVpJQUFBQUFCLzlzQWhBQU1DQWdJQ1FnTUNRa00NCkVRc0tDeEVWRHd3TUR4VVlFeE1WRXhNWUVRd01EQXdNREJFTURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREF3TUFRMEwNCkN3ME9EUkFPRGhBVURnNE9GQlFPRGc0T0ZCRU1EQXdNREJFUkRBd01EQXdNRVF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd00NCkRBd01EQXovd0FBUkNBQlVBSEFEQVNJQUFoRUJBeEVCLzkwQUJBQUgvOFFCUHdBQUFRVUJBUUVCQVFFQUFBQUFBQUFBQXdBQkFnUUYNCkJnY0lDUW9MQVFBQkJRRUJBUUVCQVFBQUFBQUFBQUFCQUFJREJBVUdCd2dKQ2dzUUFBRUVBUU1DQkFJRkJ3WUlCUU1NTXdFQUFoRUQNCkJDRVNNUVZCVVdFVEluR0JNZ1lVa2FHeFFpTWtGVkxCWWpNMGNvTFJRd2Nsa2xQdzRmRmpjelVXb3JLREprU1RWR1JGd3FOME5oZlMNClZlSmw4ck9FdzlOMTQvTkdKNVNraGJTVnhOVGs5S1cxeGRYbDlWWm1kb2FXcHJiRzF1YjJOMGRYWjNlSGw2ZTN4OWZuOXhFQUFnSUINCkFnUUVBd1FGQmdjSEJnVTFBUUFDRVFNaE1SSUVRVkZoY1NJVEJUS0JrUlNoc1VJandWTFI4RE1rWXVGeWdwSkRVeFZqY3pUeEpRWVcNCm9yS0RCeVkxd3RKRWsxU2pGMlJGVlRaMFplTHlzNFREMDNYajgwYVVwSVcwbGNUVTVQU2x0Y1hWNWZWV1puYUdscWEyeHRibTlpYzMNClIxZG5kNGVYcDdmSC85b0FEQU1CQUFJUkF4RUFQd0RtS011Z2JuZW96YTh5RHVBbUFBNkZ1OU0vNXZXNjVtYmtNZEU3YXFBVy93RGINCnZxWDd2KzI2MXlSeExQc3pBVzZ0ZTc3aUcvM0s1OVg4SzIvcXJLSzJ5OTFkaGlKNEc1VENOclRKNjI3SStxREdGbE5YVUwzOXJTK3QNCm4vUitqL24wck9mZFFYelMxeksremJIQjd2N1Qyc3BiL3dDQnFXVDBYcWJINlZFanhSTWJvblYzRU9HS1hqNEVqOGlQQW0xcW5idXkNCmQ3SmtFYUZiWFR1bHZkV3kyK2sxc2Uxcm1tR3RFRWJnWklUNVdOaU5mRFhBangzTlA1RXVBSzRuQU9KVzVwYUthb2RvZjBiTmY2M3QNClVINHVWWFdTd0FzYUNZQjFBR3Y1eTNnZW1zK205bzhmYzMvdm01S3pLNlY2RnpkMjRtdDRhR3laSmE3YnkxclVEampTZUl2SUhMc1ANClpSTjlwU0RmYUo1aE50Vkxqa3kwRkcyMDkxQW13OTFQYW1oRGpsM1RRUk9EdTVRM05LT1dvRnRnYTcwd0pmM0hZU2pFeUpBNzZJSkENCjNmL1F6UmpzOVFhYUZwKzhFZjhBa2xxZlZYR3JxK3NtTmNCK1pjMHdKK2xXNGY4QVZMSHI2bjArMEQwcmc5NEo5cld2SkxTQjdtdDINCmJuZlJVcjg3cWVQanV5dW1VNXJMMk5tbkpxeHJZRzdhMC9wTEs5bTJ5dCszKzJwTDMxWUFKWEhRL1k5ejlaS2JhV2lTV2tubHVtbnkNClhOc2JZNndTOTVrK0ovdldKNi8xMDZqV0hablU4cGpUcUtuTmZPdjd6S3E2MnNjaVZmVno2MDNObG1UbVBCNGNBOXYvQUZUMm8rN0UNCmJsc2NKN1BRWTJNejdLYmJaWlZSV3gxdGhiTzFyajZiSW42ZnYvZFdnZWcyblI5Ym11SEkyd3MvbzMxYytzQXFialgxWkJwMmtQdWMNCjhTNGozc1krcDludmJ1V3UzNnVkWk5qelo2aEFHNFd1dEJjOTM1dzJiL2IvQUZ0eWFjZ1ArOHF1amtaZlNuVmRpUGlxRG1CZ2NEKzYNCmZ5THBxdWo5ZHIxT0swanVEY0RQL21TSVB0bGJYVjJZYnpZUTVybzRrZ3Q5dnRkKzhnTXNmSDZnaFhDZlB5Zk93MHdQZ20yTzhGMGcNCitxMlVHdzJ1MXhaN1RJYXpVZjFpVVEvVm1ERG1aQVBrR0gvdnFxOE1tU2oyUDJQQzlTZmt0eTY2cTdYMU5kVnVJYWRvbmNSdU1xczUNCjk3V2x6OG0zZ0VSWnpyR20wTHRNNzZuNHR6L3RWMXVWUTJtc2g3ald6WUd0SmU1NzNPWExkUnBkYTV0ZE1PcXVCWmpDd1Z0eUxCRFgNClZ2dHFaSG9WM3ZkK2kzZnBFL1VBSW1TQnRyNGhoZzVEaFc1bGozdUx0eEZqaVhRMGdlUDdqazllUlhWVUJrRUNOR3VJMUJkNzNOY2YNCjNmVWRaN2xVc2RkNmxkYlMvSE5WcDlnQUlaYWV6RzF0OTNzL2ZSWHZhWFZ0dXI5WmdCZlU1b2plUjdiSEg2VFBadFM5UUlrQjRtdDINCkUyVHEvd0QvMGJHTDlmZWo0MWpicStnNDFHUUJIcTB1Wlc3amFkazR6WE4vejFmcS93QVp0QUcxM1R5S3hvMXJMV1FHOGJmY0d0WEINCmhwOFUrejRKVWU2N1I5QWIvakU2U1FTN0N2cWtSRFcxT2FmNjhXTTNLN2ovQUZ2cnpITkdOaVhQaHpXbmRzWVlJZCtpWTMxQituYzENCnZxczJmby9TWENqcnVRRzdIWTlMbWdSd2g5T2ZhSHZzYnNra3RKZElobkxtKzBzYzNsdXowM2Y5dC80VnVvWFFqeFNvNlBwUjYxanQNCnJMcm1XVnZxMm0wRTZRNDdhM011MnNyc2M3YzF6MjE3L1luWjFURnNiYWFTOFZZN0RaWS9ZNG5iRXViNmZ0ZjZuOGhjVlYxN1ByYlcNClBXY2FHeUJ1YzFocjRmOEFUZTJ5dDdiZjhJNngvcS82S3o5S2lWZldITHJaVlcrdGpqVWZVOWIyQXVMWE8zdXNidGRTMWp2WitmOEENCnYvcGZVU3RzakQ2ZHJsNFMvWko3RTV0RGkwaTAxNjdDSE5KSkpEWDcyTWFOM3ViN1dLRm1ZV08yM3ZEQzBCejNEVGF3enNmOUozN3ENCjVkMzFxTmx6NXJvM1h0TFhXMlEyR3lOTjBuMWY1Ti8vQUlFbnQ2clViUFhlK3Bwc2U5ekw5eGM2QTBsN05qcmZaOUpqWE0vd3U5SzANCmlBajgybWowanMxZ2x3Y1RXUjdUdExaTWVMM2ZudDk2aFprMStrNnd1TG0xTk5qNElra1MzMjdIYnZVZDdseGVWMUhLYzE5TG1VV3YNCmJBL1R0ZTF2dGR5Nm1pMzMrN1ovd1RQN2FGZDF2SnBheVhDeVcra3lzc3NhMXRaY1FHK2w3S1cyYjJmbmZwUDNMZjBpVnJqSFFHRy8NCmlPamUrc0daMDNOZFhWY00yMXA5d3BjSE5xQkE5MXJtTzlPaDMwZmZaZnYvQUp6OUZXdVQ2bm5zK3l0dzhYQkdOaHliRHNkWSsyb3QNCmN3VzNNdGR0MmZhZjBkYjJXTnQvNzRwdnl6dWNYTTl4SkxvZDNQdTUrYVF6U09EWVBnZi9BREpDajJhazVFazYxZjhBTGQ1LzE3SzMNCk1jMTNvQmdEcVMzNlFFN2ZVRzMvQUFuMG5lNUd6V2lwOWJBSWVUQnJEaG9QekExdjVyWHRmK2IvQURxMkhaalQ5TGVmaUovaW1HVlUNClB6NVBqWXpjZFBvNjJNZDlINktPdlppNGZGLy8wdWYyRkxhamJVdHFLVU8xV01FK2xidmNKYWRKQWtnSDZZL2s3OXY5dFEyb21PQ0gNCk9EUWRScVlsbzdlOTArMzJwcFpjSDg0QjNzZmd2b1NRSWN4d0FCYzVvRU8raVQ2a3V0Y3lObXorYi83YlRiV3VhQ0c3bUJ6ZFcrd3UNCk1ibTNlcDdkMjU3ckgrei9BRWlzR3R1NzB5LzB3WEUxY0ZwMCtpZHczZXBzK2haK1ltZTExVHZVc0RXTmNOeGZ0MnQxOW04ODd2WnMNCmI3UFo3MDEwQVVSSkh0RXVhVDdyQTB6RHc1NzJ0ZTdkN0cyZjRObjZTcE0xemkrcGxRSnZKMkgyaHozZ08vU2I3RDlKNzN1ZmRhNysNCnAvT0lteklnT3VKSk8wMldFZ0FTRzd2U2NXdC9RdCtreG43L0FPaXJVSDJ0TFcrb0diZ1dnRGM1dFU3Zys5bStwcnJIYkh1OXlTdEQNClNFVmwxdThFRnJRQldXZ2duY0FMUDV4Mi93RFJmdWYrZkZCejdMRzBtbHJMN0RMeTQ2TWUyWTlObjAyMTIvNFQvUjFJNDlOelpJTFENCkFYZW9RQzBIYzMybGp2NTVyZjV2MmI3RkIxYnh0cXNpQ0ErNmt2UHRCRzJxMXBqNkRuZjhYNlZiL3dETVFVWE5lU2JuOS9kL0FLSm4NCmlPTzhLWmFCWVlBZ2NEbnNPRWlwQjBhRVphRUdaanZROVgvZW96UGhQeVVIYjQ4dU9FVS9QNzFCMEVmM2xKSm1OYXlIeTlYL0FIci8NCkFQL1R5dHFSYXB3a1FuRktQYWlZN0NYT0VBaU5TWkErOXBidFRRaTQ1WXh6dCtyWGdURXpJOEIrY21uWmt3bXNrZk5uWldkdXdTeVgNCk5hSEFDUnI3TnJkN1dOWnA5TnpOL3dENEdtYTZRMHRjUTF4bG03MlR1TzM2TGg2dithei9BQWFjMk5zT3pUYzRselc3aHZMV2ZUZC8NCklheWY2KzlTZFdBWE9aRFE0K3B1SmtBQUN1WmVIVi9tKzdaL2hFeDBOQ2dZUTRHQzVvN2phOEdaTG5OMys3MnRZZjV0M3MyZnphaWINCmE2SE9CNDFkcVpEV3M5M3NkWCtZMW4rRVJyS3c0Z0F5MEFOQTl4TFJPNXJXdDNmVC9jL1BReWJHVHlkbzNFeElhVE9tMTJ5dG0yejkNCkw3LyszVWtzTEt6T2tibmN0THh1a2x2cHRlMXU1enVkN2JQVTlYL0JvVm5xdXFFRnhMZHJtdGFJRFhFN2ZmUzNhOXZwL25YZjhWNm4NCnFLd0h1RG5GcDJ1RWxwN2lOR0UyZXA2anZjZm9iV0t2bDFCdFg2Y05jMmxvSmVXbmMrMXZzMlcxYnRsM3F1LzhFUlFkbWc1cEZqcDUNCmsvM0tKYWlSTGlRSTFJSTg1VEVKN253bUJZUFh6ci9tb2lQOVlVSERUK0VJcENnNElnTDVaUlZYZDMxbi93QjNKLy9Vem1UclB5NFQNCmxlZEpKNjZXL1QvQjJmUkR5aTRzK3VOc1RzZnpISDUzMHY1TzVlYkpKcDJYWXZuajVoOU12MmJtK3Zzbll6MDl2MHBuM2IvK0gzZlQNCjlQOEFRZWgvTXFzZlQvUXpNYWVuNm5PNld6SCtEMzcvQUUxNTRrbU9nSDBmOUo3ZG0zWkxZbWQzSjliZi9oOXYwOW4vQUFmODJwSGYNCnYrWTI3dnBmU2Q5SDFQNVAwL1QvQURQNTMvQXJ6WkpKTDZSK2tuM2VwdW16YlBHL2MvMXVmemR1L3dENnovTm9UdlI5T3lOc1JySFANCkR2NTMxdmQ5TFovNkxYbmlTU1RzOWkzZ3p6dWQrVnlaMExqMGxJSEw2dldsRGN1V1NSVS8vOWs0UWtsTkJBWUFBQUFBQUFjQUJBQUENCkFBRUJBUC9pREZoSlEwTmZVRkpQUmtsTVJRQUJBUUFBREVoTWFXNXZBaEFBQUcxdWRISlNSMElnV0ZsYUlBZk9BQUlBQ1FBR0FERUENCkFHRmpjM0JOVTBaVUFBQUFBRWxGUXlCelVrZENBQUFBQUFBQUFBQUFBQUFBQUFEMjFnQUJBQUFBQU5NdFNGQWdJQUFBQUFBQUFBQUENCkFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFFV053Y25RQUFBRlFBQUFBTTJSbGMyTUENCkFBR0VBQUFBYkhkMGNIUUFBQUh3QUFBQUZHSnJjSFFBQUFJRUFBQUFGSEpZV1ZvQUFBSVlBQUFBRkdkWVdWb0FBQUlzQUFBQUZHSlkNCldWb0FBQUpBQUFBQUZHUnRibVFBQUFKVUFBQUFjR1J0WkdRQUFBTEVBQUFBaUhaMVpXUUFBQU5NQUFBQWhuWnBaWGNBQUFQVUFBQUENCkpHeDFiV2tBQUFQNEFBQUFGRzFsWVhNQUFBUU1BQUFBSkhSbFkyZ0FBQVF3QUFBQURISlVVa01BQUFROEFBQUlER2RVVWtNQUFBUTgNCkFBQUlER0pVVWtNQUFBUThBQUFJREhSbGVIUUFBQUFBUTI5d2VYSnBaMmgwSUNoaktTQXhPVGs0SUVobGQyeGxkSFF0VUdGamEyRnkNClpDQkRiMjF3WVc1NUFBQmtaWE5qQUFBQUFBQUFBQkp6VWtkQ0lFbEZRell4T1RZMkxUSXVNUUFBQUFBQUFBQUFBQUFBRW5OU1IwSWcNClNVVkROakU1TmpZdE1pNHhBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUENCkFBQUFBQUJZV1ZvZ0FBQUFBQUFBODFFQUFRQUFBQUVXekZoWldpQUFBQUFBQUFBQUFBQUFBQUFBQUFBQVdGbGFJQUFBQUFBQUFHK2kNCkFBQTQ5UUFBQTVCWVdWb2dBQUFBQUFBQVlwa0FBTGVGQUFBWTJsaFpXaUFBQUFBQUFBQWtvQUFBRDRRQUFMYlBaR1Z6WXdBQUFBQUENCkFBQVdTVVZESUdoMGRIQTZMeTkzZDNjdWFXVmpMbU5vQUFBQUFBQUFBQUFBQUFBV1NVVkRJR2gwZEhBNkx5OTNkM2N1YVdWakxtTm8NCkFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFHUmxjMk1BQUFBQUFBQUENCkxrbEZReUEyTVRrMk5pMHlMakVnUkdWbVlYVnNkQ0JTUjBJZ1kyOXNiM1Z5SUhOd1lXTmxJQzBnYzFKSFFnQUFBQUFBQUFBQUFBQUENCkxrbEZReUEyTVRrMk5pMHlMakVnUkdWbVlYVnNkQ0JTUjBJZ1kyOXNiM1Z5SUhOd1lXTmxJQzBnYzFKSFFnQUFBQUFBQUFBQUFBQUENCkFBQUFBQUFBQUFBQUFBQmtaWE5qQUFBQUFBQUFBQ3hTWldabGNtVnVZMlVnVm1sbGQybHVaeUJEYjI1a2FYUnBiMjRnYVc0Z1NVVkQNCk5qRTVOall0TWk0eEFBQUFBQUFBQUFBQUFBQXNVbVZtWlhKbGJtTmxJRlpwWlhkcGJtY2dRMjl1WkdsMGFXOXVJR2x1SUVsRlF6WXgNCk9UWTJMVEl1TVFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBZG1sbGR3QUFBQUFBRTZUK0FCUmZMZ0FRenhRQUErM00NCkFBUVRDd0FEWEo0QUFBQUJXRmxhSUFBQUFBQUFUQWxXQUZBQUFBQlhIK2R0WldGekFBQUFBQUFBQUFFQUFBQUFBQUFBQUFBQUFBQUENCkFBQUFBQUFDandBQUFBSnphV2NnQUFBQUFFTlNWQ0JqZFhKMkFBQUFBQUFBQkFBQUFBQUZBQW9BRHdBVUFCa0FIZ0FqQUNnQUxRQXkNCkFEY0FPd0JBQUVVQVNnQlBBRlFBV1FCZUFHTUFhQUJ0QUhJQWR3QjhBSUVBaGdDTEFKQUFsUUNhQUo4QXBBQ3BBSzRBc2dDM0FMd0ENCndRREdBTXNBMEFEVkFOc0E0QURsQU9zQThBRDJBUHNCQVFFSEFRMEJFd0VaQVI4QkpRRXJBVElCT0FFK0FVVUJUQUZTQVZrQllBRm4NCkFXNEJkUUY4QVlNQml3R1NBWm9Cb1FHcEFiRUJ1UUhCQWNrQjBRSFpBZUVCNlFIeUFmb0NBd0lNQWhRQ0hRSW1BaThDT0FKQkFrc0MNClZBSmRBbWNDY1FKNkFvUUNqZ0tZQXFJQ3JBSzJBc0VDeXdMVkF1QUM2d0wxQXdBREN3TVdBeUVETFFNNEEwTURUd05hQTJZRGNnTisNCkE0b0RsZ09pQTY0RHVnUEhBOU1ENEFQc0Eva0VCZ1FUQkNBRUxRUTdCRWdFVlFSakJIRUVmZ1NNQkpvRXFBUzJCTVFFMHdUaEJQQUUNCi9nVU5CUndGS3dVNkJVa0ZXQVZuQlhjRmhnV1dCYVlGdFFYRkJkVUY1UVgyQmdZR0ZnWW5CamNHU0FaWkJtb0dld2FNQnAwR3J3YkENCkJ0RUc0d2IxQndjSEdRY3JCejBIVHdkaEIzUUhoZ2VaQjZ3SHZ3ZlNCK1VIK0FnTENCOElNZ2hHQ0ZvSWJnaUNDSllJcWdpK0NOSUkNCjV3ajdDUkFKSlFrNkNVOEpaQWw1Q1k4SnBBbTZDYzhKNVFuN0NoRUtKd285Q2xRS2FncUJDcGdLcmdyRkN0d0s4d3NMQ3lJTE9RdFINCkMya0xnQXVZQzdBTHlBdmhDL2tNRWd3cURFTU1YQXgxREk0TXB3ekFETmtNOHcwTkRTWU5RQTFhRFhRTmpnMnBEY01OM2czNERoTU8NCkxnNUpEbVFPZnc2YkRyWU8wZzd1RHdrUEpROUJEMTRQZWcrV0Q3TVB6dy9zRUFrUUpoQkRFR0VRZmhDYkVMa1ExeEQxRVJNUk1SRlANCkVXMFJqQkdxRWNrUjZCSUhFaVlTUlJKa0VvUVNveExERXVNVEF4TWpFME1UWXhPREU2UVR4UlBsRkFZVUp4UkpGR29VaXhTdEZNNFUNCjhCVVNGVFFWVmhWNEZac1Z2UlhnRmdNV0poWkpGbXdXanhheUZ0WVcraGNkRjBFWFpSZUpGNjRYMGhmM0dCc1lRQmhsR0lvWXJ4alYNCkdQb1pJQmxGR1dzWmtSbTNHZDBhQkJvcUdsRWFkeHFlR3NVYTdCc1VHenNiWXh1S0c3SWIyaHdDSENvY1VoeDdIS01jekJ6MUhSNGQNClJ4MXdIWmtkd3gzc0hoWWVRQjVxSHBRZXZoN3BIeE1mUGg5cEg1UWZ2eC9xSUJVZ1FTQnNJSmdneENEd0lSd2hTQ0YxSWFFaHppSDcNCklpY2lWU0tDSXE4aTNTTUtJemdqWmlPVUk4SWo4Q1FmSkUwa2ZDU3JKTm9sQ1NVNEpXZ2xseVhISmZjbUp5WlhKb2NtdHlib0p4Z24NClNTZDZKNnNuM0NnTktEOG9jU2lpS05RcEJpazRLV3NwblNuUUtnSXFOU3BvS3BzcXp5c0NLellyYVN1ZEs5RXNCU3c1TEc0c29pelgNCkxRd3RRUzEyTGFzdDRTNFdMa3d1Z2k2M0x1NHZKQzlhTDVFdnh5LytNRFV3YkRDa01Oc3hFakZLTVlJeHVqSHlNaW95WXpLYk10UXoNCkRUTkdNMzh6dURQeE5DczBaVFNlTk5nMUV6Vk5OWWMxd2pYOU5qYzJjamF1TnVrM0pEZGdONXczMXpnVU9GQTRqRGpJT1FVNVFqbC8NCk9idzUrVG8yT25RNnNqcnZPeTA3YXp1cU8rZzhKenhsUEtRODR6MGlQV0U5b1QzZ1BpQStZRDZnUHVBL0lUOWhQNkkvNGtBalFHUkENCnBrRG5RU2xCYWtHc1FlNUNNRUp5UXJWQzkwTTZRMzFEd0VRRFJFZEVpa1RPUlJKRlZVV2FSZDVHSWtablJxdEc4RWMxUjN0SHdFZ0YNClNFdElrVWpYU1IxSlkwbXBTZkJLTjBwOVNzUkxERXRUUzVwTDRrd3FUSEpNdWswQ1RVcE5rMDNjVGlWT2JrNjNUd0JQU1UrVFQ5MVENCkoxQnhVTHRSQmxGUVVadFI1bEl4VW54U3gxTVRVMTlUcWxQMlZFSlVqMVRiVlNoVmRWWENWZzlXWEZhcFZ2ZFhSRmVTVitCWUwxaDkNCldNdFpHbGxwV2JoYUIxcFdXcVphOVZ0Rlc1VmI1VncxWElaYzFsMG5YWGhkeVY0YVhteGV2VjhQWDJGZnMyQUZZRmRncW1EOFlVOWgNCm9tSDFZa2xpbkdMd1kwTmpsMlByWkVCa2xHVHBaVDFsa21YblpqMW1rbWJvWnoxbmsyZnBhRDlvbG1qc2FVTnBtbW54YWtocW4ycjMNCmEwOXJwMnYvYkZkc3IyMEliV0J0dVc0U2JtdHV4RzhlYjNodjBYQXJjSVp3NEhFNmNaVng4SEpMY3FaekFYTmRjN2gwRkhSd2RNeDENCktIV0ZkZUYyUG5hYmR2aDNWbmV6ZUJGNGJuak1lU3A1aVhubmVrWjZwWHNFZTJON3dud2hmSUY4NFgxQmZhRitBWDVpZnNKL0kzK0UNCmYrV0FSNENvZ1FxQmE0SE5nakNDa29MMGcxZUR1b1FkaElDRTQ0VkhoYXVHRG9aeWh0ZUhPNGVmaUFTSWFZak9pVE9KbVluK2ltU0sNCnlvc3dpNWFML0l4ampNcU5NWTJZamYrT1pvN09qemFQbnBBR2tHNlExcEUva2FpU0VaSjZrdU9UVFpPMmxDQ1VpcFQwbFYrVnlaWTANCmxwK1hDcGQxbCtDWVRKaTRtU1Naa0puOG1taWExWnRDbTYrY0hKeUpuUGVkWkozU25rQ2VycDhkbjR1ZitxQnBvTmloUjZHMm9pYWkNCmxxTUdvM2FqNXFSV3BNZWxPS1dwcGhxbWk2YjlwMjZuNEtoU3FNU3BONm1wcWh5cWo2c0NxM1dyNmF4Y3JOQ3RSSzI0cmkydW9hOFcNCnI0dXdBTEIxc09xeFlMSFdza3V5d3JNNHM2NjBKYlNjdFJPMWlyWUJ0bm0yOExkb3QrQzRXYmpSdVVxNXdybzd1clc3THJ1bnZDRzgNCm03MFZ2WSsrQ3I2RXZ2Ky9lci8xd0hEQTdNRm53ZVBDWDhMYncxakQxTVJSeE03RlM4WEl4a2JHdzhkQng3L0lQY2k4eVRySnVjbzQNCnlyZkxOc3UyekRYTXRjMDF6YlhPTnM2Mnp6ZlB1TkE1MExyUlBORyswai9Td2RORTA4YlVTZFRMMVU3VjBkWlYxdGpYWE5mZzJHVFkNCjZObHMyZkhhZHRyNzI0RGNCZHlLM1JEZGx0NGMzcUxmS2QrdjREYmd2ZUZFNGN6aVUrTGI0MlBqNitSejVQemxoT1lONXBibkgrZXANCjZETG92T2xHNmREcVcrcmw2M0RyKyt5RzdSSHRuTzRvN3JUdlFPL004Rmp3NWZGeThmL3lqUE1aODZmME5QVEM5VkQxM3ZadDl2djMNCml2Z1orS2o1T1BuSCtsZjY1L3QzL0FmOG1QMHAvYnIrUy83Yy8yMy8vLy91QUE1QlpHOWlaUUJrQUFBQUFBSC8yd0NFQUFZRUJBUUYNCkJBWUZCUVlKQmdVR0NRc0lCZ1lJQ3d3S0Nnc0tDZ3dRREF3TURBd01FQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd00NCkRBd0JCd2NIRFF3TkdCQVFHQlFPRGc0VUZBNE9EZzRVRVF3TURBd01FUkVNREF3TURBd1JEQXdNREF3TURBd01EQXdNREF3TURBd00NCkRBd01EQXdNREF3TURQL0FBQkVJQWVBQ2dBTUJFUUFDRVFFREVRSC8zUUFFQUZEL3hBR2lBQUFBQndFQkFRRUJBQUFBQUFBQUFBQUUNCkJRTUNCZ0VBQndnSkNnc0JBQUlDQXdFQkFRRUJBQUFBQUFBQUFBRUFBZ01FQlFZSENBa0tDeEFBQWdFREF3SUVBZ1lIQXdRQ0JnSnoNCkFRSURFUVFBQlNFU01VRlJCaE5oSW5HQkZES1JvUWNWc1VJandWTFI0VE1XWXZBa2NvTHhKVU0wVTVLaXNtTnp3alZFSjVPanN6WVgNClZHUjB3OUxpQ0NhRENRb1lHWVNVUlVha3RGYlRWU2dhOHVQenhOVGs5R1YxaFpXbHRjWFY1ZlZtZG9hV3ByYkcxdWIyTjBkWFozZUgNCmw2ZTN4OWZuOXpoSVdHaDRpSmlvdU1qWTZQZ3BPVWxaYVhtSm1hbTV5ZG5wK1NvNlNscHFlb3FhcXJySzJ1cjZFUUFDQWdFQ0F3VUYNCkJBVUdCQWdEQTIwQkFBSVJBd1FoRWpGQkJWRVRZU0lHY1lHUk1xR3g4QlRCMGVFalFoVlNZbkx4TXlRMFE0SVdrbE1sb21PeXdnZHoNCjBqWGlSSU1YVkpNSUNRb1lHU1kyUlJvblpIUlZOL0tqczhNb0tkUGo4NFNVcExURTFPVDBaWFdGbGFXMXhkWGw5VVpXWm5hR2xxYTINCnh0Ym05a2RYWjNlSGw2ZTN4OWZuOXpoSVdHaDRpSmlvdU1qWTZQZzVTVmxwZVltWnFibkoyZW41S2pwS1dtcDZpcHFxdXNyYTZ2ci8NCjJnQU1Bd0VBQWhFREVRQS9BT2NnRTVmSlVudWtJMUp2QmdNcUxLQ2NhUVAzRER3YitHV1JXU1lBZ0hmRXNWL3hVRytLdGpiRlcvVUENCjZFL1JpcmZOK3dKR0tyNnkrQStqRlZXTW52VUhDRlZSeFBVL2ZoVnVvOGNWWDRsWEhwa1ZhcFVDbVJJVnNZT0ZYWk5rNm95SlZzTW4NCmNWeUt1cTNaUlR0a3dxN2svZ0JoVjFXUFUvZGlxNEtPNVAwNUVxdkEyeUt0Z0d1V0t2V3RkalRBUWtGZjZaUFhmSThMSzF3akZNYVkNCmx4QnBqU0ZoV28zeHBWSW93T3hwN2RzQmlyVldHMUJnNFZVSloyajZLMlBDcUNlVFdMbVQwclZZbFk5REkzSDllUENxNCtVdk9rMWUNCldxUVd5OXpHcGMvZ0tZOEt0RHlCcURmNzJhOWNTRHdqVUovRVpZT1RJRlQvQU9WYStYcWszQnVMazkya2tORDkySlR4SWkzOGsrVmINCmNmQnAwUkk2TTlXUDRrWkJlSkQzM2tueTVjOHVOdDlYWS90UUVydjQwcVJpeEpTYVR5TnFWbVNkSzFTUlIyU1NvUDNyeVgvZ2x4UXENCnd6K2NMVDRidU5iaE84ZzJKK1hISXRpYzJsOUpLb01pRkdwdmdLb2hwV0cvR3BQNnNDdUVsQjRlMktyVElRUGZGVk16bkZWbnJlT1INCkt1TW51Y0NyVEp0aXEzbUtHb3JpclRQeEFDR2xlbzdZcXRGdzRVZ2loL3ljVlcvV2ZmRlcvWE5LNHF0TTVyaXJYcjRxNHpEc1RYRlYNCnBtYjMrbkZXak8xZWc2ZDhWV2lkdUp4Vlk4c2g3MUdLcmZVeFpOQ1VEcVNQbGlnclRLSzljVUxPZUxKb3liWXF0TWxPcHhWYVpEMTcNCllxMFhxRFhwN1lxcEJtVVZVbWxlaHhWb3pEdnNja0ZhTHRYQ3JSYzB5Q3JHYzhxKzJHMVdxeHFjZUpWaGMxeDRMM1ZZek5USGdyZFMNCnQ1RTljZUpqU3hub2NlSmFVM2xIU3UvWStHUEV2Qy8vME9kSzZudm1SSUtsdW9jZjBnckwwS2o4TXFwbEJIYWNTcU90SysrVEN5VEENCkY2RG9QYkRUQzEzWFluYkFWdGNxcFhjWUFWdFdXZ0hRWWFXMXczeFcxNDY0MGxVVlQxd2dLcXJIdFhyOHNsU3Q4RjhCOUpvZnVOTVANCkN0cXR2WVhVN0FRUXl6TjRSb3ovQUlxRGp3SXRPTGJ5SjV6dWxEVytrejhEKzI0Q0wvd3hHRGdXMDBYOHBQTzVoTWhodDFidEcweTgNCmo5MjJQQ3BLVjNYa0h6dmJieTZUS3dQZUZrbEgvQ25IaFJ4SlJjV3QzYkVpNXRwb1NQOEFma2JwK0pGTUhDeTRsSlRFMndZVjhLNzUNCkdVVWdyakdCMUZQbnRrYVRiWUhoMC96Ky9DRnQxTjZlR0ZGdGluamd0YlhxVnB1Y0JGcmE3a3VOTGE0TnZrNlcxNGFweEFVbFVEaW0NCkZEZk1ZYVRiWjZZMHRxWkhiQXRyR1hmQVF0dGJZMHRyV0hMcmpTMm90Q3BxRCtvSDhjRkxhNUduaXA2Y2pMVG9BVFQ3c05MYUlYVTcNCjVkaXl5ZjZ3eUpLQzIyck9WSUVRKzg0Q1ZRc21vM0RHbkJRTWltbEI3cTZPL0w3aFRGa0FwbTRtTzVjMXhUd3RHUmpVY2pUdHZncGINCmFMOWdOdkhBUWtGYUhLOThGSmR6WTQwcTE1TzljRktvTktLOWNWV05LSzdISWxWcG1QamdWb3piWXF0TXpEdmlydlh4VnIxSzk4VlUNCnpJcDJ4VllTcW1vcUQrR0thY1p0dmlGY1NWcG9UcjQwOXNGclRabTI2NDhTMDBKL2V1UEV0TkdZZU9QRXRPOVhKQU1Xakp0aVF0ckMNCjUrV1J0UEV0TGUrRUx6Vzh2a2NOS3Q5VUhZSEdtVFhQZWxjQjJWb3lMM09EaVZyMUJRamwxeVFXMXBrU213eEtxYlMwSDA1SGlWWTANCm9JNkVtdmhpSksxems4RGg0d3JSOVV2OWtCUjFOY0hFeTRYSGtlK1JNbDRWcFU5emtlSmVGYlJpZGpYRHhvcHhBclVWeDQxQWFJcmcNCjRtZExlSHNNZUphV0ZGcjB4NGxwLzlIbWtkUERNcVNvSytIK2xRdFNsUmxUS0NNczJvV0ZhVndySkhMTFhiSkJyVkZJNjF3RlY0cFcNCmxkeDFQYkRHS3E4RU04ekNPTkdra1kwVlVWbUorZ0RKOEtzaDAveUQ1MXZEKzQwYTZLbmNNNmNBUi9zamdwV1RXSDVKZWNaL2l1UHENCjltdi9BQlpJV1AzUnEvOEF4TEpjUVJ4TWkwMzhpNENGTjVySVkvdHBBZ0g0c1NmK0Z4NHd2RW45dCtVbmtxMmNlckRjWEpYYjk1TVMNCnAvMktoY0hHdkV5Q3o4bytVdFBTdG5wTVViam9aSXZVSDN5Y3NqeEZpWkpoYXkrbjhFUWhSUjBWUUVIL0FBdE1CWGlhdW5ibFgwd3gNClBkRFhJYm80bHdoWjRoUlpRZXdydCtHRUVoSUt5T3d1UFVMa0FxQlVzdy9zcmg0bFEyclgrZzIwUUdvNmxhUXhqOWg1a1g4R0pKeDQNCm1iRU5UOHkva3hSbHUvUXYzLzRwdDJrMy93QlpWQS80YkFTZ2hoT3JhMStVYmx4cC9sMjdNaDZOSE45V0ZmSGp5ay80amdZOEpZWGYNCnRiUGV0SllvOXJhbGFHM2tmMTJyNG1RaGR2OEFZNFd3Y2xFQ1FHcGJidnRpRXJnYTlkOG53cXZVcU8yUklWVlYvaEdCVy9wR1NWVVgNCnhyaXFvdlRwWEZWMVBsOStTQ3QxSDh3eEtxYkhmclhJcTBUWEZWaDY0cTdGVnZIZXB4VnBsRmNWZFFaRWhXdUl4NFZDeG8rK1BDeVcNCm1LdVBDdDBwTmFFa2tIYkdsNGtPOFV3T3cyeXJnS3FaWWpzYTRPRWhrRmhsTmR3Y1dUWHFueEl4VllTZTV4S3FEeURya1ZVL1dCNjcNClpFcXRNcTE2NEZXbVdncmlxMzF3Y1ZhOVgzeFZhWmQrdUt1OVgzeFVMV2w3NHNscGtya1NyaXlCZmZBcXoxbDhUa3VNSzcxcTlEWDYNCk1lTUs3MXoycmp4aFhlc2FqWTc1RTVGNFhlcEwvTFQzd2VJdkMxems3NCtLRThMUkxuSW5Ja1JkOGVEeEU4TFc5QWE0K0l2QzFUZmYNCkV6dGVGM0JUODhIRXZDdDRqd3lCa1Y0Vzl2Q21Ja1Y0V3FiMEpQMFpQaVhoYUk4ZC9BMXdjU1JGcmZ4dzhTZUZxbXdIaGp4SzBSVEcNCjdWb2l1S3RjUWV1NHhZbG9vTVZDMGltTEpyMzdZcTFzY1ZmLzB1WWVvb0d6RDVabE1iUTE2MVpZZllaQ1lwSUtJZ1loaFFWcU1BWnoNCjVJa00vWUFaS21vY2xWT1IrSWs4RDJ4QVVwam9XcjNHa1hMelJSVzk0cm1yUjNzS3lvRC9BSk5TdVNSYk9OTC9BRGoxRzBVSytoNmMNClIzTnVoZ1AwY1FjRWlWVHkxL1BLejRmNlZZWGNEL3RHR2NTS0I0MGtGY2paVzArdC93QTYvTEU2cWozZDFDemJVdVlPWUh6NGxzalMNCktUelRmekI4dTNNZ0VPcldEQW5wTUdoUC9EQlJoRVZwTnIzekxva1VmcTNGOVp4eEQ5cU82akIrZ0FzY2VFTFNWUy9teDVFdG96RTINCm9UVE4wcEhHOGcrODhSK09HbG9KTmVmbmw1Y2hrUDFEU3JpNVBacERIQ3RmK0hPTkVxQUVodnZ6NTE2WC9lSFM3TzNYc3psNVgrNEYNClZ3OEJUd2hJTDM4M1BQMTBDRHFodGdkdU50R2taSHR5SUp3Y0hlbW1QM3ZtTFhiODhyelVycTZQZzh6a2ZjQ0IrR1BDdElEMUJrdUUNCkpjWkFUVTduSXlDUTM2cDhmdnlOSnRvU29PNmcrMjJTQVZ2MS9EYytHR2xiV1ZxL1pwODhkMVhlckwycGlxNVRJVHVjRktxb0FldSsNClQ0V05xeXFhL0RVZkkweHBiVmw2ZUgwNDhLMnZDUmtWUFhKQUt2RVFKcUF1M1lIZkdsdFlVWTFxcEdDZ3RxWlExeU1ndHJTaHIxd0wNCmJXTEoxY1ZkVEZXd29wMHlRQ3RjRGpTdTQrT05MYlZCalN1NGpHbGQ2UXgzVzFqV2tiZnNpdmppWTJtMFBKcGxlaDRuSThBWGlLRm0NCjBxNVRkS1NWN2Q4Z2NSdGtKSUdaTGlPdk9ObEhlbzJ5TXNSQVNEdWwwczN4RVZOUGJLREttYWlMaERVQTFQamxmRWdob3pBZGZHbVANCkVpbW1tZmtSeE5NaDRyS25lbzU5c2ZGV25jejBydmo0cTAxV1R3cjc1RTVscDFXUGZBY3lRSEhsM09SOFNYZXlwckpDWlNBdkNxUnUNCks0ZUlwcFlCdmxhMHY0MDY0RGEwM2tkMW9Pb01rQ3RPb01iVm85c0ZCV3lLMEE2OWNVdW9UdUNLWVZhSUZNVmNWOE1WV2NXT0t1STMNCjM2NUlLNmd4UEpXaUQyNjVYdXEzalRyaEZyYnFEQ3RseEcyVHBqYTNqNDRyYlZCaHRiYWJ4SFFkY2JRc0J4Q1Eyd0ZNTEpZUUs0b0wNClZXOE1VVy8vMCtNUStaYlFiTmJjUjQ1bE1MVnBkVHM3d3grZ09ETDFVaW1RbnlTRXhXV0NNOHBXNEp0OFdSaklObVRrckpjV0RmWnUNCmEvT2xjdkZOTWVUWm10VXFCTXJVNmdBMXhUYTFMd2xxS0R4N0U5TWl4VmpjbGdLVU9HbFZWbFlqN1FIdGpTdHBJUUR5WWtrN1kwcU8NCnRuSmoySituQ0FxSVgxS1lhVmRXWHh3MHEweWJZS1NGaGxHTmxrcCtvMURSUVQ0bkZYQ1NhbmJHbGR6YnVkc2pTdFZIaWNhU0Z3SUgNCmY3OGFUYTRPdE91RUJiYjVERFMydUVsTWFSYTlaS2pHbHRVVm1JMkFPTkp0RVJzNTNZNU9tS29BV0I0NzBCTk8yMk5LcVdwOWF6am0NCjRrZW9UeWpiN1E0OUNjZUZWWklXNmtVcmtTRlZCREo5R0NsYmFKcWI3NDBxazY4VGpTckdCSTJ4cFZ0Y2FaVzJGNWJZMGkyL1R4cGINClhDUGJKQUp0djB4alMyMnNXK05MYmZBWWFSYmZwaitXdVBDdHVDQUg3UDM0OEsydkViTit5UG94NFZ0Y3NMVTZZOEsyMzZMZUdPNkMNCjM5U1J0bkZWUFVIcGdOMG9MQU5SdGtXL3VGVmFjWkdCQUZOd2FacE5UWWs1T01GQ05HYTlNcU1tMDB0OVBCWlkwNHAxcDlHQ2d5MmENClZEWGZEd3JzM3dHTkxzM3g4TUhBdXp1Rk1lQlhVT0drVXVBUEU1SUpDM2hqYXJ1SUdTc0phUFRCc3JoMHhwVy9pOGNnVldnYjc0cXUNCjRnZE1WZDlKK2pHbGR4Qi90dzBWVzBPQ2xkUTRGZFE0VmF5UUtISDUweFZwZ0NOL3Z4MlN0NCtHRVVyUmpxY095RnZGY2phS2RodGENCmRSU1BmeHhXbHRDUGYzeHREWEVENTRRbHB1bUcwMnR3b0swcFUxeHBELy9VODYwYnh6S2FrZnBKUDFqZnd3UzVNbzgwODF1UGxwVEgNCnNPTFpqUjV0dVRreGdWNmcwSnpLRGpqa3FDZVVVS3NWUGVoeFNySmZYcWprSlNBUHB4VkV4YXZmZ1Y5V3ZzUU1rRlYxMSs3WHFxdWUNCjlkaGhWVlR6SktBT1VRTlBjNHF5SFFOV2E3aWxkVjRoV0E0bjNGY01SYXBwNnNuYkxPRlcrVEhjazF5S3VCSU8rMktRMFNwUFhGa3MNCkwrQnlKVnIxRDBHNXdLNHlHblFEM3hWeU14T3h4VmZWdTlUaXJxZjVPS3FncFhmRUt1VmtyMXI4aGsxVkZJUFN2MGpGVmVOVHR0aXENCklqUTRxaVlveWRsM0o4TUlWRXh4emNmaFQ1MDNBT0ZWVlk1ajFQSHg5c2lWVkZ0NTZVSWFuamlyUnRYL0FHaFFlNXhWWTBLamFuM2INCjRxcE5IVHRpcTMwbTlzVmJFSkdLcmhDVGlyZm9EdmlxNzBoNGpGVnlvSzRxdjlGZkRDRlhpTTAyQXBoVmNJSEpwU252aXFvSUNCMS8NCkNtS3JoYXMyL0xGVy9xeWpZbmZ2aXEvNnNuejlzRXVTdk45WVQvY3BlRGNEMW4rWDJqbW0xUE56SWNrdUswT1lxQXRJTmZuaWwzRTUNCkdLdUlwbGg1S3RJcmtWYkdOcTJSdGhCU0dza3lhK092d2pJbFc5Ky9YQXJzVmRqYXV3OFN1d0szUTRxMWlyc2JWdzNOTVBFcnNNbGQNCmxZVjJTVm9nNHExaEhOVnBXb1BqakpYTDB3UVZhd0ZUMXlhcmVKeUN1NG5DRmFJcGtsVzBZOU1qMVlscnBzT21UNkljUlh2VEFxMDANCkhldVNCcFdzUEVyL0FQL1Y4N2tITXBxUmVtR2x5QjRnNEpja2dzajFMNHRKZXY4QUlEbU5IbTNUNU1UMzdabEJvQzZneFZkK3pURlgNCkRwaHRCSzdHMFc0QjY0UVZ0bUhrbFVhRzZWMnBSa3B0NGpMWUJiWk1zY1FIVW42S1paYTIzU01iY2ErK1FJWkJiS1VKM0ZCZ3BLSFkNClI3NDB0cUJkQlhpRGdJU0d1VEVkQVIyM3djS1d3WHIySHZqd3NiWEtISnB5SDBZMGtGZUVjMStMSGhTdUNyVGM3NDhLcThjZlA3TlcNCitReEFRakxmVGJtVWNvd3Z5THFwL0dtU3BGbzJEUWI1cWtyOEkvYUZaUDhBazN5eHBiUmk2Rk1paDNrakM3bnFGSUEvNHlGTWFXMFoNCmJlWHhKRzdySTVDbW5GVjVFSDNNWmY4QURHbHRYdDlLdHZWNHpNWXhTZ3FXUUUrL3FxZ0gvQlluWmJYR3p0WW1INzZOWUFhRXMwSkkNCi93QmtzbGNGbGJSZ3NZeXlOYjM4V3crRm9yZzFwL3EwZkpDazFKVWEyV1JZdzhwalBWaTZKSjkzUTRrQkZxalcra1JJVWFSNTNCKzINCmtSalB5b1cvNDF5SzJsc3NNWllsQTNFZEEzVVlyYUdrZ0hoaXRyUFJYeHcwdHRpQUhvYTQwdHJsdHRzYVcxd3Rkc2FXMXd0aFhwalMNCjJ1K3JiN0RHbHRVRnMxT2d3cmE4VzVwMHhXMS9vSHhyaXRyeGFnZ1ZGY0lXMTYybE5nTmpocGJWUlptbjJLNDB0cmhhc0RYZ01SRzkNCmx0NVY1aVFwcmwrblpaNUFQK0NPYVBQdk9uTWdka3NLZ2I1ajBvS213NkhHazIxa0FFdEVWeVZxN2lNQ3JUMXlCVnV0ZHNJVnZpTW4NCmFiYUk3WkVsYmNGMndXdHVJR1NXMnNCQzI3QlMyMkFNS1hWSjJ3cGNRQVBmRldzaEpCTGhzYTRRRVc3SmtyYnNqU1FYWVV1eFkyczQNCjBBTzU5c1F0dDcrRk1TdHJkaDh6aU5sdDNHckVIRGEyc3cwdHVKcHZpQXRyU2E0VnRyQlNhYUlHRzBVMWtiS0dqV25YRlZ0YTRWZi8NCjF2UElVRTVsTlNMMDlLWGFlOWYxWUpja2hrbHluTFRHQjNIcG44Qm1OSG0zUzVNVFZXNFpsQm9YOGE0cTdoVHJpcmFyVVlzU3U0SEYNCkRZQkJxVHQzd3hWbFhrZ3gvd0NsS1czcEczNngyeStLc3JxZXhBSHVHT1RWeFdTbFE2NzVFc3dvdHpwdXd3S2hwR3AxSStqRlZFeW0NCmhwdWZEQVdRVS9XZitVREFsc1hBcnZzZkRGZ3ZXVlNkelRGSVZnMGRPdUxKZUpVQXAxeFZFUlRLTmh0aUVGRnhGeWFnMHlURkZReVMNCnFmdGpmdjMrL0ZVMXNyMjhoK0pMZ3F0R0RMVWtiOU5oVEZWSzcwbURVR0R6VFRxYWIrbEpJbjhjVlJGajVPMGJtQzl1WitKcWZXa2UNClN2ekRHbUVCV1JXdWphZkd3RUdsMlVTOVNWdGtyWHAzR0hoVnR2SitrVFRHV1N4dGpVMTQraXExKzRESW1EYU13cW14NUowMHRXM1MNClcyQjJJdDU1b3g5eGFtRGdwcU82TVRTUHEwUmoza0pYaTBrckdWdXZpU2NDb0thd0NzVjRuYnBpcUZrc2gvS2NWVS9xbzhCa2xjYmENCm5TZ3hWeVc3bjdJNTA2Z1lxcUpDWElWZDNQUlZ4VlVObktnNVNJNmhkaVdWZ0srSkpHS3F5NmJPd3FJbmRRT1FjQ3FzUEVIRlY2YVgNCmNPaVNJZzlKelJXWnVHNC8xc1ZSS2FIZW1EMWZTS0xVS1djRUFGdHRpTVZjK2xYQ0VncXBDL2FjTXBIMGZaUC9BQXVLclV0NHorMEsNCmpxQmhDcTR0VThja3E4V2dJclU0cXJMYkFnN0d1R1BOTHhmemhINmZtZlZFL2x1WEg2am1oeS8zaGN1SEpKbTZaUjFVTEtBamZGSzINCmd5REoxQmlxMDljVmFvTWlWZDkySVZ2SksxVENBcmVIaFZvYmc1RlhjYWRjVmRRWXE3NWJud3haQjNZSEZMcURGWGNhOU1oTG14TG0NCkZFOThsRkRxRENycURGSWFQWEZrMkFLWXNTdElxS1lvYUVaQkdLdE5Ra2pzTVZheFZvZ1U4TW1xeWkrTmNWZFFZcTZneFpCYlN1MksNCmxyZ2NQQ3hhYWh5UWlxeGlpajRqUVllRlgvL1g0QlErR1pyVXEyaW42d20zZkl5NUpESjJXdGszaUViTWVQTnVseVlzSTlzdmNZdXANCmhDRzFXaHlTcnNWZGlycVpHS3N1L0x5d3ZMdTV2aEJ3UHB4aG5Ec3EwRlFOdVZOOHZpckxCWlhCSkJlaEJvZmlCSDRaTlZzbGt3KzANCjVQeXlKWmhRbHNtVDlrNENrSU9XM0k3YjVGa2hKWW03YlpFcW9HUDRoWDZUZ1Z3QkJydmlxOEZ2REZWUUY2allINTRxcklIcjFwWHQNCmlxckdPbTV4VlhqRDErRUUreDJ4VkV4M0lpMmxYZ1BFbkVGVTV0dVFWV0JYY1Z5Nk1sVFMza2Mwb1FmdndNRTRzM2VnclQ2TUlSSk4NCnJXVHAxK2pKTVV6aGZZZkNUNzRxaUtwNFlDa0tFcGpOUlRJc2tCS0VvYWJZcWc1STFyMHhWRHZBaE5EUSsyUlZxQ0QwU1RDeGo5UWYNCkdLa2o4YTRxaUxlVjRXallwRzdSdUhISlFLMDdWUUsyS28yOTFoN2wrWXRvWVc2c0U5UmxZZUZKQzQvNFhDQXEyNDFpL3VKUkszcEkNCndIRWVuR3FDbnlvY0lTMWM2amYzS3A5WWs5VUlPS0JsUVVIaHNCaWhZdDFlZWtrUm5rOUtQZFkvVWZpRDdEQ0ZXa0ZqVWtNVDFKcVQNCitPU1ZTZlZOS3QyQW11b1lqL0szWDhNVlcvNGkwSWRMdmw3SWtqZnFYRkVsOFhtYlE1SENDYVVueE1FaXI5NUdMRk5MYThzWlFQVG4NClExN2NxSDdzVlJrZnBWRzQrOFlZODFlR2VkbEgrTHRXcHY4QTZRZitJcm1oeS8zaGM3RnlTSnV1VWRXU3dnMXhWb3FLWXNtZ01CVnANCnNpcnFaSUZXc0JLdTdIQXJ1d3hWMkt1eFYyS3V4Vm9xYTlNaXJWRGlGYW9Ua2xYRUhGV202WXE3Rm03Rld2cFAwWXEzaXhMUnhRMXgNClBoaXJXU0NyU0RYQ3JoaXEzOW80cTAvWEZWdUcxY1FhZUh2amFRbzNFeXdyVnZ0ZUhqa0pOZVRKd3BhOTVLNUo1RVY2TDJ5ZUNGbTMNCkc4ZTFFdkk3VWRqVHNEMHpmWTQwRjhWLy85RGhRdDJQYk15Mm5pVklJSFdlTTA2TU1pU3ZFeU5VL3dCR1plNUIvcmxFZWJrRTdNYWENCjNvVFh4UFRNaW5GUE5yMGZiRFN1YU1qdDB3cTBZNmRSaEFWYXlrZkxIaFduVUoyd0NLTFpkK1hGdXMrcDNhT25MOXh5b09sVlpjdGcNCnR2VFl0TFZSeENMdDc1WlNWemFXRHNBdGZESWtNZ1VOUHBjaDIvVmdJU0NsMXhwTFYzcWZua0dhWFM2UXhKb1BveUpDcVA2RW5KK0UNCkVrOXNGS3VqOHZ6dXJOdHhYcUNRRDkyTktxSHkrUkVybDFvVDAzSi9WalNxL3dDZ2JkZUMrb1BqTzlCMHhWV1hSTFQxaEdyUHc3bW4NCjZzUENxNWRIVnVZQkttbncxV21QQ3FySHBWVXFRZVEyeDRWUSt1NmR4ME84TkFPTVpZa2pldnRrVEVvdE45UHR3OW5idW9GREZHZGgNCjRvTW5BV3RwdEJieUtLRVpaVEZNcmFNZ0N0ZnV3Z0lJVE8yQ2pydGhSU1l3c0tENGlQcHhXbGYwMCtlSldsQ2RRRjJBQThhNUJQRWwNCnR3QVFTRDkyRmJRRW5MeE9LYlVXTW5JZVAwNUdsdG90T08yTkxiUmtsSFhHbHRjSnBxYllWdFU5YVR2c01LMnFKTzFQSEZiWHJOWHENCnB4QzJ2RXlkT21HMXRjOE5uSnUwS1NIeFlBbjlXR2x0eTJsZ0tIMEZyN0RFb0tJU08zWDdNU2hlOUJVWUxSUzlwUXFnUk5GSFQrZEsNCmo3dVF4dGFRYzk3cjY3dzMxZ0FPOGlGUDFNd3g0cTNVQjVGNW5rbWs4eFg4bHc4Y2x3OHBhUjRmc0VsVit6OTJhVE1LbmJtNCtTVW4NCnJtUFRKeDJGY1ZXSFk0cHQyQXB0b2lveUt0Y1c4Y2pTdFlnSzdKMHJzQkN1eFZ4TlRrZUpYWTJyc2JWMitOSzdEU3VxZStGWFlxdGINCmJGV3lOcTRzcmF4QVcyaGhwYmJ3SWF4UTAzS25YRFN0Q3ZmSGtyc05xdDMyTkRrcVJiVk42NHlGSmFJcmdDTFdsU1B1cmtRTE5NcTINCnREWGwySUk2S3dNaCt3UDQ0ZUdpNDg4bzVCSjVKSm5xekdwUGJLNVMzcHhKMzFYQ05nRjl4WE5wb2NKcS9OY1lGS3dqcVI3WnRXeloNCi85SGtwc3dlMlpUaGNUY2RtQTZtblE0bE1UWlRNeDhLZytHMlVEbTVuUktwTEFWUFhNa09IT1c2dzJkRDQ0c2VKVGUwTysySzhTbEoNCmIxeHVtUWtvdkFhZE1lSlBFdE1kRGxpV2Zma3phK3Y1bm5oNDhpMW5LUUswK3lWT1RoelNIdGE2QWxUeWlvYW4zNzVZbGQraGdOaEcNCkI5R1JLcVV1aWN1cW5GVUJQb0FKUHduSGhaY1NEazh0amY0ZnB3R0s4U2lmTFEyTFZIMGpCd3J4S0UyazJOdTFacG80eVRYNGpVL1INClN1UEN2RTBOTjA4a0lMaENEM29mNllSQmVKSFIrWDRYK3hJcDhLWkx3MTRrVEg1V2s3S1Q3NE9GZUpFTDVWbE81VEhoWGlWNC9LejkNCmtwandyeElIelg1WWtUeXRxOHZIKzZzNXBQOEFnRnJsZVFVbTNlUmRGT29lWHJhZFZxQUVTdE52aGpYK3VPSldUcjVWWWZzWllxckQNCjVZNFNTT2EvdkNDVjdEaUtiWXFpVjBOVjdmVC9BSmpGVzNzVWlwU015ZXljYS84QURFWkF5VkRUelNReUVIVHJoby81eThFZi9FcE0NCkJrcUJrdlpDcmM3R05QNUM5N0dvK2tJai93REVzanhMd29HUytYaVF5NmZHL2libVp5UG9WVng0bVFpZ3JpOVYxNC9YckdCaDFlT08NClp5ZitDYitHUEVuaFFvdmJPSWoxTlNFdityYmtmck9QRXZDNTljMGhlalNPM2o2Zkd1UEV2Q2g1UE1OZ09rRWpIc1RRWThTOEtnL20NCmVOZGx0cS9OdjdNZUplRlJielM5Zmh0MXA3a25IaVhoVVg4ejN4UHd4UmdkcWduK09QRXZDb256RnFaNk1pL0pSL0hIaVhoVW04d2ENCnNlazFQa3EvMHg0bDRWTnRaMVJ6OFZ5LzBHbVJzcndyVHFGKzNXNWtJOE9Sd2dsZUZZTGk0UFdWL3dEZ2poNGw0V2k3azFMRS9NbkgNCmlYaFZPSytBSjhUdWNCSzhMR05XRk5RdU4vMi80RE5abjV1VERrZ3lLaXVZeFVOVXFNQ1ZwVUU0cTdpTUJTSGNSa1V1NGpGVm5FWWgNClhjUmsxZHhHUkt1NGpBcnFERHdxN2lNSENyUkZNUENyWUF3S3R4VjJLdXhWb2l1S3RucFRGV2lLRDM3WVFyWDYrK1NWMlFLdXhDdEUNClZ5YXRFVXlKVnJBcmp5SW9Qb3Jna0NHcVVDR3hGOEhLdEc2RkQxK2VRaGtvN3NjY3FPNndVcjNwbDRIRnlidU1JTFVMeGJjRVZQcWsNCmJKNGQ5LzhBWTVJdzRRMFpNdlJKMWpacWNqeWtrK09SajI4TXFFckxpeWx1NzRXa29Od1BESEhEaWt2TkVJb0ZLYmV4em9jT1BoalQNCklDa1FpYjlSbHlYLzB1ZmZWQjRabE90YkZwUTc0bE1UdTY0QlVxVzNybEE1dXdpZG12cXdZVklyWE1od2NuMUYzMU92VVkyd1VtczkNCmp0amFvYVN5QTZERmtDaDN0UlVDbUdrMmhwTGI0Z0taSUZsYjBQOEFJUkFmUHl4Y1FTOW5jamYyUUhMQnMyUTNMNkpOcnVUNmZYdysNCldUNGtrTFh0MEg3QnJrVEpRRUpjUjhRU0VJeEVscEtyaDJCWWxUUWRxVnlYRXRKZlBlU1JvSFcya2JrYWNRQWZwb2UyTnBFVUxOcUUNCnl5bU0yWlBFY3FtaEIveVFmSEcwOEtpYjl5a2JOWXhyeU80a29yTDg4YlhoV0RXRVF1UFR0aFQrN0pJKzg0aVM4S3NubW0zakFxYmQNClQrMXhxZC9hZ3c4YThLLy9BQnZacHVaQWY5VlcvcGc0bDRYZjQvdGdQaEJiM0NZOFM4THYrVmlNQjhFQlB2c01lSmVGTGZNUG4yNnUNCnRBMU8yRUFDVDJrOFQxWWRIU25USXozRElCTC9BQ2Y1cDFIVGZMZHBiMndSbzVFU1RrMzgzRUFqOE1yeGxOSnkzblhYM1UvdkZRK3kNCm4rT1NzclNpM21mekEzL0g0UlhzQUIrckd5a0JTZld0YWNmRmV5bjZhWTJVMEZCN3ZVWlB0M1VwSFljMi9ya0R6V2dvR09RN2xpU08NCjlUWEF0TGZSWHdIejcvZmdTMDBDMCt5TVZVVEYyQXA4c1ZVMlRZbkFxbVJYRlZPUmFEcGlxMEtNS3FUS2E5Y1ZXT3UyMkJWbEtZUXINCnFERlhVR0cxZGlwWEwwd01iWG9oT1NBVzFROURob0xiR05ZLzQ2Tngvci93R2FmVkVpVGt3NUlMS2xEdjJUZ1N0d2xYYjloVStHVmsNCnBEdHZIZnVQREpBSmRnUE5WZ0ZUVEdQTlYzR21TS3RVMytqSWhXc05LN0Nyc0JLdXBndFZwclhCYklVM1FZZ3JzNGpiSmJMc3R3RmQNCm5keGdYWjJTMlkyNm40WWtyYnFEd3dXdHVZQ21IaEpXMXArMVRIZ0syMTN3OEpXM1V3VjNxdFAycVkwcXBGQkpOR3pJalVVMWtlbXkNCmc0RFB2WTVKckRScENzWnJRMWpJRmEwR1VaY1pJdUxWa3FrQmQ2b3NTazhBc25VbnIwUGIzeS9Ca01PWWFMUU9wYXhQZHY4QVpTSzINCmxjU0dGRkZBVlhnZUo2N3Jsc3NuR2ZKZ1VacitqeWFKcVU5Zzhna2tIQThsRkJSbFVnYi9BRHpHbEUzUVlrSVNLMzRKVS9iYmRoNFoNCnRkRnBlcFp4Q3RISG0wbkFqcXpLc0JUNTRNZDlXTC8vMDRpTGNWekt0MXE3MEtkQmdLUWhMNUNySnR0UTVRRHU3Q1BKRVFSa3dvYWQNClFNeUhCeS9VVjV0dHNXQ3hyYmJwamFMVVpiU3ZiQ0ZLRW10alNnQXdvM1FjOXR2MHdnc3JaeitSSHBXLzVsV0JsVWxYaXVFb0JYY3gNCkUvd3h5UzIyY2pUODMwOUxQcDYvWmhadTRHd0crVjNKeUpCQnozVnFmczJvQjhlV1RCS0JGQVQzYS9zMjhZOXFrNFFVOEtWWFYxTlUNCjBpakEvd0JVSDllU3RlRkpiKzZ1dlJjY3FiTjlrQWR2Ykd5a0JpRTgweEZUSTlmOVk0YlRRUzZTcDI1RTE3WTJ0SWQxb1RqYTBGRXENCmE5TWJXbXdhREcxcGNnTk1GclNzT2xNYldsdDh2TFRydGY1b0pCL3dwdzNzZ2haNWE0dG9GZ2ZHSWZyT1J4bEZKdEdwOE1rdEt5cVQNCmlrTDFqeFMwUnZrQ3JWSzRvdHNJQjF4U3B0SFU0RlcrbU1iVlM5TDJ5TnFveVJHdlRGVk5vajRZVlUzajJ4dFZLU00rR05xb3NyVTYNCllGVzBQaGtncTBnMXdxNEE0cXV4UVZ3RzJLSFpJS3FlT0ZXTTZ4L3gwWmgzNVYrZ2dacGRVUFU1RURzZ3NxU0dzVXRNUlQ1NEpTQzANCjBldjBaRG1rT3l3SnQyUmx6VlpRNEFkMWRUSmxYVXdCWFVPRlhZTFZvOU1FaXJoc044YVZqdXVlWXIyd3ZqRENFYU1JcEhJSHYxekkNCng0eElBc1NsVGVjdFVKSEV4Z2Y2bVQ4SUxiVGVjTlZwdklnK1NqK09TOElMYXcrYjlXN1NyL3dDNFJoQzJwbnpYckRMUTNGUFlJdVMNCjhFTGEzL0Uycm5iNjAzMEFmMHl2d3d4M1dIekRxeFArOVQvaC9RWVJqaXU2MXRlMVlyVDYxTFR0VGJENFVWM1dmcG5WRzNOeExVOWYNCmlJeVFqRmQxaDFQVVRVRzRsUGlPUnhxSzdyUmZYUllmdnBLOXdXYkdndTdKZktMeXNMa3V6T0tqYXRmMTVqNWhSWnhESWVaRzM2OHgNCnpNQmtpYmJlSjE1SHhZZUlQVE1QVjhRTmo2WEd5a2ptbDJveWxUOVhpZGtydExYcXgvcG00MFdXSEJiQ1V3UWx2MXlHQkxtR1czV2INCjFveEhGT2E4b0hEcTNxTFR2VGxqbXl4YTdRRFU0UUlmZ3BJRVk3N0kxTno5NHpFaElHVEVzeTh4MzhubS93QTVhM3F0akZTeWlEWEkNClh1SW94SENHMi95dml6Snh4Qm1Gb3BaYjJvWXNKS3FpNytGY3k4dXFqakd4WkFFSzMxS0prY0l4TGJjQjIyekR4ZHBjVXFLU1VPdFYNCkpEZFFkODNjWldFUC85U09oQlhwbVE2MWVJeGhDRXUxVmFJaHAzSSs4Wmk5WFpZK1NLc1ZyYVJIMm9UbVZIazRXWDZpaVBSd2xxTFINCmhya1dLbkpCdGhESUlhUzNHRktFbXR4MXhZeFpMK1VxK2wrWStpbm9Ha2tRL1RFK0pjdkJ6ZlNyTHRVL1I4c1hKUTBpamZKQlVCY0sNCk1JVkxiZ0VBNU5VbHZRZUQrNEl4VmlNNmtqRlVFNmpGVU82bkZWRnV1S3U0ZzRxdlFHbUtxaUExcmlxNlpDMXZNdll4dVB2VWorT0MNClhKVUo1UkJieTVwNS93Q0s2ZjhBREhJWSthcDhxbWh5eFZaRklHS3IxVWxxZU93eFZ4MGZYM1ltTzFQQW40U1NvMjhkejB5c3kzWngNCndTbHVwcFlhaGF5OEx4T0pkZVNVSVBhdmJCeE1aUTRUU3VzQlBiSklYZlZUNFpFcTc2a1R2VEFydnFQdGtWV05wKy9RWTJxakpZKzINClBFcWk5bnQweFZEU1d2dGlGVUpMYjJ5U3FFa05Na0ZVbWk3NFZhS0FVRk1WYUswNllvTGFnVTN4UXU0akpCVlFLdUpVTVcxb0FhbE4NClQvSi9WbW8xUE52aWdTSzVqaG0xaFZieE5BUERCd2hrN2ljSUFRV2lLWW9kaVdRY2RzcUhOV2lLNWFyZ0tZcTNpcTJoeUN0QVZ4cTENCmNSaDRyMlZoSG0wVTFKaC94VXVaZUFVRVQ1SlBiUkk3bW9xSzBBeWMrYUlja1dMS091NmdmamtlSlBDMkxPRTl2d3BnTXl2Q3ZXMGoNCnAwR0RqS2VGMzFTUHdBeDRtWEMzOVVpQjNGZkVlR0F6cGpLS1BrMGExNGxvNWZqTkNFWmRnQ0FldUR4R295cERTYVlVSUNoWDVWNDANCk81STdVT1BpTXhJVW9tM1VkVW9mRTdWeEU3VFlRMDhhQkdhbEtaZEZDZmVVRkhwM0ZUU3RCWDN5bk9kMlFPeklWYU16ckQzWUZpeDINCkFVZFNmbGxZd2NUUlBMVHJPK1NLOVNiaHl0STNvLzhBeFlyZmIrNGZaeksxR21qSWNJYXB6NGtMNWxzRGE2a3JJM0tLYU5aSUg3TWgNCnFWSXI0ak1DQThQME5LUnZmeVIyNzJvQUt6T3NyTjFQd0FnQWZmdmt1YW95YXpnaTBCbm5KTjVKSmJuVDNCK0dTMlpaVms1RHhEcWcNCjN5OFFqVE1Jdnl0NW9PaTJsN2JwQ0pQMGtzY0U3THN3aGpZdVVYL2pJM0V0L3E1WEtYRHVFM1NLZVZqR2VKSkJweEJxU0I0SE5mUEsNClpIZFRPMVNFZnU1SlNhRlFPSStlWk9ud1hJRkNoeEIzSjNPZElCd3hWLy9WSWdEWHBtUTYxVVZSWENxQjFpUC9BRWRTUDV2NFpqZFgNCk94U1ZkS0ZiQlFmZjhNeUFkbkd6aWlTaXdvcmh0b0J0c3JUcGpTRnJKVWRNS2JVSGl4VzBQSkRpa0JPUHkrWDB2UGVodjBwZG9QOEENCmdnVi9qaTVPbk83NldlTnQ4aVM1U0dralBmOEFoa2dUU29DNGpPK0VFcWxzNk44VzNUSjJxUzNnYmNFQ2hyaEJWaVV4MklwM1A2OEsNCm9CMUdLcURxTjhVRlJvTVZ0dmdEaXRybzFCeFcxVlZBeFNycWdjY2ZHbys4WkdTQ1VyOGtBdDVicy9rdytsWEl5SUZMYklvMXl4S0oNClNQYkNFN2RGM28xVSsyOVBjZE1pVFNFK2xTQ1dXRWlTTUlEQzc4MlNpL0N5dnNUeThEeE9VeTV1NTAyYUl4Z2VTSDFDS0dSTFgwM1cNClI0bzBTVGlkd3dUZnRnanpkWnFUeFR0Ump0ZHVtV0UwMVVyTGI3ZE1oeEtWNHQ5dW1Ob2Q2Rk42WkcxV21BSHFNQlZRa2dVZHNGS2gNCjVJZmJDQ3FCa2dHRUZVTE5ENERKV3FFbGc4Y2VKVkI0dHNQRXFrNkd1RzFXY0RXbUVGV2lwcmlpbHdYYkpoQzdDb1l2clcycFRmN0gNCjlXYWJWSDFOOFVBVFhLYVpyVHVhRHJpcldWVW0zWktOQmJjUlhDVDNJV25yamFiYllEcmtmTmJXNzlza0pKYkE4Y0pLdU9DMVc4VC8NCkFESEFyWUZNUVZhUFg2TU1ZQzFZVDV2SCs1TSs4UzVtNG1NK1NVMklvU2U0YkRQbW5IeVRKR1lqbDlCK1dWRnN0dmlHTmE0Z3J1MkYNCkFHRzBidTRqSzJlN3FVQlp2RFkrSndpSVBOU0xSRHVaclZXTlE2ZkR5NmJkY3EydHdjMFNoMG5hT0NXTUFNc2hVOHVyQXIvS2UxY0oNCmlHb0RaYXR3ei9ESTFRRFFkOFFLM1pEWXFFMFBxSzZJUU56VGx0V21Yd24zdG95SHFtbWhldGFXOGlVWDE1bUFpNU1Bb29COFRFOUINCmprNEpTRzZKNWdBbkVTUjE0UXlldnpOYmk0Njh5T3dIWlZ6TWh3UkhOeGRwYzE3cHgrQ25ManV3cGtORDZ6eEZZN0l2VjJrdVBMQ3kNClBRemFSTXRhMHFiZVkxSUgrcTNITVBWNC93QjVaWHpZaGRTUmNZdUpCa1JqMEZQaExGZ1Q5OU1oUUEyVkhYaVFqVG96YTFhR1hoTksNCnJydkN3THJ3VW43U014NWNoanczdVUydDh1VzlyYzYxQWwxVVc0cXhDN0Vzb0pDajZjaEk3SWtka3p0TG4xdFdndDJGVExNeUh4UEkNCjBYTVlRc29pbk92MmFXVjgrbnhwd2t0eHhtV3RUeU8rYm5Td2pGdFNxaXJRRnFlM2ZNL0pLeHNoLzlZbXpJZGEydlhDcUgxaFA5RDUNCmpvckN2MDVpOVhNd3JkS0orcWY3SnZ1eklISnAxSE5IVUdFT1BIazdKSWFZN1lxc0lIRVlxb3VoeFpwaDVSQmo4MjZNd29HK3UyNEINCklxTjVBUEVlT0xiaDV2b0tMV05WdWZOdDNvTWNVS3dXc1N6UGRnRW1rcUtSOEpQaStWa3VUYWZwcHpzS3ZOeXJ1T01hTFVkdXRjQXkNClV0cmJxd1QwaTRrWXNCc3RGQVA0WktPU3pTUWQyUFg4VW9SOXlCeFBZWmN6WXZkTHR0N2Zqa29xdys0UTFiNW45ZUZVRE1BRHRpcUcNCmJyaWdyQ0JYRkRXS3IxV2h4VUt3V29HTEpFUXJRajZmeG9Ca1NncFI1RjM4dFFBZFZtdUYrNlZoZ1F5SjNXS0NTWTlJa2FSdmtnTEgNCjhCa2VKbkVjUzNSOVVpdm9ESXFsUUFyZ0hxVWJkVDlPRVNiSjRPQVc5QzAvOHZybWUwaHVEZHhLc3NhUFRpN0g0bEIzM0dRbEpxWXYNCjV3dnRDOHFhM1o2TmZUU3pYdDdDYmlMMG9sV1BpR1lFRjNrRkcvZHRoanV3a1NFdWw4NitYYmVGbVRUOVl1cFZQeHgyOEVMQUwwcnoNCkJaVGtxYllHdzdTL1BObmY2aGIyVVBsdldZRnVIQ2ZYTHBFU0tNZnpQU1BwMnlNbFpVdHVRQlViOXo0NUFJTGZvSENoY2JmYklxc04NCnZ0MHhWRHkyM3RpcUdsZ0lHMktvT1MyNjdZaFVGSkFhbkpLZzVvaUR2aXFHZVBGVkNTUEpLb2xhYjRRcTF1dVNWckpCaVd5TmpoVU0NClcxdi9BSTZNdit4L1ZtbDFmMU44VUNxa25LMmJUS0trWUZhWUFaQldoZzRiVng2NFFlRldxREc3M1YzMFY5c0JVT29mNVNNWXNtK0oNClBUSlNWcmdjQ3U0RWUrS3RFR25TbVJrVmNxMU5jTVpLd2Z6bUthbWYrTVMvcnpPdzhrVDVKVFpWK1A1N1pPZk5jZkpNcmFPYVdzVWENCkZtWVVDcUNUV3ZZREtwTTA1VHkxUERHRGYzRU5rVzNFYnNPWSthanBrUWd6cGUzbDZJcVREZXBJdzYwSHcvVGhZK01nTDNUbXQ2ZW0NCjRtRzNYNGNpangwRDlZQVpSUEN3NG1wVlRXbys0NENMUWM2Y21UUnZRUXRaM2NUUDltUlpJM1UreFZndnc1amNHN1RMTGFSMzNDRzQNCjR3eXJJckN0VjNBK2VYamswR1c2d21Qc2ZpSkZSNFlWRTYzYm9TQnRVMUozeUVoYkl5NGxXQ1dHWXJFWlVqYzdFUFNoOXQ4RWNSdHINCk1EYVlwbzl5RTVSemlJL3M4WE8vM1paT0JDengwdCt0YTViUlZrcFBHcDNxUUdBOXlNeTRUOEp0NFUzMFhVN2JVSUxxM1lpSTNFRWsNCmJLNEpvM1ZTQ2ZCZ01wMUdUeEF4a0dQbTE0S2p4MGFWWDVJRDluaU94ekNqa283c0V5dUxpYVN4anQxSU5zbnFQRWg2eGNqeWtUbCsNCjBqdHVtU09UZFZQUjdTVkxXZlUwa1NOVWNRUXF4M2FSeHVSL3E1TUMwRnErbCtwTmJ2YmN2cmNEZXA2by9ZUFdwOTY1ZU1XMXJGQlMNCmF6ZG1kcnA1VExMSzNLVnlha2s5Y2haYkU5MDJhQzlTc2Nxckl2Mmc1M3JtZHA4L2VyLy8xeVZlaHpJZGEyQjgvcHdoVm1vcHkwOXoNCjRFVitXVVpITXdxR2tFZlYyQVBSOG5ENlF4em8vSnVHSEhicmloMkVLdElQSUhDcXhrRmNWUi9scjkzNWwwcVRzbDVidDkwcTVJY2kNCjI0dWIzK3hpOUw4eXRTb05wckNGajlCVkIveERLdjRITURNUW96R3gvU0dSaGFqUEg4RGZMTFExaUZGam1vUi91MkhmaWN1aTNNU3UNClUrSUE5TnN1Q3NOdTFwTklCMERIOWVGVXVuNjRxaFg2NHFzeFYyTEVyOFZDcXZiRmtpWWxxUWZBaitHUktDbFhrVW45Q3pLZXNkOWUNCkxUM0V4SndJWk1JVmtqWkNLcXdLc09sUVJRNUtUYkdmQ3UwWFJiYlRZdlRncndDSkdPUnJSSTY4Ui93NXlrczhtbzR4VDIzUVlRZEYNCjA4K0Z2RVB1UVpWSXRMNTQvd0NjckxWUDAvcGs0KzBscENBZkQvU0p2NjViaTVLK2pkQmtFbWoyRW5kN2FGaDlNYTVqejVxcTZzQysNCmwzaURxWVpLRC9ZbkdLc0dGbjFQRTlmRExWZDlVR0tyL3FxZzc0cXNhMVVrZ2JtZ08zZ2NWUTB0c0FhWXFoWmJVbllERlVCTkFLNHENCmdMaUFWK2YrMWlxQWxoNGc3WVFxRmtpUGhoVkN5UkVIY1lxb1BIa29sQkNpVUZEa3VKSENwbEJqeEx3dEZOcS9RSzdZOFNRR05hd3ANCk9veWtIWWhkL296VjZuY3QwVUF5Q3ZUTVdxWnRjQmlydUF4VjNwMXhWdmdSMHhWcmg0NHE3MDhWZHdHS3U0REZYY0JpcnVBeURKdjANCi9iRWhCYTlQSTBoZ25uaFNOVUgvQUJpV24zNXNkUHlDeTVKVnBjWHFjajROdWZESlpVdzVKcEZMY1FTRnJhWm9taklNYktTckFnMXENCkNPbVVzdUZiTExMUEs4czhyU3l1MVdrY2xtSlBpVGl6akN3cVFsNFpBNkhpdzhPdUxHV05NSTNTOVBvWDhvaFlDcVNuYjRmOVVZZUoNCnhja2JTclZKTFpURkRaTElHVDdjc3ZWei9rand3ODJvN0Nsa1gxdTg5U0ZwR1k4VFJhMEZRS2daalJ4aTJLVWtnZDY1a0dLQ0YzT2gNCjQxSUhjanJnSVJTTnMzQmtSU2U5Ulhya0dTWVd1Z1dsOXBGemVTU21PUzJrV0lNdS9IbXZKYWp2VW9jbUowRzJISkNOTnEyaXpOYTMNCksrdGJSRUFTb2VhOFNCeElmM0dTRWhKaVU0c2JqNjFENnNaRTBOUGpDSGk2bjNHUmxGQjNVM2hqdHBGTUJLcSt6QmgzUHZtTGxqczQNCitXR3k1b3d3NURkaFVqM0k2NWhnMDBjbG5BQk50MmJZajJ5Umx1eEpjOXcwZGttbjJwUTNFMG53S1FTM3hkMDdMbXd3UzJjdkVOa0gNCnFNbC9wTFBhR1llbzZnVGhLN21uUTF6STRtNGJKRTBsYTA2bkNDc2pia2Rrb3dZbzNhaHhZdjhBLzlBbEIycG1RNjExVDN3aEMrNUgNCkxUSnZiS2NnY3pDVURwQkFpa0hnUitPU2dka1owdzVaWlRoaHFwT0JEWU5NSVYzSVlWV25GVlN5bk1GN0JNT3NVaU9LZGFxd08yRUYNCnR4SGQ5R2NpdjVsQmxVOEpkTFhlbEs4WlpQSEsvd0NHbk1ES2pNQU9vK2tnZnJ5bU1hRklNeVZDUzl0Vlpsa25qWGo5cmt5ajlaeVYNCk1ZM2JIZFQxZlJJMVl5YWphSUFHcnl1SWgyLzFzdUJicllaZmVZdkxTa1YxYXo3SCsvUS9xT1dDUzJ3eSs4eGVXMW1sL3dCeWRxUVcNCk5DSlZJM3c4UzJrODNtUHk4WEtycVZ1U1A4c2I0OFMyaFpQTXZsOVNhNmhBUDltTWVKYlVHODJlV3dkOVFoLzRLdjZzZUpiV3Q1dTgNCnRBa2ZYNHpUdUswL1ZodERSODYrV0YzK3VnMDhGYittTnE0ZWYvS3k3bTVZL0pHd2NTYlhmOHJNOHFSQUV5U3NBZCtNWndFb0pTenkNCno1OTBUVHJLNmFkWmlsMWUzRXNDcW0vR1YrVzlTTzJWSEtBa1JadEg1dXRDb0tXOHBGYWZzaitPVDhRRmFSY1htKzNBM3RaU08veEwNCi9YQVFtZ3p6U3Z6bzBhMTB5MnQzMDI3YVNHTlVZcVl1SklGTnF0WEt6QlhuUDV1YWxiK2Y3aTJlMmpleFdHTlkyTXRIWThaZlVHeW0NCm5jOThuQVVGZWlhTCthU1cyaHBFdW1NejZiQmJvNytxT0wwS1c3RWZCOFB4YjVYUEdTVldYZjV5VHp4eVF4YVdpaVJHU3JUa2tGbEkNCnJzZ3hHTXFrcC9NTy9xUXRqYmR6eVl5RS9nUmsrRld4K1lXcWNRZnFsdjhBOGxQK2FzZUZYUDhBbUJySkcwRnVDZGdlTG1uL0FBMlANCkNta1JlK2NkUmp1Q2thMjVqQ1J1cFZTMjhrYXV3UHhFZkMxUmc0VnBDbnpmcXJtbkdFZjg4LzdjUEN0S1o4eDZtNDZvS2VDZjI0S1cNCm1qcXQvSlFzNDY5bEdOTFNLZVN4a3QxOUtiMUptWldBb1FlQ3hxcjlmQ1d2L0JZMHRMUmJST0J5cWZwcGlGcHY5SDJUTWVTTWZwd3INClNvbWlhVzdDc0Era24rdUdscFVPazZMS3pUUTJxK2xNVExEVUgrN2NsbEhYcUJ4d1VrQmNtaWFTVzN0SS91Si9qaHBLcU5EMGc5TE8NClA3djdjYVZYaTBqUlpZK1NhZkFPSTQ3b1B0SnNUdDJya0NWZVUvbVRhUXcrYmJoSUkxaWk5R0JsUkJRRGxHSy9xelg1alJaQU1YTWYNCmpsTWpiTnJnUERJcTdodWR1dUt1NGVBeFZvZ2pGV3dnUGJCYXRGRFQ1NDJxN2dLVXBqYXRGTnVtTnEzNll4dFd1SHRrVTI3Z2NJVWwNCjNEeEdHME1COCtMVFZSL3hoVS9qbWJoNUprTmtwMC9hM0oveWowNjdZNVpKeGhIeDdwUVpUeE55dmJXYzhySDBvakpRVmRsQlBFYkMNCnA4T3VTanV4SnBrRnBwTVZuSkZiS29tMUc1QmFCNUFSRW9BSjVWL2Iremt4QnFsSXRONWVqdEp3Mm9Ncnl6Z2tzYTcwK1E2WkNXTWgNCllEWmo5NW9seTkzSTZxcXF1eUFIdG1ITFVnYk9EUElPS2x0bll6UXlwelVrT1JVcWZlbjZxNCtPR1BHRWsxTzErcGFoUENwK0ZXSlENCkgrVW5iTS9DZUlCa0pOMlZsZVhza24xZE9iUkFNeWREUStHTTZHeVNVUmJ3ekpPRnVRMXZTdjd4bEpGZkE1QXhwQUxNZkoxdDlZMHoNClVyWnQxdUJVRVUrMUdRZHY4b3F6NWpUUEZ5Yllja1BlYWpZMk5yUEJjb0NIQnBRVlUwR3hDbnFOc3YwMkl4NXFRdysxMVJyTzZXYXoNCnJHUlRtRCsyUGNaZkppR2RpV3gxS3hXYUVJNzdFajlwVzZVSUdZdVU3Sm1PSVVvWGtOdkZlTERBeHBJcU5ER2Z0RU1nTGY4QURWWE0NCkxMaEkzY0hMaktDY0JlWUozVTByNFpXQmJTQjBhdGRIaXVMKzJsdUR1eTgwUVZHd094TlA1dTJabUtWQ25Qd3hwSi9OQ05IcUVtM3cNCnV4S052dlRNcUl0bklwS2dacUVia21nVWRkOG1EU2lCVEd3MGkvdkpUR2lsVUgyM2ZZTGtUbEFYaGYvUkk4eUhXdGcxeVVVRkVxdlANClRyb2R3cFAzS1QvREtjamw0VXIwc2NmVlg1WXc1TG5SM0k1ZDBjTU5WT1JRNnB3aFcrUndxNHNLWXFwbXAvenAxMjdZcEJwNXRxSG0NCm56TzEvS1gxVzdNa2JQR2ptZVRrcThtRkFhN0RmRjJFQllRMG5tTFduSk1sL2NPVDFMVFNIL2piR3d2Q2htMUs5WnF2TTdFOVN6RTENCi9IRWtLQXB2Y1NNdEN4SThDZkhJc2xLU1ZuTkdwOElDcjhoZ3RYQ1NuZWh4NGxjMG5hdGZuanhLMEhQeStXUEVxNE9hZGExMng0bFgNClJPeGpYZkpjU3Fna2VsSzF3OFNyVEtWSjZZcW9TeThsS25vZW93RW9SOWt4YTFqVUhZRW1uenpFbkp0aTlUc0dkcmh3ZnMrbEV3WHQNCnVXSDhNeUlzVTFWZHRzdFZXSElMVEFyYWNxNHF2bmVWSnJWVlloSGRoSUFhQmdJMllBai9BRi9peFZYVlI5cm9jVlhoZXByaXFxaUUNCkFkeGlxb1ZQR21MSjFpdktBMC8zNUtEOUVyVS9EYkZVU2taNWZSaXFyRXBxVnBrU3F1Z2FoMjNvYWZkaXEvVHpWNDY5Vlc3SHovMHANCkRYL2g4VlRSRkFKSGdjVlZrVDR2bmlxSnQwS3pSN1Zvd05QcHlTcmJCU05PdEIxL2N4ZjhtMUg4TVZSS0tlWFQyeFZWQ2tEcDB4UEoNClZXelFDRng0U09QK0diK21WRkllUy9tY2gveGZQUWJlaGIwLzVGNXI4L05teEprM3BUZktFaHIwejRZcGR3OXNWZHdIYkZYR0xhdE0NClZjRU5PbVJLdTRHbEtZRmR3UGhpcnVCOE1WZDZaeFYzQVlxN2dmREZYY0Q0WXE4Kzgvci9BTGxnUCtLRi9YbWZoK2tNcGNrajB6ZFgNCjhPUUgzWkRLeXhwbWlVM0dVdGl0RkxPaXVxU1BHc3E4SlZRa2NscldocGczV3JUWFFycWM2cmF2Skk4b2lXUUloTmVJVlNlSURFS1ANCnRaYkcydVVVMjFEVmZqTHljMFVOU0lHTUVBSC9BQ3VXK1RsS3d3a2VFSlplWEVNQlBKaFY5Nm1nNjVxY21JeU96clp4c3BWTnE4Y1UNClNzS0hjaGFlUFRESFRscjhNcFZxY2x2Zk1KZWFpWUFLM3lIam14MDQ0UlRtd0FFUWpQTGRzUTA4cUgwaWFDTW5mcDRaajZuTlJEWEwNCklBdXZOVzF1M2taTGlSSFVHbGZURkRsMk9mRUdJUEZ5ZEY1djFXMmRqQnhpYnM0UUE5S1ZIMzRSQ2xsRWhLOVJ2TG5VZjMwNUR5SVINCjhSRzlBS1V5d1NwalpTeWlnZkVLWksyNUdhVnE5enAxeTB0dlFsMUtrTnVOK2hwNGc1R1dPd3hKcEUyT29TcE9Mc0VtZFRWVzZuclgNCmJLY3NiMmF6dXlXNlAxcThXZEZwRElxTklUNGo3WDY4eFl3clpFY1Nhd1JLOFYxY21pcXNoOUp6c3FpRGI4VHl5NFJjcUlwaGx5OHUNCnNYaWhBZUpjaEFCWFpqOFIrWGhtUURRYVpjMDJSTERSSjJ0MnRvNTVrSUt5ZHhYeHJsSGlFcG5PaWliQ0RWWmxNMGtRa3QyUEpBV0MNCkoxM0pJM3g1czQ3di85SWhCTmN5SFdyc01VRkg2Y3ZxVzkwditSK0JCR1ZaSEx3cExwNVlPKy9VQ3VNT1NjdzJSM0llR1hkSEQ2T3kNCkxGYVR2aXJWVGlyc2JaVTRrZ0VqWWdIOVdOb0llU2F1cFRWcnhQNUo1Qi93NXh0Mk9Qa2c4RkpiQnhwV3lOc05zcVdFbjFIK1dPeFcNCm0xQVpndmM0ZUVMVG1YaTIvWEkwRVU3QWFDYWJDbWhvQ2ZZWUxDMHF3UTNESU9FVEg2RGtoa2lpbGRkUHYyRlZ0NWE5aHdPUGl4VFQNCmhvMnJzZjhBZVdVMTdjYVlQRkMwMlBMV3VTSDRiTi85bFFmcng4U0xLTVFtMm4rV05aV0pROElVZzcvRXVZczVCblFlaDJRV09YbXoNCmdmdVlvei9yTHlxUCtHeTN4UXZDRXdXNWhVQUdwUFVrRHQ3WklabU1najlHaGkxZTdtdElKVVNhQmVVbktwcFhjQTA3MHcrTUdGSjANClBLc3k3ZldFci9xdGtEbjNaQU5UZVdwUzBUZXVLUnNXK3dlaEJYK09BYWdMVGhvTHF0RE1EdjhBeTB5WDVtSzB0R2owMmFiOE1ScUENCmdoVVRUajlsWkM1WGMwWHRneWFxSVppTGQxWnp4MmNrMEVVbDFNZ3FsdkV0UzVyUUxVNFB6a1djSVdpWU5EZTMwNjR1YVNHT080S3gNCmZCUlpROHI4bURsdVNoUlEvWXdqVXhreE9NazdJU0M0dDJKcDZsVkZDREd3Sm95aXU0SGprdnpFTzl5UHkreTRYVU1ZNU9IRmZGVC8NCkFDbkplTkJBMFVqdmE1ZFV0ZmlIRnlWQkpLcVQvd0FhNVZMVXdDblJTaUx0YmNhZ0xIU3BkUXQ3YWE2dUVsbWpGcW9DL3U1M2pjUHkNCnJVMFpQczhQMnNoK2RnMFN4bEdyNWdpTlNiV1JHcnVyRlFRZkRiYjdzQjFzV0hBVlQvRU5wUUQwTGtTSDlwSll3bytneE4veExCK2QNCmluZ0ttUE0wY01vZGxuS0Jmc21SSzhxYmRFR09QVld4NFNPYklkTmNTYVpaeUFVRFFvZVBYajhJb0s1bXhsYWlWb3VNRXR0a2tvamcNCmUrdzhjVElLM2FjVmlrNWxSKzhmMjcvMjVUS1lTSG1YNWg2WmZYbm1oNWJTRXp4ZlY0QjZpL1pxcVVwVWtaaVphTE5qZzh0Nnd3b2INCllwN3NWSDZ5Y3hxVmVQS3VxVTNFYSs3T1A0WThLMnVIbE8vSStLU0pUN0V0anNtMjE4cXpWbzl6R3BIV2lrL3J4cStTMnUvd3lvMk4NCndUOGtIOGNlRXJhTTAzeWRiWFlscmRNbnBrTHNnUHhIdFN1UjZyYU5INWQyOWY4QWU2VDZZMS9yazZGY2syMmZ5NWdQUy9ZZjg4bFANCi9Hd3diTGJaL0xlSC9xNE4vd0FpUi9DVEhodGJXbjh0NDYvOGRCditSUDhBMTh3Y0MyMS95clpUdXQvVC9uai9BTmZNT3kyNy9sV3ANCi93Q3JpUHBoUDhIT095MjBmeTJjZE5SVS93RFBJLzhBTldPeTJ4dnpGK1JVbXNYZ3VCcmFRZnUvVDRtQm1KSzc3ZkZsMFowRjRrQmINCi93RE9PMDBDa0RYb2pYY24wSDYvOEZnbEsweGxSVmgrUkY0dXcxbUJ2Y3d5RDlSeXUyM3hRb1hmNUtYZHRINmphdGJ0K3lpckZPV1oNCno5bFZVQnVWY0lYeEhRL2twckRRcVd2N1pKVzNLT0pEeFBTbFZVakpDVFdac1o4eWVRcmJUVlkzT3ZXMTNPblMyaDlVbGZuOEFRZmYNCmtSa2E4a3JZTE9HYVVveitvRU96ZTJTMjZPS09hNFJoclpXMm9oYjRjUEVXVFYvYW1HeHQxTnM4VXNsWFdWZ1ZFaWVDRTdQOUdHSFANCmRQUkZhUmZPbG9VcDlrN0R1SytPWWVveDNKb2xBSWllN3RMbDQ3ZTdabzdjc29rbGpYazZMVWNtVlNWREduYko0QVF5Z0s1SnRxSDUNCllhdFlhSE5xcGxpbGlSUk5HcXVHWjRHQUlmWWtCaXA1Y2YyY3pHMG0rYkQ0N2E2bkxQQ29NYWl2MmdLVi93QW45ckJRWTBzbDA4QzQNCmVCMklLOUNPK1NNd0VMWHNoQ3hGU09PMWV1K1JHUzBIZFhnamtCVm9neFpUVU1BZGprSk1Ec20xcnFOL2J3dEhJcEtBbVVPd3A0Y2gNClUrTkJsWENMWnhraDlSOHp2YzZiSHAwU0dLTkFmWGtyWG0xUzIzdFVuTWpoRmNtd3kyUlBscTZ0b1licmxLdHVzNFNzaCtLUUttNUMNCkwvbE1CbE1pd0c2ZkRSNHJncktrUlY3bjRrREhreXhnRXNXUDh6WlVZamhaVGdEdXJhYlBIQnBNcFpxZW55RWZFYjBicGtjTjN1akgNCk1SNXYvOU1nNUh2bVE2eXc2cEFxTWxGQklUWFFDWGVaRC9MWDhjb3lGeXNVZ2tOc1FyYjdiVStuSlFHelpsRmpaRjEzcU4yYnRsdlINCncrVHVSNzVGZzZvQnFkdHRxN1lwYTNPNEZjVnAxRDNCSHpGTUZobFNtOHNLZzhwRVVqcFZnUDFuR3d2QVR5ZVZlWVhpR3QzcERxVU0NCnBLc0NDQ0RqWWMrR3dTNzFvdjV4anhCS0p0Yk84dVFyMjBFc3l0WGlZMFpxMDJOS0RCeGp2WkNKVEN3MHUrRnpFbHhwTnpNcnVGSU0NCmNxZ0FuYzFBOE1ybk1kRTBXY3grU2RHUHhEVGlENE0waC9BbktQRWtHVVFyTDVUMHRDcFhUVURJYXEzRS93RE5XUGl5WmJLdzh2V1gNCjJqcDBkUjNLTFhJZUxKUEN0YlNvSXlPTm9xMC9saUJQL0NqQ01oUE5lRlpKQkt0ZU50Ti9zWVcvaFREeEZlRlRBdWgveDUzYi9LTmgNCi9IR2lVYk5GTlJwL3h6cnYvZ09QOGNhSzdOS3VwVi80NWszemFnL2ljTkxUWUdxZy93QzhEQWU1UDhNYVZFUnRxd0cxcngrWVk0OEkNClNxQnRaK0w5eDlQcHNjYUNBWFYxbW02TUJ0VUNPbjY4aVFHWENVLy9BQzl0RVhYcnE0MUF6UXlTcEZ5WXQ2SUpTb3JYdnRUQlNPRjYNCmo2R2o5OVFOYVZQNzVCK0ZjZ1lzU0Z2MVBSditXLzIvM29YK3VSTUxDS0tTK2FudE5QMFdlN3NwM3ViaU5rcERIS3JzUVdBTkFPWFENClpPR01kVnBJYnk0MVlXc2t0ckpJODNBbUVNVkFxMytzS1Z5N2doM3BBUVhsYythWmRmVFQ5WmlsbWdDd3pHNFJTVjlONWdrZ1pvd0YNCitCVHUzMmN4YytNSGsyaW5xVnpwM2w4SjZLK21EUUJ6SEl5N2p2OEFhT1hlQkR2YW9Ta0VGcVdsZVhuMHE0OUpmU3ZVU2x1MFVwUlcNCjNIWHQ5T1JuZ2pYTlJPVjhtR3RwT3N5V2Nobmw0VHJkL3VxM0xzcldoU20vQ00vSHo1ZC9zNXJEcFpjWE55dkhOSWE0OHU2aVRPc1cNCnFlbkdWaUZxNUU4eERCZ1pmVXFvRzZmQW1ac2RMR3Q1TVBFa2lVMGkyUUtaTCs5WWdiaEVJVWZTNXlKMHNlOVJrbDFYUHBtbmtmRkwNCnFEMG9mdHFnMytZYkI0QVJLYTU5UDA1alZJN3ZqVHFiaHFIN2x5UTA0WWdsdExHMFQ3TnZJZjhBWGtsYnI5Mkg4dUZzcWdFS212MVMNCk90RHNVYnVLZDJJeVl4VnlZN25takl0YTFPT0ZJMFlSb2loVlJGQUNnZGh0bHNja2dnd0hScjlLNmt4K0tlVDlYNnNsNDBtTkZhYjINCjVacVBPNVBoeU9IakpXaW9QcWxxV28xeUNlbE9YSS9jdGNxSmtrQXFiYWpBZGs1T2ZaSC9BSWdaR2l5V0c5WTdyRklmbUFQQWZ6ZSsNClNFVlVuMUdRL1p0LytDa2pHSGdWUWUrdkQ5bU9FRHhhVmlmK0ZSY3M4T0xDMUkzR3BNZHBMZFI3Qm5QNGxNZkRBVzFqL3BKaUtYcUkNCk80V0ZSL3hKbng0UXRvdlRZZFNnZ2taTlNsQm1tVXRSWUFLOEhQVGdleVpId2hhMmpWdU5WcC94MFpLLzZrSC9BRFJrL0RqU2dxaTMNCldxMHAra0pQK1JjSDhJOEhoaEhFdlc2MVViZlgyK21LRS84QU12SGdBU1RUYTNtckFuL1REOC9SaC81cEdQQ0VjUzc5STZ2L0FNdFMNCi93REltUDhBcGg4RUpOdE5xV3I5cnBDZkF3ci9BQXBqNElSeHVHcDZ2VGVlSW12ZUQvbTdId1F2RTJkWDFVZjdzaFB2NkovNnFESHcNCmd2RXMvUytxOTJncC93QVluLzZxNCtFRjRtanJHcDdBZlZ5VDI5T1VmOHpUajRJUlk3MXAxalVnYXV0dUtWMzR5QWp4NnZnT09tUTUNCmJQS3ZQWDV0M2NwazA2MDR4S3JGWEVSWUJ5TnZqWWt0eC95Rk9ZMG9tMnMyWGw5OXFWOWVTajFYWnlUeEVhZ2dWOEFPNXlVY2JHeW4NCk12a256SGE2SWRWdXJiMHJmYm1oM2tWVHVHZGFmQ1Bua3pqSVNFdWlXcU5IVThQdGI3N2NjclZOL05QbUNQekphdzNBUnJTMjBXMGkNCnRJTFozOVQxSExIazFSUXF1WFJDZWpFSTd1U09hdjdJNnI0NFo0OXQyc2hIcE5CY044R3gvYVUrSGZLUkNrQlZtMVBVZFFFRnZlWGMNCnNzY0NDT0tKMllxaUwwVUFiYlpZQVMyTGZxTGxRc2N5b3hPMWExL0RKY0pWVmt0YlZvMVBxZWxkais5US9aTk42cTNmL1pmSGtEQmoNClNaNmJiV3MwWW1aRkxDaGNOL01kaFFZSVFJS0NhVEpadG5pVUFjUVdDamJwMSs3R1VTMXlLbUVNb2xRT2hFOExvNnlHZ1VFZmFIdmgNCkVVQzJITkJHUThoSVZWK3o0a25wVEdpMjcwblZpZkw2M1pOeUtSeG9qY2FNU3poU0dVVU95azc1VWJURllkYmt0WkpCYXlOSGJ0OEsNCm94SjRvVFdncjc0QkVvbk1nN2NrTmM2cEpPd0VCUEFMUUQzUFU1SVk2WXpBTC8vVTVXMzVtNlIremJ6ayt3QS9qbDNFNHY1ZFJmOEENCk5LekErQ3drYjNMVS9WanhJT25Uenl2K1pWbTNxVFRKRmJ0WGdJNVhZa3JzZVEyeXVlN2JIRlRROHdRUEpmR3o0elNRUWlXRlFTUksNCnpmczlNaUpFYk52RHNsMFBuanpQR2hFM2w4VE9Uc1Nzb0ZQbFhKQ1p0cThHeWhianpwNTVsWm1oMHFPQmVnVVd4YW4wdFhMT0pQZ0INCkNQNXEvTVZ4eDlONGgveFhiaGYxS2NybklzaGdDRmwxcno3S3Z4eTNncC9LcFgvalhJOFJUNEFROHNIbWFja1RQZDFwWDRtY0Q5VysNClRYd2dsNzZWckpxREJNNTdCZysvMG5Bbnd3RkdUUzlSNWIya284ZjNiSDhjVjRXanBsOG54ZlU1R0I3RkcvcGlVaDZqNVF1SXJUeTcNCmF3WERwYVMwY3RDeDRFQXRVSGZ4eW1UYUU0R29hZlQvQUhyako4ZVl5dWlsY05Rc1ArV3FQL2tZUDZqSWtGTlc0YWpZZFBya0krY3ENCi93QmNHNjhLNGFocHZYNjVFUDhBbm9QNjVOanV2Uzh0NUdwSE1zbitxMVQrRmNCTkpBS09pczcyWUF4d3lNUEhpMVAxWU9KZUVvMkQNCnkxcmNvTHBhT1ZvVzVOUlJ0L3JVeUptV1FnaWw4cWEyU0E4YUpYZjRuVVk4WlNNYXFubEcvcjhjMEtkL2lZay9jb3g0aXo4TkZSK1QNCnJnN1BkUmdqd0ROK3ZiSGlLK0dyeCtUbzZnRzdaMk5CUlk5cW42Y2VJcjRhSkhrNnlqajlXVnAzVWR4eFEvcXc3c09DazJoL0xxUWkNCk1qVExwdVorRXU5QjByVThhYlpLSUtlSk9CK1ZSU0NOeGFSUE03Z1NSTkk1QVUvdFY1ZmhrK0ZxbEpOOVAvTEhTSVpmV3ZZSXA0by8NCnNSUjhnRy8xK1crUEN4NGtWWWZsL29FTHVibUJIY3U3TEV0QXF4c2ZoV2dGZHZuaEVWNGsyc2ZLbmwrMG1EMjlqRUdUY09WNWRkcWYNCkZYSmNLOFNsYmVTOUN0NzlydExVTTBsZjNibm5HaE85VlU0UkFMeElxMTh2NkhCTFBKYjJrTVpuREpNUU4yVnZ0S2Evc3NSOW5Ed0INCkhFdEhsL1FnWXVPbndjWUJTSUNNQUtlaDZEK09SNFU4U1R6L0FKY2FaSmRlckRjU3dRczFUYmlqQWJkRlkvWndHQ1JKcTQvTDIyZFkNCm10Sm10dUlJazUvdlMzZzIvSEkrR25pWFIvbDdZdGJjV3U1V25yWDFsQUFJL2w0ZE1mRFhpYmovQUM4dGhFd2t1M01wSzhIUlFvVlINCjFCWGNOWCtiSHcwY2Jycjh2ckptcGIzY3NKSVBJdCs4cnR0dDhPRHcwY1NJSGtmUkdoaVVnK29uR3N5T3lzOU9vTmVRQy81T0hnUVoNCmxLcFB5K0FFb2p2VkxGNjI0WVYrQS9hRCsvaGp3c2haU0hWdkxlcmFhV00wSmtnRlA5SWpCYVBmNks1VnV5RXJTcVJIaktxOFpETWENCktwVWl2eTJ3RzI2TUZwRkF6TXRBbjJ6UTBYNStCeU83THcyZzRYNGdLQWRXb0QvRERaWHcyaktLRUJ1cHJRSCtHUEVVakh1aUxiU3INCjY2YmlrTEZBeWhwR0ZGWGwzcVRqWmJQQ0NTYXhiWFp1TFNDd1JwaU5RaGlsZUJTNEtjanlHeTk2REpBbEJ4Z0kyTzB1bXVXdGpwMHENCnlxZUsxaVB4RVVyUVVydFh2aHNvNEFpSk5JdllvQlBMWnRIR1c0L0VnRFYvMWFWeU1aRkhoQkV0NVgxQlJBMDBNVWNjNUFEa294VU0NCks4aUJ1TnQ4c0pOSkdJRlNsOHVYd0RTdzJ5endBc0ZsUlI4UVN0V3A5RzJSc3A4QUxoNWQxcmpSYlJVb1N4UXRHQ0tEclN2ZFcrRVkNCjJWOEVJYzJHcEtxbHJRMVp1Q29Jd1dKUFEwSDdKL214NGlwd2dPdU5MMVMzSTlld1pPVmVKOU9vTk4rcW5HeTFlQmFFbzdLckpDRHoNCkpWQndOU1IxRk90Y2JMSVlRV3VGMThLL1Y5NUJ5VUNJbW82VnhzcDhBTDJ0cm9NaGF6NHE5S01VSVg0dWxXSjQ3NGVNdFF3bnFwT3ANCkZ3YmYwVU00SEwwMStJMFArcVRqeGxtZE1GR1IwREVORXFzUDJTR0IyQlBRL0xCeGxqK1hiZFNrU3l0QlNKbUtMSnVGSkJvYVllTXMNCmhwa1ArN0NsekhSUTNFc1NhVjZZaVpTTktxeFF5VEdpUVZZVVBENHVaNVYreXRQaTJGZHNseGxUcFFHSGVldk9ENkRwalBGRW91cmgNCkNsa2prN3NTVlptRlBoNERkYTREa2NmS0JIWjRwWmFQcTJyM0hDemhlNW5rNU14QXJ2V3BKcDlueDN5SjNjYUVaSGs5RDhvZVQ3N1INCnBFay9RLzF6Vm1VZ0dTV01MR0IxcEh4YWhIODdmRmdFaTJ4d25xbUhtbTExQ0pBbm1xSm80Q29ramd0cnVGVTQrQWpJQmR2OVpzc0UNCmkyU2dBSG56M1BseHRRRFd5WEMyVEFpZUtxTXdVZE9KNC9EOHNyTWEzY2VTdm9YbHpUOWNXL2h0TGxvNzVSemd0WmVQQm93YUFGaFQNCjR2OEFZNU9KcmRRZ3g1QjFZMnM5MExKcEZ0NURCTkJHU3NxU0FWcUViN1d4SDdXUzQ3Ylk0clNiVmJKTFNlQ1cybTlhS1NNTUc0OEgNCkRMOExwSW5aME9HbU04ZExZNDNlQlp1bk5xQnZISGsxQlV1MU1FQzhTeHVDYU9BRFJWK2VHMG9SbWxMc2ExTGRqOFdCVTBzTlh2N1INCklrNHBMREhVaHZ0SGZmZHYxREZoTVdtTVZ6TTVFaDZrMS80UHJpdzRWOXZhVFNTcUFTWTFrRVpZN2lyVTIrUnJpb1NTOTAzVkZJa2wNCmhQQ09xclFnaFIyRzJKYlJ5UVZ1WmVaVmdSL01mSEljTEZOVnQvVVJZdUtnQWhneEc1SkEycmtKV0dKVTF0ZlRla2JBTUNRUWUzMG4NCkJ4RkQvOVhpcTZUYUtSeGdRZlArbVNiTjFkYkNGZnN3b0Q0OFJoQVNMWCtnaUlTRlZhRHNBTWxRVFJWOUJ1WkliNXBSeXBzQWVuWHQNCmtTdkM5UjBqeXo1ZzFjcTJuV0Uxd3BJSE5WSVFjdWhMTUFLZjhGZ1lrcnRVOHQ2N3BOL0pZWDlwSWx6R3ZxRkVCY0ZPN3F3QkRLdmYNCkZpekh5ZjhBbEZlNjNZdzZqZVhRdGJTNVFtM1dNZXBKc1NBWHJ4Q3FjYnBicEh4ZmtSZW1OV2sxaUZLa2lSZlRmYmZhaDVlR1BFdkUNCm5tdS9rbG9rbWt4UTZWTDlWMUdBcVd1cG5aaEtvKzN5VWZaMzZjY0ZvdGRwL3dDU2VoSHk2TGE5a1o5WGJreDFDRjJvakU3QUtUdW4NCnpHTm90Z21wL2svcituelNldFBHYllOU082QVlxNDl3SzhQcHh0dGdBVU5GK1dUa2oxcjhDdlhoRzFQb3FjZ1psbjRRdFhmOHRkSU0NCml2TmN5U3lLb1NvVkJVRHAxRFpHeXlHTlZqL0w3eTByQWVuSzVQaXdGZjhBZ1Z3OFpUNGFxbmtueXhHQVRaS2VWU1BVZHUzM0RJeW0NCmU1bEdBQ1BrOGs2ZGFSUlN2bzhhUlNpcVNlbnpCSHVTVHh3Y1o3azBGbzB1MmlEeUxwOGFSeEdqdDZLRGlUMDVIajhPUlpVRTEwdnkNCi9lM2pvc1VYb0pJalBITTBaVkdDaXUxQmp3MmtHSVRXSHlmTVpGam1lYVFPb1lTUXhqaHY0bGoyd2VHbmppaXJieUhISDZqVDNIeGwNCnFReXhrQVVQVU1DRytMNmNtTVlhamtON0lodko5ck5hZWxIeWd1ZUlLc3o4bUkvYTVBVVhDTVlaQ1hlaUxUeXBZcFZsaWxBa1VCUTENCkdvVkZDYTcwQnc4SVJMTFJWb2RBMHExUm9KTGNFU3R0STFDeTdiMEo2REhoQzhmY3FXdWl4V3NBUzBxcERCbWtDQjJJcWZoYXBQOEENCnd1UENwbXJTdnB6bzhkeEUwemtrS2pLSkc4T1FYdUJrN1l5SlROSXVQQUpLUXFJT1NjUnZ0NDlzRFFTVkc1MUdTM1JwbVVTUnFHNG8NCmhMTnNLNzRzVEZXUzVtY2Z1bDlOQnQ4UzhqV3ROL2lGTVZFRzJ1bzFEc1NCSWdQS1hqMlhyUVlwOE51SFViZVdNU1VLQmh5SVlVSTgNCktqRGFEaks5cnlBQWNIQlpxQmRqU3A4Y2VhQkFyWkwyMkI0bml6aXRGSGVncWFZMG53eXBXMnBKTk04TkFIai9BTHhVTmFIL0FDdGgNCnY4c2JST0ZLdHplT2lSbUpRUTMyaXhvQVBuanhKaER2VVk5VFdWT1lqSWozNGxnUVR4NjdIL2hjZUpud0JUbjFFa1FoZ1Fqay9aMkkNCklHeW43OGVKZkRSVU4yamZ2Q1dVOTBZVXA3NzB4NG1zeEtqZjZsNkVMWE1TcklnMmJtL0FVOXRqamFSQlJFa0QwZ1dRcVhZczFDS0UNCkRlbGQ4U2JaZUdwU0NONzZralB5NEVvRk5GcFdnMkcvemJBMkF5SFJXRjNLWEVSUUttelZwVlNGRlNCVS93RERZVk9NQkxibUdhNkYNCkRKRk9xa3VYY2ZaNUNvQzhmaTVVd044VFNIdXJmVDQ0WklwSXcwRHFna2prY2dTQmpUa2RqKzE5bjlyRmtKRmRMWjJVZGxIWnl3SkgNCmFsd3FJM3doay95YVUvRTRPRkhIdWhMZlI5TlFSV0tXMGFSY2l6TEt3ZVNuR29CL3R3Y0liTjZ0SGVsYXh6cUZpNVBHaFhpNDUvQ04NCnh1ZnM3NDhJWVg1ckVTS3pMVzhhR0V5RDFPTEJqQ3dkaDhJMysxOHNOTFhFbHd2SnJQNnJZUld4THlDb3RwTjZBdHVBeHEzSnUzN3oNCkdtd1lvYzdUUVJGcDVPUEZMYVFVSElFRU92OEFlYk1TZmxURVJEVUNoSlpZNTFuWjRGdUtJVEdGNHl5RlYrei9BS3ZMc21FdGdBRFUNCm1uU3p6VzhzdExhTXdtTVc2Z0p5TGpmNFIrMXdQZjhBYVg5bkJUQVpGRlVTNHRJM3UyNXhrSjZVVFJsRHpwUmh3UjQrZE8zTEF6bEkNCkJYaHRaTFYwaktmV0lyaWlGMEhwclUvdE1UV2tZOE1WSnNJTTJaK3VJNlN2UEZ6TENDTnVBVVJmYTQxSUpGY1c2TWh3cXR3K3JremMNCkZDTXg5TmJma0VIZHVTRWZGei95dVgyY0lZUmpFN3JKTDk1YkpFaDJqa1lSTXlPZmlhb0RWNGxUUWZ6OHNhWitDb3hXc3F5K25JeXANCkhPcHJCTTFheUo5bGlOeC93UEhJVW1SVmJtQkhqazVXL3JTVEgwK1N5SG1FaUpwOFkvWmNyWGoremg0UTFBbTlrSXVrYWZITTBscloNCnBINmhWSkpXQ3U2K3B1V2JxRFRqeHc4SWJCYlkwOWJpMXRZUTRNTkdFRTY4WS8zZ0JJYmlPaGs0OGNIQ0U4UkhSUjlDMWhySktTa0kNCjVpNW5scDZsS0FoMmNkVmZsOE9BZ00rTmhubW56M0RGbzA4K25wSDlWZVdTMGd2S3NJWG9BYXhWOU5qd0FLczM3UDJjanhPUG15Q24NCmgzNWczdHJxMnRXY3R6cXNjdHQ2TVl1SHFXWWtMUXFzY2E4VlVBQmE1RWk5M1VUbnhteXErWC96VXVQTHcxTVdQcFNHOVdHRkFZajYNCmNVVUlLcW9CMzc4alhDQlRmaTFCaDBDTHQ5Wi9NenpaTGMzR2h6TkpNZml1UllnUnZ1S0xYaUtnREFKdHZpNUpkSXNmODFlUWZQV20NCjB2UE1sdGQ3aGYzN3Y2NFhrS2dNd05WOThaWkNXdkpDUUZsSzRMZnlwREZGSkhGZlBlOFdFaEVrZnA4K2dLcnc1VXIvQURISXltU0sNCmNVMnErWFk3eTN2eExheVR4Q1lpTzUrcWdOUDZaTlR4VnR1dVI4UWtjS1lFTXgxVFdmTDJrM3NNMmk2enFkNndWSkxwYm9yKzhaRkENCkh3bFNEUmR2OWpsdU9OT1pHWUF0aEhtbnpIWmE3ZVFUdzJFTm5NRktTdkdCeWxZdFhtMU5xNWJiVGt5Mmw5dGJ6c3dqa1lsRU5GVWQNCkFLNUNaY2UwNmtpRFdMVE9pL1Y0RHhPMndQaVdPUXNvdGpGeGR4R1NsdXZwcFhjdDEvREw2VzJhYUY1VmU5MDAzclNxMW1QM2JzcUUNCmdjaDlwbzFwSnhIOHljdjlYR2x0amQzWno2VmZ5UUxldzNrS2tjWmJkeTZVN0Q0Z3JENU1NYVZOOUcxSmtFeXZGenRya0NPWitOZUoNClUxQkgrVi9Ma1Y0VTE4NDZoWVcrbXdhVlpSeGlTTkdqdWxSU0dWd0F3Ym1UOFdTSTJ0TEFFaGtTUmp5M1VCalU5dnB5SEV0TWowdzINCjV1eFp6M0VVM0FSdWs2azBvZXE3Z0Ribmp6UVlxVm0wTWVvcUpKVWFPUDdSYjQxb1BHbmZEU09CLzlib3VpL2tMNVF0UEs4bW02c0YNCm0xYWRDWk5TM1Y0aTMyZlJHNmZEKzF5eVRiNGlVM0gvQURqWm8wSUJpdkxxNlVDamZZUThnQjRERWxzaE1GVXNQK2NlZEFhU01YdHQNCko5VnJXU1V6OG1Ic1ZHRGlSUEpUSjlQL0FDUy9MVFRyc3pRNmNKNUVDdkVzanN3WTRXcmp0bmtFY3NTQnFMSEVncEhiam9nSFRaUGgNCjJ4YWlWc2QzSFBjdTZHQmlpOFZkcTFJUFdoL2w5c1Z0WHQwQXRXQ0tCM0NLS0lPKzJBcEJXWE51dDRzYVNURkFSWGd0Ti92d0piOUoNCnhiK2tXVVNxS0I2VUJIYzF4VlVpU3pTV2lVNTBwMUpOUGZGVlYwamtSa2FOWFhveWtiSDVnakZicEpaL0tHaHpFbjBqQXpOdHdjZ0QNCjVER213WnFkTjViMFZwUFNlMmljOVZWU3lOVDZOc2VGUGpOMjNsMndzbmVTMmpqaWJwRklhdVJYeHJqd3NqblJhMlZpWXg5WTR0U3YNCkpXVlFDZXYyU05zQjJZU3pJYUhXTFYxOUV3K29LOFJFbEdxQjREQnhNUEVSTGkydUlYamUxS280QVpYNHFTQjAyNzRLVDRpcll4QVINCmlwZEVpV25wTlFiSGNiakNBamp0V1F3ekxKd1BNZlpLbndPSGhXeWxrK24zQ2VwRGJJSWJWVFZWVS9FU1ZyVmExSFgrYkEzd25RUVYNCjNHYmVXM3VKWGEzSkhwUnEvRitaYnNlSStFNHQ4WjNzakMvbzI2cEhNR2N0eTQvRUF0TjMzWGZwdjhXTGp6Z2JhU2U4dUNQWGo5TkoNCkN3VlF2SUJRVFFzVC9PdTYvd0F1TE9JcGMwdkVwNmJGdUo0dHdYdDJQRWRhWXBwZDlYZ1dqUVUrc1BYakp4TlFEMTZZc1JrdER0RnENCkNGQUo0MWdia0paSlF4SVBSQUFUKzFpeWtRVldhNGpTRDArQW1aQnhsb0R2WFlrSHBRZnRZc2VGYTcvR0ZSbE1SSWVTVlhBQmJhZ04NCksvYXJpa0RaMTVLRlJ3T0VpcFdyRnQ0ejlvOHFIMnhaUkc2V3d6WGJURnBWakZ1U3FsMFZtWjVXNm9hOFBUNnAyYkZ0a1FtQ0ZvNWwNCkpmOEFkU00wWlNTaWlNZ0NvQm9LNzRRMUdRUkhvb1NPSlVlazNJY0dHNUlwUmlja2ppUXRsT3NzZktROEpDV2VPMnJVN0VpdTZnOU0NCmd5eXdYZXF3Qmk1K21LVTRTTnlOV0ZRU1I5a0NuZjdXS0JEWlRHcHhsUkpFeG40bmhJWTZQUXJUZmMwcC93QU5pbndsa3R6SmN1eXcNCmNvb1dCV015amlTM0w3UVEvRXczL3dCWEZSRlo5ZWtaWUdlVlhkQUk3bHl2Q2pydTM5NFEzRnYyY1d5T05jMTY4d2FkeXNVQ3E2cEoNCklkaUNSU213R0tEanBiZHhvMXRMREdyVy9QOEF1MGpVSG93YW9yVC9BQ3NWaUdpWFF3VDNFWWVhTVB5a2lKb2l2MFFxUGliYW1MTFoNCkUyeGpEZW5Ed1lLN2xtZW80MUFJV25XbUxWa1dUQ2NUTWxvc1N6TUJ3Y2toWEJOV0ZCL0wyeFpqa29NMG9mMEVqcVpGNnk4cVVSNjANCkpBKzBHNWY3SEZOTExpL2ltbkx0V2Iwd1VSQWYzSmIvQUNRMzJueFp3dzdXZ2pCTzBzZDZiT08wVWxZM25rWi9WQ2ltM0VCaFJsOFcNCnhabVg4S0piVXJlVkdTU1V4d2w2cXJMd2NjV0hCYVVVc3JkYVUrTEZoNFJhaFc3YXprdlZUMTc1WWhSV2NoT0pKS2lOVzJWdHEvRjkNCm5GaklVYVZwZFFSR2pnaVV4enlvUXZSL1RGZDJJYW5KaVFlUCtyaWtZaVVPckQxbzdNM0xjeUNxRUJoSWVZNGt5aU1jRm93cXVLU0sNClZJNUxNUnY2WWdqZzlUYjBIK0tWbGNxM1FjcW9SL3JZc09hR2ZWb1k3c1MyL09lVmxSWVVkR1BCV2ZnL2JsdnkzcU1XMDRObC93Q2sNClBRdkYweTR2UHJOM3dvRUtCUnlZZ2hRYS9BUW9Jd0ZyOEN4YUgxcGxtZTJQMWh4RkQvZnhSRWd5RlQ5Z0h3d09SampRS3ZLSWlsbGMNClhFUWtNVENhRjBHNExjdmdacUhaUjhMNHRCRmxjVEhxY3R2SmFyd2EzS1Nlc1FmU0tnMCtFMEFkaWh3aEppY2UzZXgyNjBxMmFTWm0NCm1hZlNIRFRXNEQwZEhyOFpVZ0FNRlA3R0Z6c1dhZ2pYMUptZUZyaXpQcEt3VldCQ0VsRjJEcWZpQ043ZkZrV3VVVmtrazhPbHgyY1ANCnFwT3E4U2k4bElrbGNNVllrYnJHOHA3L0FHZnQvRmdKWXdBdTBudjN1N0syTVZySWwxTWlpSzdoU1J1VHluY0hpbytMNDZDaWZGOFcNCkMzSmdRcGVhZk5sam9rTnF1b3ZiQkVsNHBZUkVtUmtGUUgrRThxNzVBeWFNc2dMZUwvbUIrWS9tRHpITzFwWk1kTzBkYUxIWlFBa3MNCkFLQnBHZWhiNWY3cndjVHJKNml6VEJaOVAxelU1WVlaSjdpN2RmaHRvbVpwT05lb1JDYUxYdmk0MHBFbWtaZGZseGMyRm0xM2Z6VzkNCm9xTndhRnBheXFPSU5TQXU2LzdMQ0ZsRGhTbXhiU2JlVmhEWkhWSnlmVGhIeGVpQ2VoS3I4VFlXTE12S0hrcnprYmxkWnNUTlpWZW4NCksxWXdKOEorSlk5NlRlbisycThzcmNuRENaTEpmTW1tV0RNMm9haHJqYTdlcS9IOUhYMGNrTVVqazBOWTBaWkZwL2xERUIyUTA1STMNClNqWDRiSFdaQm91bjZKYmFQcTlzek8rck1UYnFxQmZoUXBWazR0OFh4U2Z5NGVGeE11R25tMXo1anZiV1NlM3NuRVVkQkRPd0lsRWoNCktTUFVERUNsY2tJT3VuajNLVkc1a2w0YzJKS0d0QnNEODhrRUN3aU5MZUdUVVhrbGk1amlXRWE3Y1dQMmNteGxKTTdzaXlDcktPTTUNCjNaYVUzTzR3VXhCU2FlOHVaaVJLN0JDYUxGWDRTZkdtRGhTaEFRc3RRT1FHNXh0V1kyM21XMmk4c1M2ZmJ6R083S2NsSkxWb1RYaUQNClFqcGhpVlk1WW0yZU5FbmZqTEpKL2VrL0R3SUhnUEhKcW1NdDNwdWwzUnQwdVByMXNycEpJMXVTRVlqY0twYmVveVBWa0VwdmRWbXYNCko1WGtOVEsxYTkrbE1tZVNWUkxPWjdXUzZkdU1jTEloSjdrNVFxakt3U1VtQWtRbW9qUi90Vm9DVDkrVENyWTJZQU1CUkU2Y2RqaXINCi85ZjB4QmUyVTB5eHBjUlN5UmcxVkNHUDNqRGJHbHN4TUVyeklWWXhnbVpXcnpwVHRpU2tCdFBxeko2aFVwSGNnTlZpVitMcFE0RXINCnBvMWd0Z0kxOVFwdEdXcHNEMStJbmZGVmgxQ0xtbHU0UHJPdk5SSHVBUGNydGlyb2JlMlM1TXNhOGJsaCs5U3BGUWUrS3J4QmNvOGwNCkhIMVJ3ZmdicXRSMndXcWxhQkZmMCtaZmlmZ0RGTnhqeEt1dWJsWlVrUXhNekkxRlZqeHFmRENycDdpZEZoTTBUS3JVQmFJL1pKMkENCnJpcW84elIrbXJnK29UOEtyMCtUWXF0bnV4OEpSRHlQMmhYdy9aeFZmRE9zL0o1SXFVK0ZHSTYvVGdLb1F5UUM4bHRwRUFDOFNKS2sNCmRSWEFxQ3VmcWx6ZEgwbmRIUTBFVkFRMjMyZDhWUkVkbnp0QVh0RmlrZ0xOR29OZVorYTc0cXV1VVpyUkpZRlB4bmpORTVia1BHaE8NCjR3OFRIaFViUFVJWGRvSUpCSEtyZW5zQzdNUnQ4V0pLUUVYQkVZVElDR0pjZ3lTQndwNURzRlBiQWxVdHplMVo3aG5SUlV4b3ZFa3INCjcweVFWRFh1b1dFVU5Ma3NlWlU4YTFJT0tSTGgzVmJXN2htSDFpMktyQ0NWZG0rMnhBNkRGbFpraE5SbXZaYmRsaFNOQS84QWR0eTQNCnQxNmsvd0FNQllDVkxJSWtEUkt4U1NmaVFIVTBWVDNIMmYyc0RrUnpMb0xiVkZpWkhpS0VOU0dOWkFRZ1A4ckhyVEZueFkxR2V6dWkNCnh0N3FRU0k4alBic1ZOT0lBb3IxRGZIVUhmN09MS0dTSTVJbzJMeXpxRnVKVWxvcnJHYUdQZ05tVGpUaTJMU1owVXNnMEZQVXZGaXMNCm80VVkrb3RxSmFwSTVweVowRzFkdmgrSmYyY0JEY2RRZUVJdTkwbERNc2tGckJPOHpwOWNrbHB5NHFEOW1vNDlhZE1BREdPZTFXU0gNClVKQmJsQ3NTSzRNa1M4YWxRZWlub0JrbVhFRlZZUFJpNU1IUmdmaDZTSGpYa2FoYStPTEV6c29LbHUzSmJkUFR0Q1dlU1doSENSZHgNClJHQUpZNHRna3BBWHpHT08ralNKV0xGR1NYaSt3SXFGL1o1MTNRWXM0a0xidVMxaFMxc0k3T2E2bVUrb1pXcFJRQ1ZibkkzM2NTT0wNCjR0Y2ZxS0lzeXptNWoyaVNOaFdKUUVOU0tGeVJ0OFIva3hTQlJhdGVVZHJheHV6UzJ0WFI1UTVZdDlyaXJEKzhyVS9zNHRjemNpVksNClVXZ2psbVdUMW9MaXZwdUVMcWl4amZtVzNGTVcyQktXcEhZM21xV2FRWFpqK3B1cGNTY1F6QXFmZ1JlSVB4VnhiNVNtSW8zVmk4TU0NCmZwc0VXZHZUa2trSVRqV2xFVUR1MzdYK3JpMVl6S1IzWHlDOGFhUXhYS0NGWStJUmxaaUpnVm94QVljME5Qcy9heFhJTmxKcnFDMWoNCml0NUpZRGNHbnF5THhpM2NnRmxpM1lFRmg5cHNGcVBFS3ZkVEphOHhEY2d6U2Z1clZlSVBhdkVxSzdpamZGamJDSUpQcVczVUYyVlYNCm1MVE9pcy9HT3U4blJSOEpBN252aGJSd3FVbjFpRklZNDFhSlFvSWNxelNLRllzZVZTZnRVT0tZaUJKS25FK3B4YVpjcEdyK3R5WjANCkV6MXB5YXAzVFpsLzMzWEZKakFTQmFoRjdwNEF2V211dVJhT09PS01jd0tCeHlhb0ZEOFM3ZkRpcEY3aFVzSjVvNFBxOGxwS1ltaisNCkpYajJZbXA0QTh2MktZb3lSakkzMWJ2THpTb2dETElUY1FOOE1SVWNRUVBzbnRRWXBqaXpTK2prZ29KOUpRdzhpc2tOd25HdHJId1YNCjZQOEF1d09YODJSWlNzd1J0dzlySGNMSEhibTNrbVZmVGlLa2tFRVBUYlliaGNJWVk0OFVLN2tGSTg0WmJxQ0JKcmVnamtrNUdIbE4NClhpb1UwUHdvZmI3VFlXWU5iS1N5M2pXMGpOSDlUbDlPUmdXNE80NGJyKzhwaTJqbXQwMmV4aDA4VzZlbmMzU0JWdWdzZ1JpWDNma3INCmpaNkQ5bkV0ZVNGbFd1NTdXNmtqRnBjdkZHeFFSVzhJVlFOK1RFc2RqeVh4eUtJamhVcHJhTzV2ZVVMMEFVK21HYVVScWhxR0w4U3ENCk8zK1RYRnVqa05KZkhjV05yWlBiNmJBcHVFSVpyZUZHZGdWWVYza2RsaTUvc2l2REZQQmFZUHBrMDcvVzQzUHFvR1lvU2VjWU5TQ3kNCnN6YzJwOW1peHg1SmhITFNYdzZaclVjN1RYR3BQY3hRczd5Mnlvak5LSkRWRlI2OWdmakIrSCtYRXN6TVVrZm1MVU5ZakU0MEhRcmYNCjFXaldYNjJRSTJpVmxyeUNmWTVsZDZqS1pOVWpsL2dZQm8vNVllWTlYTWs5STdhS25xelhNcCtKdlVQTDRRdXpWcmpXemhaY0VwRzUNCmZWL0VnOWE4a2Fkb1UxdGI2amNIVU5UdVFIZzB1d0pFcEQ3eHM4ampnQVYrMlJsZkN6eDRxMmExQzNOaVY0VzlwcGs3amw5WElsSWgNCm9EVDRqU1dlWjZmRXg5Tk9YMkU0NVlCVFhrMjJWOVA4aTNIbW1DTy8xaTVhVzFtZmpCcDlvREhBdnBwVXZQVGt3YjR1L3dBWExHVzcNCkxEcGhJZXBrbmxuOHVOQXRGZlZMalRZTGVBUk5IQlp6T3pLWFJXQWVYa1JLV0xMei9kY2NIQzVRME9OanYrSFB6RFZKZFEwRzF1TkkNCjh2eWh4ZFdpU0t2ckYyS3lteXQ1dVpRcW54YzVXOVdUSmNLNUlBRkVhVXZseUszdExyNjdZNlplU01EWjZ3OHF0Y3E3VmpLWGNNcisNCnFyVSswZUtyeTQ4ZVAyc0lpeThVUUZzaTh4K1NyWHpobzkzcDlyNWdzN1M1VU5OUGNRUkpkR1dOVlpxL1dFa0JsZ0pSWG9ZK2NiY3UNCmVIaGRmbjFIRStVcFlWanVpQVE2S1NSU3REVFlVcmhjTzdWOVQwNmV3bVZKZnRzaXNRT2dCN0QvQUZjQ3BwWlhFZGxlNlJxTnhhbUwNClQxQ2lRa2J6K2tTektQOEFKNWNWYkZVbjFiVzd2VTlRdUx1NS92cm1WNW1BNkRtYThSN0RKSzVYV1hpc2c0amlRcDcxOGNyS3I0UmINCmtTcXcycFJYOFQ3NHhWQnE3UlNjUXFtb0kzMzY1WVZaTHBiNlBEcEt5elFMYzNzN1NLc0JYNEVqNkFrL3pWcmtXc3FRMDIza3MvV1MNCk1BSTZxdzltTytGSVdhanBTd3FySkdBSEZSVHJUMndKU3k0dXJpTzBGcVQ4RE42aC9tYllpcHhaSVJTZUk1YmdiQWVHS295YVJWdFkNCjdaS3lUc1M3MDJwL0tLLzVPUUlRL3dELzBCV2wvbmZlV1VoYzZSQTRZVVlKSklsZjE1U0oyMlVueS84QU9SeGtWNDV0Q1VxeThLck8NCmEwSHpYeHlYRlNERkV3Zjg1RTZOd2pTNjBPZCtOT1JFcUdwNmR3TVBHRWNLS24vNXlBOHNYQ0twMCs5Z0NtcW9ER1ZIdVBpeDR3dkMNCm1NSC9BRGtINU5vaXlwZGcwb3p0Q3JINk9MWThZWGhSTVg1Ni9sODg0bGtsbWlJQVZaREE0SUhldENjZU1Md3BQSCtjZWx5WDF4RU4NCmFWYkZ1WWhka2ZsUnZzMUZPMlJNclhoVS93REhmbDZWUngxNkVUQVVEL0VsUHd5TnJ3c28wejh3TkdmVGdzbXU2ZkxPaG9qdklCSUINCjRiNWJ4SXBNYlB6bFl6dEtqNnRaU0lRZUlFeWZ4eDRscEZKNWhncEFVdkxlUmgvZUJaVWFxamF2WHJqeExTS212ZExrS05FNGNrVlYNCmxjZkMzanhCM3cydEtWcEpkUVJ1MXc3aFpQaVZxaDBiMnArejlHSzB1YTBpbmVPWncxcEt3cVVOZUxVMkJxZjFZS1dsVDZ2cFVhT3oNClNyY1hYVm01VW9mYkZJaTB0NUd0dFA2RFNlcEFoWkdZYk1UOHpndGVGSklkVmswKzlqbW1sSmlmNDNWVmFXbk1mNU5jZ0d5T01sTTcNClcvMEM0VnI2elpiTzdZY25rNHNoSUozNWhnQ0ZaaGt3R3VZcEt2TUdyMkl1bHVyYTREc3ExbG9maGNEWWxLL2E0bkN4Q01zL09HalMNCnhJOHJ1bzJwSWgreTFQQ3VEaVZiNWordXoyVXMvcFJ5UUwrOGFSV1Rud1FiR2dQMnNsYktFYk5NVDhrYXRFMnZhaEpkMzB0dFlXekENCldrRlR4a2FRRU01MlBRQmhnNG5ObkVDT3daNWMzWld6ZWIxcmZVSUl4eTlNZ0Ixakg3UW9kempiZ1JQZWs4L20vVDlQalM3a3R4R08NClNpT0pCUjJEZDZrbkltVGxRMDVseVRpSHpITTF5SWRRdDFzNFhRdkhMNm5JMUJDaFdvUGhZbHVtU2E1YWNCSDNGMklydFlXMlYwYmcNCnAzQktqa3hwMzJHQXNQQzdrczFHNjFTM0FMUkdPeENreVN4VjVFbjdLaE9vci93djdXTnQrTXhsdDFiMDdYZE1qZyt0eUhpd29qTUcNCkhVK0l5UUxMSmdQSk1VbGlLZlcybkRod3A5TlFHQXI4c1NYRkFyWlJsa3RZN2xwMm5NakZDQUF3Qlh2dXUyK0JuRUVwYmNhMllVTnANCmFNWmJweU9NcnhPM3BxNCswU285c1c4YWVTeC9NVnRHaXh4eFMzclI3eXpzUFRqVnFmczg4V3lPRTk2WHJEYTZvMDE2cVhIMXExWnANCkc5T1VzQ1ZITGl1eEh4VTQvRGkzR1BCMVJrVXpUMjdmVkxGM3U0TnpHNlNSTVJ3MkJlUXJ5L1pYa2Y4QUt4YUJRTjIxWXo2NnJpL3YNCnJVcTd5aFdpalpmVUFKb0J5QlpTaS83SEZua0FxZ21VbHpPbHpJOE5wRklKS0JXRXNhT1JYNHVSQVAwY2NJRGloRHl0SGFTWGsrbzINCnlRV3lMeTlUMUtwUWo0NjhSc1QvQUt1Tk5vbHRZU1RVTk9qam5tdjdpMWJpZUJhNzlVbFFzYWtLVlFMekFRSHJUN1M0SE93NWlkblcNCnR0WVhGdCtqNDArdnhFMWlxOVhXcWptMi9MN1AyOTI1ZkZpeXlVRFoyUmRoYXkyVUNwYkNha2RYbGxsSE4ySTZMdDArakZybVFlcUENCjFEeXBFK3B4eVFYU1c5eXdQcGs3bVFicy9xcTNjRnRteUhDMllOWlEzaVVSUFo2dFpXNjJscGNNajd1MXdJZWFsNkUwOVg5a2VPRUMNCm1JeVFuSzVLVTF4cUNhWmQzYzZrV29WR2pRRjRoNm5TUWxxY3d1MVZMRERiSVJ4eUlBVkJxOTVKcFVFOHllbkhJR1MzTXpsSk4xWmUNCnA2bmY0VGphL2w0Y1JBVUxTL21sdXBvZFJTWllkNVVqV01zSFZnblhnVDlncU9LNGJSa3dBY2swZTN1Wlk0NG9RVXRaSHI2MGpubXANCkJyVmFWWmZocjFaY1dzemlQZXBCL1hrdGJiNnc3cEh5RnhHamhmWENnclZ3MUplcmRzV0loWXRML01lbVc5NnYxeVZwME1BSU1VS2gNClN3WDRWRVovbTZzVStOdU9MazZUVTVNWm9Vc20rdHRlclBhSXIyRHhwY1c4S2pneXQrd25CZ09ISUxYQlRaaWxBdzRVWERITnJJTFgNCkZvVnRYSDd5U1Fzc3ZyS3lWQ3JVRmdwSDIvaHdnT05LY1liSXFTSy9XeFdDM3UyK3NLZDU3aWhZSUdKY0FFS3ZRSnh3b3gwRFo1SlINCll3NnkwSHJYYVJLT0pMWGlTa0RrcHBTZ0E1VjYvYXlOdVVjOENhQVRiU2x0NG5lQ0tGSWdxdEkzSlZacFdOQXp1VnB4KzFqYmhaYk0NCm1na1M2bEdJb0ladjNmMmxQRlJ4Q2d1TnZpZGlBdjhBTC9sWUZpZThvQWlXWkxtVUFySGJyeW1oVm0yYm9BNlVIWC9KKzFqVGtqSkUNCmJVNy9BRU94dEJjQ1ZvOXdKdURzMGJGalJGQUZDV0IreXVHbElKNUx6SktZV2tveGQyVlYrS2hLY2pVS0VQSWNSUUovdzJObzhLbXgNCkNMVjJ2cFpKdGdxTjZpbG9sZXUvd1VaVS93QXA4V0VnRHN0dXJXM2xXT1M2dDRIZWRsVkNsRFdKQlN2UUx0U3YrcmtUQzFobW5IYUsNCkF1cnpUYkxTN2l4MGxHdWcwbGFzcGpnYWVhZ3BJeDQvdXR1cVpMa0tXUWtmWExuSmhVTXkybHpKZTZiTXNqVTlPNDFreGhtREhZeFcNClViVjRyd3BFcm5JZ2J1Qmt5N3JkTTBDeTFpK2sxTHpJMGtWcmVjaGJSekdVQ3FEalY1VTR0czNFN2pqaVkybUdJbjFFb0Z0TjhnK1gNCnBMbTRNc09rM1B3aUdYUzU3bWFaMkRWTEVSc1BoYnZ5eGpDbktPcXhBY3BNTzFUODE5WGcxa1RhYzF6cWx2YnM2d2pVWTBSRlZ4dXcNCldNcS9xYnVnWm0vdTJ3RWdPUFBXUTZDVHRiL1BUenRjdGJ2cHNNT24raXZFRUF6TWFuN0tzK3lxUDJkdVdIaURpWk5RWmNubUdvYXQNCnFzWTFHT2VWMy9TbFd2MGNic3hibHlITGNIL0tYamtoSU5QSEk4MHBzdFR2N04xbXNyeVMybkhJQjBZb3k4eFJoc2FjV0JveThmaVgNCkpjUVNDRXg4cjIrbnRyVUw2azZteGpKa21xYWdnZGprQ2dxMTNlMm1yNjFQcUY2ZlQwKzNCTUZ1YUJtU29vZ0JJK25GVW8xN1ZZNzINCjlhU0JmUnMxSkZ0YTFMTEVwcFZWSjdFakRTcGR4clJxZzRxcUtSU2pOdU9sZW1STVZWWlVEQldVbWhGUWV4T0lpcS9UcktiVWRUdDcNCkNNVmx1SFdPTWRLc1RzSzVJb1pKTnA4TmdyVzFBMTBoS3piN0xRMElIdmtXSlJHbnBHK2thaVdOSFV3TXBIdXpBNGFVSVdTNjRLSTUNCjA5V0ZkMlN0S2U0T05KQ1JYYXhxN3NwNVZOVUozTk8xY0RKQlY1RWtualhxZTJLb2lXU0psUklFcHdyem1yOFRFNUV4US8vUjUyQjcNCjVpeGJuYmc1WWVTdXFmSElxdVB4RGZmRldpdkhwL0RGWENoN0N2amlxNzU3NHE2dnpwNFZPS3RsaWExN21weVBDVjRYS2Q2NGpaZUYNCmVISXBUYWxhVTI2NUxpWGhWQmQzYS9abWtYNU13L1VjaVNWNFZkZGExaGFjYjY0RkJRVWxmYjhjRzY4S01pODArWTQ2TXVxWFFJNkgNCjFuL3JqdXZDaUU4OCtiRUpJMWE0SlBjdlhKQXJTTVQ4MFBQaXFGL1NzcnFPektocVBwR1BFcUpnL05yejFDeVNDK1JtUWdxV2hqUFQNCjZNRVpKTXFWWC9OanpEZFhadWI1SUxoMnB6WDArSEtuZ1Z5M2lZVEZoTnJUODJpaWkxaDBLeEptcURNNGRuQUlKTzNJZFRnNGxqRkwNClBObm5MVkprZ2poVzN0N1lScTNHemdXQ2pIczFLbXYwNE9yWUlLbW5lYjlhdWJNTmNTYlhRUEZnUGlZZENRQWFaWXBqVzZKbmx2ZlgNCmd1eGVvOEhwK25JaUJoSWpTY3FCMUlHeW45ckl1ZGhvamRINkxxMXhEYk5EOWJXUXcvdVhrSjRzeFAycWN1TlYzeVFhYzhZaVFwWGwNCnN0YmkxaXg5ZTBkZ1dQMWZtNDlKd1J5M2RxTFhqOWxmNXNyazM0NWdQUk5MbXRwVm1OMmZWbHNRMDhGV29QVVFIci9OMS9heXgxbkUNClNzL1M4dXNKTE1JN09TNmlGSUd1UzFWcVRRQXJUajltdGNTMjR4WE5iZlhkMWExbWtuamxLamxKR2prMVlnbGh4STZkdjlYSXM4TU8NCktXeVNhYjV1YlU3bzI2ckFndW5JS09ncEVxS1Bob2RqN1k4VHVKNldvQXNpdEx2VGJmVjRkTmhvem1JdHhqYzE1amJnUDlVZFh3ZzcNCnV2bGd1SktNMVc0U0cwTWR1OGZvUjFhN2tRbVM0QXA5cWhYZFFmdDhmMmNrMDQ0YnBDbW9hTXFMZHh5M005eXpOeHQ0eTBTTTBYVU0NCldMY3FWNC9EOExaRW1uT09LUk5LOWw1c21lRTIwZ1NTNWI5NGJad0hBajdLSzdGLzhuQnhOT28wa2h1R1E2UmNXRDJhU3BFTFFLZHYNCmdNUWIySVlaTndNazVTV1hQbWpTN1V6aVNWREtyaUtkVVVmQ0RTbkpkdmgzKzFpeng2UEpQa2hmcnVsNnBiQzl0WmhIT1M0aFlGMEENCjRzUjhRUWdmTGJGc0VKUTJLQTFYenJvZWwycGlaUDBocWFqa3lFRDA0ejQ4eXZUL0FJTEJ4TW9hS1V6eGRFMzAyZXcxYlNvYmxyZDUNCkxlN0I5U053eFE5elVlRzN3NDJ3eVFNRFNwUEZMR1JOTk42TVVqTEhHeFV5T1ZZL3RCK0tvTjkvM2JZV1BFZWkyOWl1bHRTdWpoWjINClp1VjBVQUIzMm9oK0VVR0tZbS9xUksvcEdDMGlTZFFwUlFlRWIvSHk3bmFtK0xSTEhmSXNYMUxTSmJhL056WlhEOHlyRzdnbi9lZWsNCnpHb2xaNnJ3SCtRZVhQOEFaeXFpN1RCcXVQWWltOVAxSFZOUEZidUpybHBCVlpSS0kwVlFRT1ppSit3ZnRjaU1JQkM1Y0VEdUNxVCsNClo3eWFTaStuSW9JQ1NSbDVVTmV0UUFlMlNZZmt1b0tuZWFwZFhHcHd4aXg5WDFGRWJ6aGp4aTcvQUJLd1hqOEl4Y2pIaklpak5NMS8NClQ3Y3RCRTBUSmJWQWlqVUtRemZDSzdsbXIyNURFTkdYVHlKUlVPdXp0UEhFMXNzQWpOUUdaRldRME5PTzQ1ZS93NUpxbG91dHJML1cNCldFc0VjbHRKTTl3YXduaDhDQmdUOFRsRit5VU5maXhZdzA4aHNFR21zNlBkM1V3dEt6K2pOd2VSbExocFNoSWlRS0dQTGhWbS9aUmMNClc3Z2xEbWxJOHdQY1hRaWdzTHllNDJqbmtXaXhWazJYbVFlUEJPUjRuL2dseGNnWUlqcW04bXF6MmVtTkpiUXhUTEV4aWRZa0lka0YNCkFucEdvRGtNdjd3NENXbU9HTXBWYVNMNWl1Ymw1V1RUWldaeXZxdVI4WnFlSjVJdFNPbVI0bk4vTHdBNW8xOWQxRzRWZFBSSTBkU3gNCkxURXFBQlFxQUNQMTRiYWZDRWR3cC9wcElZVXRpVnViK1F1M0JlZHF4VWRQN3hWSnhRWVdMS0wxcnpFOXJaeU8waGpDdEdpbUtQMUcNCkFvQ3lncnhIMjZML0FDNHRNTWNDVW1oOHgzeTZveXRwaVJDNkFFbHVXRFhjb1VraXFLZVMvYTdyaERrNVlSakd3V1J5bU9vaXNvSkENCkkxTmFCUUJ0WFpYRmZWRmZ0WVhVblVFbGltc2FocFNwSGR3M1UvMXVjTmJXOG5JZW1qdjErRUFKSEp4clRJdWRqRWtxdTd2NnRDOEYNCnZmWFRYZFZqTTVZdEpFVkh3ODNMQlN0RDBwZ3R6SXhEb3RYOHlKYlRSWFdyZXRHMGhqZ2RuQmRBb0lQeElkcTE2WThUQ0lqYkVmTWUNCnBhbjVrMXpSL0xndS9YdDUxZUdrVE1qaVZWK0dTU1FmR3dVQ3V6Y2NnWk9KcnBFRFpyemYrWWVrMk1pNlBvTEt4c2VFRGFsQ0FGSWkNCitGeEYvTHliOXI3V0lrNmlNU1FTeEtYekZmYXhjRVBmU0NHaFo0a2tNVE1CL05JMWYrRnlYRXpHUWdVb1RKOVljUldzUkNpdkdHRXQNCng2Ym1TUWtzN2UvTEhpYTdwaitvM0VhM3kyY0JXUjFGWm5VZkFvOE1OV3lFYlZvYmFPNm4vZURrb1AyUjhJL0NtTkx3cFQ1dnVvbGwNClN5UVZuYjRtYzdrS09tK05NU0dKeXRIS3hLMXA0L2hoUW13bnNyUzNzbzQxRXp5S1h1K1h4QVZQMmFZRlFHcGNGdW1FTGNvMkFKcDgNClBYdFRGVU5JQXk3clR3eDRsVWdyanJRREpBcXVDaHU0K2VGV1I2ekJIQnAxakduQU42S1NWWHVyZ1UrbXZMRlVudDdyMExpQ1ZEd2wNCmpsVmc0MklvZW93RkJUNjhpcmRTQ0JqY1JiT1pSWDR1VzdOOStSWW9qUzVlV2w2a3BIeHQ2VksrQWsyL1hrZ3FucUZsL28wVHhuWjANCitDdTVERDdTL3dETk9KU3hxNmN2TXpjZUFBQzA4ZVBmSXNsQTByMHhWY2pNRVlnL1Jpci9BUC9TNVA4QXBEVmsrMWF4dC9xdWY0NVgNCjRSYmw2Nm5lOVcwOWo3cTRQNjhQQVJ6UVcvMHlCOXUwbVg1S0QrckJ3c2JkK25ySWZiV1ZQWm8yL2hnT01yYTg2NXByL3dDN3VCL3kNCmtZZmhnT01wQlZFMVhUai9BTWZFZGZtQit2STBXU3NMeTJkYXJNbEs5bVVuR2lxL2tyL1ljVTdIR2lxcUZCTk1GbGJYR01qcGp1ZWENCjIxUWpyaDRWdHJJN3Jic2QwMnVGVDE2WThhYmRRWTgwRXQ0MGgxZkhCU0NMNW91MnRKNTBDd2psSWZzcU9weVFTQ0J6VmRQa2tzNTQNCjVibU5URXIrbThUbjRnYUd1RllDeW10eGFUYXA2TmxheUtramhSeGJxSVR1WFA4QXE1SUJ0bEl4TlV6Mkh5eFllU0xHMHZiZTROMWUNCnN3YXpzMDRsbCtHc2pNcDZVeVRsWTRqSU9HdDB5MC96eE5yOHJrYUJIcU1ycjZiM2h0U3pQR09xbDBGUlRDMFR3eWgvRXE2SjVaMEQNClI3VzcxbTBrQWwrTzZnc2J5SGxIYnhuY3hxR3I4WHV5OHNMUmxKdm5hZWFCcW10ZVliQzR2VUsra2ZndGxuVnVMTXRDcFdNQ2lxUDkNCitMZ3BnSkZnOXZkNnBjUzZnMnBYQTAxSVhGckRibGViUzNNalUySy9zZHNodTdDR21pbDFuZWF4WTZzZEl1bWx0cFpDVmlJRGxISEsNCnJzakJmajQvNU9BMjVYNWVGUFh0T1RTdEs4cit0Y1d5Nmc0VW1hYWxXZENhYzI5VDR4UUhKeExyY1lNcDFEMHZQTDI0OHVpR2E5dEwNCk9Lek1oTFFSd0t6TUFDUXRUeUlYbGtKYzNvSXh5Y0lFamFDMU83djU3clQvQU5GUjNKdWVRRXFwRXpFSjBKSkhQNEQvQUsyQk5RQW8NCjhrd2x1Zk5HbTNDUnl4TDlaVlpKWTdhSWZHMGFDcExOWGh5L3lHeDRpMCtEQTdnTlhXcVhMeXh2SkdscktxTHhpTk9hUE1wWlR4WGwNCnZ2eStIN2YyY2lTVy9Ed2ptbVBscnl4ZTIrcEpkNmt5cmJwSnplUVNBeXZ4M0I5TS9Hbkp2Z29jc0FjWFhhdmFnOUQrcGkvWnhGcUUNCjdROFNYVWhHTlQwNGtqYkpPZ2lTRHM4cTg2MnQzWmFsTmJxSkpHbEFlTzVkS0J2RWNoVUZ0dml5R1FubzlSb013QTNSUGtvM21tM0MNCjJWNUU2K3JHSlJHeXZKelFsdVpqNEt6QWdGZjhuREMrcmlhOHdseVZkYjBDMjFhMmwxR0M4bGdoaFJnWlJESTRVcHNRenNGVUQvVkQNCk5nbHpZNlhXY01CRHFHUTZEckdtalQ3R0JUTzF2Q2c0M0RFcjhNZjdYQlNRVi9reVY3Tldvd21WeVVOZHUwdXZTbjBneTNoV1RrankNClVKQ3Y5b3FXK0tvNjRMS01HbnNiaEJYWG5hWlpsMG0yb3M4YktpdzFDbmtkZ3prbWxUalpUbDBRNXFyVy9udEdpWXdCSkp5ZVUzcUINCjFqQi8zOFZxeS82eTQ3dE9PT09QTVd5QzJzTDB5Ukcra1dSWVZaeWdVdEcrNnFwa1pxRjJROHZTUTVaYlhsbkVjbTcrOXRMTzNtdloNCkpZNUlhRjdxV1dNUE13WG9vV254S293RXRNWUdaOVBOaTBWNWEzbDlETm8wQy9VWm8ya25sallwQUtuYmk1QjR1UDVNamJ1b1FNWTcNCm9xUzR0SWZVbWtjYWdxMGg5UDFUUkdOU0dZclRsOFlDNzRVRW1oV3pWdzJud1dLU1cvMWEydXVhU1hrMFFEc2hmcUNhMTcvdFlEeVkNCnhoa250YUVuMWlHU3llS3dKVzdWR0ltbm96eUFFL0VOL2hadkE1RGlMa1k5T1kvVnVnYmExMVc1bmlzeGNQZGF2T3p5WExDUXJGR3oNCmZhcVFRRlNKYUtmOHJKeExMTGt4NDRFZ2NLWTMrcHJZSTlvVXVKeXRJNWxlcVNUUElBRmVOVi9ZY24wL2gvWncyNElseHhzcHJkU2ENCmplUkpZMmR2YjZaQUcvZTJ6U0F5c0ZVRndSR0tjdjJYTE5oTFJqODBnaDFUekJlaWRrOUswVkxoUkcxeExHdHRid3hBcUVSUVMzTWcNCjhteUYyNUp3eGlMQTNSOTFKZlEycnRwMm9wKzlWSmJ1NVVyR2tqQWo0STFBTS94RC9ZNGRtRVFMOVFVTk9QMTI4Z1c3bGxVb2hsbHQNCnpWcEc1RHJRN3Qva1kyNU01UjRhQVIrbXlXZGhxRVQ2cXlUSTNxTFpQS09iUkR3cWZ0bkczWDUrTXhJQmRyV3ZhZlBFWnJVc3MwTlgNCkR0eFJTQjlyNFI0akZ3OE9LUU82SjhvM05nZE5lL1pGaG1FanhSVEZnZWJ4MWF2cEFBcFVtbS8ydnRaSU4ycmxLSkFCMnBFM1dwMnkNCjZYR1VsVnJtTmVjeFZlSStJMUtnK08rRndSczhsODBhZmR5UTNEMlY4a0thaE9YTVVwSHBoS0RkUVBpNS93Q3RsWmRyajFJNnNjVzANCjF1MHUza2oxT0NWeDhMSUdJUW5hbklud3lzMjVNOVRBalpVbXRmT2t0ajZrRmxEOVNEbmxjUnlPd1ozTzVxUjQ1S0xqbk9BeG1LRHoNClJwdDNOZDhuU2YwNUlsZUpmc2VvT0pJUHkyd0htNHVveW1RWTVNdXBzeXdXOEx5ek9hQ01JQVMzUUd2OHVBT0VKRUNtUWVYdktSdG8NCjVieldaNG9tSkpDdTRBSVVWYmlQMm0vWlZja3czVjlaODBTbXlGam8vd0RvZHFVNHpUQkZFamh0dU5kNkRqaEZNZ0w1c2FndFlyWkMNCnFMVnoxSjY1ZEVobUFpTk5rcGNjZXRSMHdjVEhkSXZPQ3d4WFJrcWZyVTlDeFA3S0RhZ3dIZEJCUTNsdnlUck92eFR2YUJZWUlsSkUNCjBteXM0NklENG5JMFVVbDk3cDdRT3NJUEM0VVZtQk5RckEwSUJ3OUVFSUdSM2FpU0xTVWJmTlIzeUtFVloyYlhBQ3hvWFovZ2pwM1kNCjdBbkRTOFFSSG1IVDdiUzlSbXNvcFJQNlZBN2dVQWNpcktQOVhDR1EzU2dCVDhRSFhKSnBIejZsTGNXTnRic2xHdEVLR1h1eU01Sy8NCmRVNUVJcEFibGV1OWNsSk5NKzhyM2YxMnlndExTM0J2NTZXalBXdjd4bVBGaDdNcEdSYTVJZlZiT1hUN204dEVrVXRFeXBNM2JrcDMNCis1c0V1VERkVzAzVjdHQzNsYTkrTVFxWlk0LzVwT0pVVS95VzVjc01kd2tjMkRIbTBuTDdUTnVWUGNtdFNNV3hlSWlVREhwNCtPQlUNCno4dGVYYi9YcjJTenNsVW1OUFZsWnpSVlJXQVp6L2tyWDRzS3YvL1Q1YWdVYlYzeVhFM0tsSy81MXh1MEZZd1ArZTJGalRqSFZSa0MNClNoZHdCNy9maUNrTERCQ1NRVVUvTVlhRE5aOVJzaWQ0RTM3a1kwRVdzL1Jkai92a0QvVnJqUVcyeHBzQ240R2tYM0R0WEJRU3UrcHoNCkQ3TjFPQjJvOWYxNENFT1dEVU9RSXZaS2RQaUNuSTB0cHZZYVpxVWhxOXdKQWV4UUQ5V0RoVzJSMm1nekVBc3lON2xOOEhDZ3lDS1gNClFKUDVGUDBrZmhoOElNZUp0dEFOUDdoZm1DSy9qandVeUVrTFBvRDh2N2xxVXBVRUg4S2pIaFRZU3VmVEpGWUtzTXhaaUZVQVYzTzINClY4QlJ4QlRhYVhTYm9KTEhKeVgrOFIxSUI3RlRUb1IyL2x4QUtSUlRUWGJ5M3U3ZTJ2YkxVSTdxR2RsamRKRUltaGxRVkFrVWo0dHYNCjkyL3RZMG1Cb29aL011b2M0NStFYzBsbUNpQlJ4NGh2dFVZZGVXU0JkbkRJREViYnB2YmFxeGhMeVc1WkxoZUxzcGJsUW1temRza0cNCjJPWUFWeUxQL0wyczJubDdSWUdVa1J0czBVVE1ESHlQZ1JUQzRHZkRPUjJSNytlZEgxYS9sdFVEU3luaDY3RVYrRk94TlAydWpZMjQNCi93Q1huSG1FemZ6bE5KZGlXMmRvL1RUMDFDS0NGWHBUcU9tTnRaaVFqaEpwbDVxZG5ybDMrOTFPeVI0NEplRkIrOTd1bzZ1djdEZnMNClllRlFUM3BocVlzcnVlMGwxU1JaTFMyL2ZSb0dDbjFhY2FNUHRjZUROOFA3V05OdUdVNzIzU1B6SCtsdFN0emI2VmNRK2hOV09HTjUNCkFpcUtqN3hUOW5LNU9aaWlNWjR1cVZwK1hHcFdrVVhvWDF2ZHBGekYxeWYwdnRFdXFxVzIyZkFJbHlQNVFzN3JKN0xYcUw2dDE5U1MNClNFSWx2QXBaeTRKb1p2VCtIQ1lsdmhrak5qZXBheTMrS0xTeVFTVzl6Ynd5cGZldEZJOFJqWURaZW5xVi9aYmw5bHNod2wyRnhqSG0NCnlUOUhhbnBzWnVOVHRyZEltVUlibFBqckNvK0JITGJveTE3NDhKZGJIUEdSNXBscFZ0YjZiWXp3UEtyejM3aVJua2RIbVdBQUdPcWoNCjRnZ1A4MldBT0RyQngvVHVvZjRxdnJlNG1ndGxhUzJVSUI2YUhueVk4UVdQenhaNGNBS25KcUYzcXRqZFJ4Sk04OFBGblF3dVFLc08NCnBBN3JrZ0xjMlZZeHZzelBTWVdzN2k0dWRSNEkwcVJ3MnIyNEswdDQ5MVB4YjFOYWNjTHBzdVVrN01mOHhTNmswc3lhaHFiSnBjVlANCnExdmJxRjlVRUZ4NnJOc3I4dmh5SkRrYVdBNW5teFVYK3I2UXpPbGcwbEloeWpsQWtqTWxBSTRuSTI1L1RsZEczYnlNU0tQSk1MSjkNClExUFVaSUo5TWUyaFdOSm8rQ2lJK3FUOGFFSzFHUmY4ckRSWUhOQ0kyS1pQWmFOWUIzYTA5RFVHaUVaa3VoRTMranBJU1BoTlZyWGwNCngvYTRaT0lhSTVaek5nZWxBNmZybWw4N2g3UzVGdXR6SUkydGxJQ2hBU2VhS2RnWlAyc096a1QwY2lMcGxCUzVzN2FTYU9WNXBsS3kNCm1Eb0ZSVFFxcEh4QWovTHd1bWtBVUxxTjlaR0ZvcmFONUpacEszVVNNR1JhN01obEMvRFVIbElGeUV6M09WcHNkRjU3SjZHa2pUZzANCjl4QnBmOTliK256UlZqVW5lWG1CeWR5TitPVjd1NGpPTXhRNW95ejEzU0RkZW5NdHJjRy91SVUvMGI0VmNHUU5IejcxNUZGazQ1SVMNCkhKcnk0WlJqeVpGcm1qSkpBeVhWNHNyTnl1SkZqQlVNUUR4V3UvN1ZNSjVOT0xMS0o1TWZHbGpSYll5cVpMMVdRUEl6Y1AzSzdFRUENClZLaHZpK0luSzZjc1p1UG1tbWd6SktwMUdBUlEyMEt6K3BRcXBjelVDZ2tmM2hKR1RpYWNMWFJqVkE3bFZ2dGVXSVcvMXRvWkxKWFYNClNXVUY0WEpvWkZPSGlEVGcwOCtIWUk2OTg1YVJKRXNkaElxV3NTY0VRZnNxT2xhYjVLUlJpd1RITU1hdjNzN2U1azFabU1wbWpXVlkNCkhvVlE5QTVIWDRzcWtYTWh2c3dXNjh5NjlwZDVJMGM1dUpTcnp6b3FqMG80NmdxeUg5bnBrZUp5eGhnUWlyZnpoNWcxTzJudmxNc1UNCklYNHJzS2ZzbnRVQWZEaDNRY01BT2JKL0s4bDk1dkVVMnFYSDFiU05Qa3BKSkNLU1hVb0hJb3AvWlJSMXlVYnQxZXBrSWcweVRXdFANCjhtcEFCOVE1UElha2VyS1BocjhQMlgvNExKdW9sbGtEeVE4Q2VWYkdSb3JWWlVXVWd5eU5NenR5SUE2TUtjUlQ0Y0lZNU1obnVWUloNCmJHeGdsNFhjVitKUGhveW1panVoREg0aWNOdFJrQXhYemZlYWRiYVd0Nk5PaFVYTGhabXF3TURLS0QwMkRjVlVuN2FZVFRiaWpmTkENCmFoNW50Tks4bzJ0dHBka3BhNXJMTmNYTVNzSktuNHVMZGFLZnM1RFp5cHdBR3hZMDNuM1Z6RjlYS0liWlQ4RUhKd2crZ0hLeVhHa3ANCmp6dGMwK0t4alB0emYrdVJZeE82RWs4eDJja29sK3BORk1OdlVqbG9TdjhBTFFnN1lySUtMWCtpeXlzOGx0Y014N3ZNSCs3NFJnM1kNCjBweXphSTVIN3VWTnZCVGhGc29oRFRRYVBKMGtrWC9ZcmxvTExaWkRwMmtxL0lYVHA0SGhYS2JMR2xDODhyZVhiNlFTM0YwV2xwVGsNClF5N1Y4TU1TVnBNb3RQdDQ3ZU96aTFORnM0aFNPMTVGSXY4QVpCUlZ2cHlWeTdscFExTHlUb2Q5RWswTjNGRmNzQ0pWUmdGK2hlZ3cNCjJVRUpUUDhBbG5GY0txcmV4bW5jTXY4QVhJaTdSd29qVHZJVjdwY2p5V2x6R1pTaFdPUWtIZ3gvYUh5eTIxOE1KSGQvbGxxUmRuZTYNCjV1ekZtZW5jOVQ5T0hpQ0RDdVNGZnlKZXh5VWhtUkFPdFZOVGp4QkZGMGZrYlVTckJKb0NXRktrc1B4cGdCV2lwU2ZsN3JkS284RG4NCnc1LzJaS1VndEo3NUwwSHpGb1dyd1hra01ja01MaVFGR0RGWkVId3QwN1Z5TnRjZ1ZmOEFNSFNZOUp1SUxSVzlSL1FpbG5sb0J5ZVkNCkVoU2ZZRGxqTGtvREY3enk1cmtzYU5EYVNPaDZNS0g4SzRJeUZKNFV2bTh1YThvRmJDY3NEMUNFL3F3Mm5oSzA2TnEwYU4vb002aHUNCjNwdHQrR05yUlJlalNhN3BjMC8xSlpvQmN3dmIzSjRNS3hTZmJHNDc0MmluLzlUbC93QVZhN1VPTGMyQ1NhRFluQ0ZkeElPNTM2WVYNCmFxMzhjaXdLOEE0cUc2dDBwdGl6YU9MRXRqRmk3RnNhb0FhNzFPQXNTcXdrZW9vOThERm1tZ3dSa2hkaUFPdUtTeXVLS0FLT0lHTFYNCjFWUWtYZ1BveVRKM3BSbnRpcTFvSXlPLzRZcWdiNjFSVjVLM0VnMThmNllKS3hiekl0c0dqa3Q1WlRPVkxUVzhsQWlFR25JTlVzL3ENCmRmaXlBNXBERzBQSzdsalVzb2ZqeTNBSDNpdTJSazJ4VFBRZFBNclRNOTM2U0s2eHBHVURsNVhORUZNaUhPMDZmK1dyZSthYWF3dm8NCmxNVm9wUDFpS3ZCd1dQSGZMZWpYcVRSc0pwckZxbjFKSmJTUjQvMldUbHlVKzlNaXowK1FubWwwbXUzbG5BdHBGRTZsOTNDTHc1blkNClZaNmZ4eGM2UUJESWRHdDlYOUtPVFZPTVZwSXhoWm9qUjZ0OWdmUEYxV2ZHYlozcDNtWFJvTEVhZVZqaGpXZ0x1YXlNMU5pWDY5Y24NCnhORU1SS1VlWjlJMTJDT1M4aW50N2xZVVpuaUVoRE5RQWh0d0J0WEFUYm42ZkNRYlIrajZ0cG1sV0ZuWnl0YmhDZ2x1TGljZW92S1UNCmNxMUc0L2wyd0psQXlLUzJ2bmJTSWRYdUVaUmNSUThoQUFTWXpJZDBjZy9hVlA1VGpiZExSZWtGV3V2TnVoWFZzMGR5TGRHSy93QjYNCnNmb3Z5cjFIQW9vSHpYRzBZOEpISk85TXU0N3EybEVhU1BLc1ZMbVNFeGo5MkdweUJJMkIvbHhiNXlOVVV6MHEvRi9wTEpMTExkUVQNCkdSVitHc3JCRHhJcUF5NFE2ckxjWmJJZldiUzUxR3hEZVhiYTNqbWc1UnlSU3hJUXhHeXJ6WXFWZi9aZjdIRk1jbmVrVGVZUExDU1QNCjJVK2poTGVLUUxQY3JJOGNqTWxEeU8vMmcyK0J5c2VDUTVGbEdnM3JoWlgwNktTNnRaQkhPWkExR0hOZUpWcWtkT0lJd2h4dFNaRG0NCm1XcjZnanRFTG0zZFpBQndOZS9Ub1ZPRnhPSkJ0b21uM1VEdzZyZFQ4aktzOE5zeENCUW9vaGFncFJqNDR0OE5WdzdJQjlGdUxaVlANCjFsTHdSRkF0dkp5SlJFM0pIRmlyZkw0c1hPaHJSTDA5NkV0TG14dXRidG1VcWJVcTVaNHhMNmJGQVNRelU0ZzF4Y2ZQS3VUS0w4NlQNCkxieHpYRnZhenRVS2ZWVmZzaXZUbFdvM3hjSVpjbGJNVWt0dkx0cnJLNnFMU0VTT1V0MWhqRWNjVWRRUjY5Q2ZpYjZNcms3Z2VLY1gNCk5icUdxcTBGeTBFdnhUZXRGQVhrUlQ2cWRLa0UxUmprWkV0bUhESEdkMkEzZWwrZWRQczdLL2pnZWFOcFpkVXZSQUNRU0dvcWNrSkQNCnF3WW5qL2s1V0NTN2JGcThFL1R5S1krVVovT0dwNk9XbUVpMlE1T3kzWUFnV2pFOFFaQlZhL3Nya3QzSGxreHdsWVEwL3dDV1M4TFMNCjZnWWFZc1lhNzFHV1NWV2xDOCthY1lxaWpmeTRSajZveWF3UzJSOHI2SlBCSk5aNmpkWEUxak5FOXhkenNWUm8vQklnS0t2S25mSk4NCkVTYjNUV0RUZEd1Ylo1TDY4aDlWNitvMXRJMFpkU0NXU1FtamZHUDVSaXlsTHVRTm5yV2dYbkczc1FMU0tOZ2tjYUdob215OHVYMi8NCjlaamdxMkFqeGJsYlA1Y211STdxN3U0STV0T2lZd0NXU2JqTFBLV0I1UWdiS1k2L0RYSGdaalVjSm9NZTFueXphYWZkenRaU1NtMDANCjVZN202QmF0STVUeTNZMDVVNDVEZHljZVFIbWhOY24xN1dyZVdEVDlPdTJrdUpPTThpeHVVaURnRlJWUWFEaUZ4b2xNWjQ0eVVkSjgNCnJhdnBscTZlYUlmcmR1cENMYVNBb2dvYXF4Y2NXZW43S0g0ZVdQQ3NzZ1BKUGRVMUxUWGhqZVBsUGFXL0V3NmVTRmhSK25KMVdsUi8NCmtISmhxTXJaWFphL29rR2lyOVU5Q0NHVjJFQVNnYVZZL2lsa1B1MG53N0RDSFZ6eFNPUWR6RWJ1emE2aHNwUk04TjFmeVN6dXdZbmoNCkZ2MFgyeVRiTFRCZ2QzZGVZdFExNnkwblNwcFpSTktDcThTckZsSjRFTTFObndxSVFoRTI5ZVBsYTFXeFczMWpVSXpmeUFQY3gyNmsNCklqcjBIUGt0WEg3VkY0NEhVWmMwU2Rna09ydzJ0blpTMkNYWXZyY0lWbCtzS3BRN2tqdjhUTCt5K0xacDRFbmRndXI2akJKcFVOa0wNCm1kelpxQkZCTjhRNEUxTEk5RjJydHh5TW5QMU1BSUQzcENEWGVsSzVCMXNuRTcweFlob21vN0QzeFNzS3RRMCtnNHExUWdDdlh2amQNCks3SGlWM1lBZFJpclZDUnZoQlYzRDZEa3VKVm9GUG45R0JXelFtbzIrV0t0ZkY0bjd6aXJmcXlyc0hKK2svMXdGWGV0TDNZajZjQ3QNCityTUJzOU1rcnZyRTRGT1pyOHNVRUlLNjFMVW9UKzZ1R1FIWmdPK0swaXRQMXFPOGptWFcxTjA5Rk5tNXB2SW15aHY4bmpoNG1CaWkNCmYwL3FVUjJFWlgrWGhUK09EaFNJb3VEelZkcU40SXlUN3NQNDRzMFF2bTJZZmF0a0k5bVlZb3BGd2VlRWlWdzFpRzVyeFB4aitLbkYNCmVGLy8xZVlHdTFCc1BIRFRjM1h1T3ZiQ2dsdzMzUFh2aWkzY0c3QWtkTWlWcGNhMEJwaXRPcnNENDRhVzNVMnhLQ1hIMjY0RUx1SXgNCmJGckNsS1lzUzVXZFhVOGExT0NtS2ZhVHFucEQ0bXBUR2xaTGIrWUl3bEdjbkdrY0s0K1k0QWV2NDRWcFVqOHd3TnNHL0hGYVJDNjENCkVSc3hyODhWcERYMnRBcFJTRHQzQUl4SzB3N1daMm1sTGN1UllVWWpicDB5TlV0SlBaTklzOGlrVW9CdWV2WEs1TnNVNDBUNnNZWHYNCkpDNjNFVWg5RmtOT09SZGpwNGlyVExTTmJ1SnAyMDYwUXgyd1V5Tk5LNVp1Uk5TM2F1K0ltVXp4QXlDWmFUZU5wdW4zYlhOd3MwM3ENCmdsajBvRFhZSDJ5WEVHMFlnRVhySG5XMDFHM0Z2YXcrdGNTRUxicUZIeFBVRUx0M2FtRzJaZ0t0WnF2bUs2NG1HNFdTS1ljYVJ4Z0ENCm8zN08zOHd4YThZRWlnRTBuekpjVTFPNXVKVVNKMWFGSmdGV1E5ZUpybGZFVzJHQ0lYYVo1anZwZFF0NGJ4SnBJR2s5SzVVTVFDQzINCjRMQ283NFJJdVhFQUJtaWp5WmYyalNhYzF6YlhjYVBTeXVLVFJ5QlNSc1RSdUkvWnlWdUhDUkV1U1I2VHBHczIxNk5RdVlBSVpvcFQNCmJxRi9kZ3BSZDZiVm8zMlNNQkRtenlpa0RyU1JRMzEwUWhpZElPUWNIaHg1YmJpbE1oZE04SnRWOG9YTUZ4cFpsdXRWbWlOdzVoWDANCnExa0ZTTnovQUNnbkppVnVOckpWeWVsTmVXdWh3V1dtYWJMSXh0K1R5U0t3TkFHNWNuZm9xcVRYSkUwNjNHVFBtRlBUcjdXNzdUYVcNCm90bzdkNUo1T1N6b2p5UHlQcU82dFRpZmJsZzRtck5nU1hYdER1NUVEVDIvcUNTcDlhT1dOdzIxYS9BelpKakV5aTFwK3U2cm85cUwNClJrWmxkL1VNc1FQRTFId2o1SUJpMXprSmMyVldPb1h0MWJ3YW04M0dTemJra0VsVDZxMHFRTitveHRvRUxPekdidno1SnFtcXMwMDcNCkx6QWpkZ1BocXBxQWFkYVlESjJHTFNXQmFQMDdYZU1ucFh0ejlRdWxCSDFnRXRGS2c3cVZyUnNIRTN5MGdFYmo5U0wwenpYNVEwalQNCnBOTHRyeHIrNGtkakxMOVhZS3hsWWZEMUhUOXBxWWVKaEhRNUpEZGJlL21INWFkNXJPNTBnbUNFVmxtRHNwRmR1VkFmYkJ4SUdnSVENCmNqYU5xR28yczl0R3N1blJKNnN5TXpqMDZEOTJ0ZVE1R1R0WERWdWRFa1I0U2pyanpEYVJSU3lyYVJReW9wU0loUVNvYndyVVpLVEcNCkdIdlkxb2Y1ajZ5SjdpM2d1aE5HSlBUQWtSR1hnTmh4V2dWUnZrQkttN0pwSVNIODArVEs3Ylh0Y3ViYVdlMS9SOXNiVmdheW9vQmsNCmNrQkFvclhsa3VOd1phVVhWeVlYcnY1amExS3k2WmMydHZhUEs2b1pZa2s1UDhYSGFqa01GL2tUQnh1WERSeGlMQktKc05WMUZ0UW0NCnNuOUc1Yi9kVU51aGltS3J1ZWFOeXB4eURYa0lESE5kOHpTUmFzQTlyRk1TUjZsQVJYdDdkTVc3SEFFV2hwOWNzNEpJM2tzNGVZZWoNCk81bzFEMHA4c2VLbDRTcTMvbkdkYlQ2c2xvdjFPTnhPc1piNEhsVTFCQUIrMWp4bE1NUWx6Um1pZWVMZU8wQ1gxamNTdEx6VXFqUnkNClFtSTE1UnVzanh5NzEvbXlmRzQyWEhPT3dUSysvTnpVZE9BdDlOS1dkaUFDaXl3cXJWSzArS3JQeTQvc25sZ2xOaCtVUDFFc1g4eC8NCm00OTJsdEZKTkhLVVpKWlJ4SEF1akhyOUdRdHk4Y0FPWmJ2L0FNeDcvVXJNV0VrZG5GWXVTWm1oVlZka08vVWVHRGlMYU1adXdwMk4NCjFwVjVBcXdPYmVhM2hFY01UZkdHVmQ2S1IwWS9heVFhc2twZzhnaFI1dGxnOUFreUdXajIwS1RJUlFNZHd2VHRoc3BBc2JwMWFhN0YNCnAwbHZOQnB6cmR3SUkwblFzYVZGQ2V2V21HM0hsak10aXEySG1STlpUVVNKSEZ4Wkp6OUVic3pGcUVHdjljRnVLZERHTzZVV1BtblUNCm9mVWxnZ2pjaGlxOGdLZ2R0bXh0eUk0UUVyMS9YYnpWYnVLVys5T00rbVVBajRrMHJYZmo4c1R1MTZ2NlI3MHBMUnFhTFhlbVJwMWMNCmk1NlY1RG9NQ0F0RENsRDlydU1DVy8xZUdLclQxd0VLMWpTdXlTdUdOSzdEU3JUMXdxMlFNVmF4VjJSS3RFQW5mcDRZRmFKTk1tcTMNCmNnZzRxcFQyNk9sRHVjVTI2R0FSeDBVZlBBUWhWS2cxQjZlT0RqUEpWTGdRMjNUSkRkVnc1ZEtZU0Vob3NRTUNYLy9XNW95QTdnVmINCnRrbTVieHB1U0srQnhZbGNxazFJcDlHTEZ1aTl6dmdMTUxRdGFEQUZLN2dlbmoweVRCM0VENGUvamdLdUNnZlBBcnFNTnlkaGkyTzMNCmJvUnR2dGl4TGl1MVQyMkh6eFlya1dpMDc5OFZiRFRmem43OFZXczBwRlN4NjRxNUpYNmhpQ1BmRlZlTy91VU5PZlhwWEZWejM4L0UNCmhtcWUzamlxaTRZaEN3TzRyVTc0UUZXUW10d3cvd0Fud3BrWlJiSXFXbnlYd3ZoQkVHK3FsejY3QWZDZy9teUhDNStuTE1OVjlKcksNCkdlMWxSU1ZwVmR3ZllVeXN4Y3doSkxheXZiOGZXQkc4aTIrOTFFVHhIRUhmQndLeWJTcmUya2xodm9oOVdqdFNERkdpMSswS0RsOS8NClhKeEZPUG5uU3BkSkJmQVR3bGk4WTNVR3YySHBRbnUyRkdsbFpWdFMxbVc5MDZTMVVjNUpsNG92WDQvMmFWOThIQzVjWkpGcjV1cmINClQ0VnQ3amhLa2FrMmtTaGp6VWZ2VzlSZnQvRlRBUTVPUGRJdEQ4eGEyYmo2c3FlckpjSVk0SEtubFJRU1ZqcDhUZjZvd0lPS3QzcEYNCjMrYWtNbW5RV3lXc21tM2oyNnhYRHR1aDlNQ3ZwZzkycCs4L2F5ZlJwamhKa1NsOTNZMlYvR3JUVHlOSjZBbHVDclZxNy9ZWGYzNC8NCjhGbFlHN2ZmQ2dMbDlGc0xKTEtPNUVFdHNoRTBYQm1CZDl6Ung4UFhKSFpCanhMQnFEWDFnc2x4ZUxBaFg5KzRPN0JUUVZwOHUrUkINCnRwT0VSS3p5NzVpMTZ5RjFIYlNyTnBUTUFnaytKWGtQVUpVN1Z3dGtzSUlSUzZ4cTEzcTBPb2ZDVmhKU1hhaXFwVVZRZUJUSnRHZUENCklUKzc4NVFReHgyMEJqbUVqVWozcVFXMk9MclB5cGtVUlA1aTFBNk5jd3c4V2xqUXFKRklOSzdFbGY4QWpiRnlkUHBhS3BvL2xLYTMNCjBPUFdMbjk0aTBsU0pLc3hCK0hlbitVY2lYTU9RUlBEM0pTTDRRcExjeXhxMDB6K29DNDVJaFZ0eUkvc0szL0M0R1htRUJkZWFiSzUNCnVYVDYvUDZqVkh3b2thQmovay94eGJQRWtBbVdydDVmMVBpK255UzIxeEhHa1UwVEdvWmxGU1NQY25GcXg2ZzNSWCtYYk9TK3VaTk0NCmx1L1JlY0dXYTVLN0trSzFUNGUveGNSL3M4dGk0MnN5VnlSMnE2UjVpYTBXMGpraHVBQThwbGpKNXVrUUxNcXExRHlJRzJKWllkWHgNCklTNzErd3NiVmRBbjAxNDdpSzNFc1VhcURJRktoeXo4Unk1QUg0djVNcWM0bmExdWdYSXR0UXNyUzRrTVg2V2xTSzNJM0N0SUJ4clgNCmZwaXhtS0ZwbnBsbGFXOHNWeEp6ZWFQNnhidmNzQXl4eVI3bGtYK2JkRC9zc1d2SE85a2Q1UFc3dUxmVXJtR0NTZlVvbmFMNjhVNHENCnNVbnhsUkowTDhUVFk0dXUxbVNtTTNubDdVNzI3Tnl5eHhvcElqU1JoeTJPNWFtTFpwOVR3eFFkMTVac3BaRk40N3pSeEhsNlVYd3ENCnpWNmxzSURDWGFIUkNhM0hwa2s4RmxCSUlaMkg3b0Jha2ZSaDRYSjArYmlXVyt2Nk5vMW1iR2FKNWI1cWw1V1BHdGY1Vjc1RnVPT1oNClNQVTdkZGJBdXJaQTBmMlNISVU3ZGQvbmdMUEh4RDZtSjZob3R3OHl4UjI1NUdvUEhwOU9CWmJzMTBDMjBpd2lTTFZ0SE4vY21JeUsNCmtWMTlXUEVEYzBFVDErL0NBMHpNeHlTdTU4eTZHdDdLbG5wTnhaUm9hNzNCY29mQTFqVGZMUUU0eE1teXlDMTA2NXZ0T09vWDhUZXUNClJXR1Blb2pIUWtqOXBzUEM0dVRWVk9sQnIyM3FHTnZkb1IxS0ZxYkRiSVNEc0l5Q1RhVkpjUXozd2NHMU10Q1BWTkN3SnJzV0l5S00NCnNndWplWnhxRnpMQ3MxdW5Gak1YSVdvN0FETGVGcW5za3k2czBsNm9odEkyVlkyWldoSkJLbnhyZzRYWDZpVmhVazFZbzhhelc3b3oNCmdFQUZYMi8yT0hoY0dTNDZyRnU3SklGWG9URzFDZjVSNzVFeFVjbHI2dllJUTdsbEoveUdQOE1IQ2xvYTVwWlArOVNxZTRZTVAxZ1kNCjhLcXE2cHBqSDRicUluL1hIOGNlRlZaYmkxZjdNMGJld2RjZUZWeWxHRlF3Ky9LMWRoQ3V3cTdpY1ZjZW1LcmNWZGhBVjJIaFZvQUUNCjFIVElxN3ZURlhVR0t0NGhWbVRsRFpYVUdWeFZvN0hKeVNGcEFQWElwZi9YNW1BZVJyMHlUSmVFQk5CaXJpaEcyS3JsVWNnVGlydUsNCm5jOWNWZFFERlc2SHJpeURtREViSEZrNElCN3F1TFc0S3gyclR1QmdLdU1aSFU3NEZkeEkyeFpoMktYWXExaXJSQnhWb0sxU0ZyUTkNCkNNVlYwbWRBdndySndIUnV1S0NvMXJkcXdweDR0UURyVWIweUVrUTVzcHNwZEpzZEZSeDhielZWRVhkbmtQOEFOa1E3V0ZDSUtHMHINClNEWlFTcGZXckxMZE9KYmVBU2ZDZ2J0VG91SlpSemIwbVZIaXRoSEVRc2hJSk1lNUNuWWtmekxrRnd5dVJkZjJreWFYOVZCWkNzWUENCkkyclh1UU1XSXhjVXJTZlFiYUsxaGIvUzVBNUpGeWdOZjlVajN4Y3c0NkNhZVc1ZElPcDNFcXVaZnFxaHdrblpUOXFsY211UVRLNjcNCmUzdnI0UVJYRHh3VDFsa1ZFRWFtT3V5S2c2QWZ0ZnovQUdzQll4TWh6VWZNTHhXMXhwOHNFU1NHeUJhTkFkbElxUVRrVzZNbTlMc04NCld2cmN6M0Q4R1k4d3YybFlOdmt3dkh1NXJpOHE4V2wyN1RLckF5cVJ4Q2xOdnd4TFpHYUVnQmlkRExibWE1K3NlbzBjcWNsa0pCV2oNCmVQWDRjZ3duRUZVMWU2a3ViU09BeENUMTUyanRpRlZDd0ZPU09CMEt0WEZyaEdpMCtsU2FXSTU0M1F4UUVHT09hcEVUUHMwbkg5b3INCit6aTVCTmhmYnlSWE1EeUM1ZFVEY1huVmZUVjI3MDhjV3FxVlpOS212TkpXQ0NFVzZweTlPYjlvbXRRdytuRlJPbldlbVRmb2xXUzgNCmRwSlZLVE13cWVTbjRzVStNbGVvZWE5YXNOUGgwNXJwekZDMUZVZllkRDE1WW9PTy9WM3B2ZDZVSWJVdkZxTXJ6TU9SallMeEJicUYNCjloaWlNOTZTYTIwRyt1YldXNWphTUY1MUFaMUJQQlB0ZFA1OFU1NVVHVzNHaFJOWTE5VXhPMUJJZzJKSDdJQnlRZFI0NUJUSFI5RjENCklLV3VHRUR5RUFFSGtSR3YyYW5DMFpzNWsyZE11WmRUa010ODZ4Mlo0d0NKaWg5WTdzeFlibWcrREZ6TkhIZ1czV2k2amR5bXpqdW8NCkRkM1FrUmJ5NFQ5NkZJTE12cUo4dTR4Y3JKbXJkTUlQTDkwdW82WU5UTVVrVU1rSzIxdzBvUlluUlNwZGVOSFp3UHM0dEkxVnNmdXQNCmExS3gxcTgwK3hrTHZGTTd0TEp1Q1dXbk1EK2JJbHlPT29zZzBpejEvUTlJTnhlU0JJYngvV2hzMllzOWZzbHlCOEtjbEgyTVE2clUNClM0aWxGNTVqbmtsUDFXMWtia2VQcUVFSUNUUW5KTkVkbU9hdXVzTmZSVEpkdkducUNHYUlqaXBYcVdVbnZ2a1M1V0hjcVZ5ZE0rc2UNCnJRd3pBLzM1Ym9WNk1mbmdkcUk3S2xuRnFVdWtYRWwvOVdtdFhRdkFWNHRJRFVrVkl5YkF5L2VLT212QmFhSVZlcVMwZm1yamdTeDMNCjJKd0ZrZDVvZ3gyVjlZdGNXNmt5cWl4MjFwVGc2c2Z0dTdIN1hqa1ZPVXhVN1h6SGIrV05SZjZ6cHRwcmx3M0dKYmkrTXZHTlNQaVgNCmloSEpSa3d3eVlQRWpmZW50bDUxdE5YMWRtVFNyQ0NGVm9aSUluamplUWRGVDR0K09MZzVNUmhFdGVZdk5rOW1qK25IQ3NqVTRxcUENCmtmUVF4eGFjR0F5TEhWODVMS09NMDRxb0pZYkoyOWdNWGFISHdzYzFlN1hVdlZwSUE4UURLS1ZKRGRCaXpFNmRhNndvMCsxczRtZTENCm5pWDQ0Skl3eVNNZHVScmsyZ20wR3VrU1JhakpJOUZFcUVPWXpVYi9BS2prWk9McU1kQzFDVVBEZkxhUk54amppTGswbzdWMisxL0QNCkpSY0NTczhUa0luS2hwdU9sY2tvNUtEZ2h1bFQySGpoQ2xUSzh2NWY5V21GZzc2dkN3K0pGcjRFZGNWVWpwOW94K0tKQ1BDbUt1YlMNCjdLdjJBUDhBVkxML0FCeUxZMTlSaEJwSEpJdnNIWWZ4T05XcTRXc3creGMzQ2tkL1VyK0J3K0dydjl5SzdmWHBLZUpDdGtURmdXL1cNCjFsUHMzZ2I1eHIvREJ3MmhkOWQxc0xYMUlYLzFrWWZxT0h3VmN1cTZ5QWF4UU1QOVpoajRMSUwxMW5VVisxWm9SNHJKL1VZK0N5Y3UNCnUzQ2lyMlQvQU93WlA2Wlh3dGJmK0lFREFQYlRvVDI0Zy9pTUlpcW9QTU9uMW93bVErSlFuOVdTNFZYL0FPSU5JSW8wNUh6VnE1RXgNClpoVVhXZEpjZkRkeG42YWZyd2NLVjZYMWsvMmJpTS83SVk4S3FnbGhQMlpGYjVNRCtySGhWZlh3M3g0VmYvL1E1dnhQYmYzeVRKMUcNCm9TTzJLdDdtbmZ4T0t0OU1WZHhOU1BERlhjVDQweFZ2b3U1cVRpeURRY1UrV0xKdGh0c0FNV3R5MEswUFh4SGhnS3QwQTJIVEFydTQNCkkyOXhpekMwYkUxNmcwcGlyZER4OE44Tkl0cmlmQ3VCYmN5bWxNVnRyZmtTT25ZWXNseEFhSXVOd3Yyc2JRU3BwSW4xbUljYVY1QUUNCmZMS3BGRUJ1bUZqcHQzYnUycXBNU3R1QzhkdlNxa2p4QnhEc01lT1VodHlUNkpMdTlpZ3ZibVpZNHlJNW9vYWcrb2pidW5qVmNTek8NCk1oTWRTdVlZdFByR3FweWk0UlVGR0ZUV2dQdmdwamc5TXQwRFBkM1MyYWVuYkNNS2dWWWExQUEzNXN4N2tuR25NaUtLV2FXTkx1YlkNClNUeVJ3WEk5WmI4OHlyZ04vZDA3WUtaNUxXMnMrazNkN2JwQVBVbHQwRU4xZktDb2NnR3ZKUjF4RWtYa0EzSVI5M1kyVHlRTHBzek4NCmQyNmt4dXpWUEYvdEtld1Z2MmNrUTRKMVJFdlVySnBjRDJVbnhGemNBR1djN3NLQ25GZTFGK3pncHpJVHRCMlhuSkxIU3BiSzZpVloNCjRRRlNieEEyQk5NSVdpU2l0RzgxYVkrbXhpVzRWYmxtZjFTVHdQWHJURnNNU0ZuK0tiYWU3anNMT0g2NUo2b2xvajhDcnJzQ1RUcHYNCmthUlVncWF0YVN4L1ZMdStsUzNNY3pYQ1cwSStHdlYyZGoxWTRrTTRIWkxMeEw2K3RIdVZ1RjlOMkhwUnRVczBiSDRtOE5oZ1pReWMNClBOTUgwclZZTFJVaFQwL2pLd0tRR0JKNkx4Nllwbk1LaitaN1czdEFicDFqblZRcklEOFFkYWhoVEZFY1NWYUxyc2x4UGVsWTVZN0cNCkZmVlNKZnRCbjJEVTNxcWtjc1V5Z0FhUWx6ZTI5N2VSeDNpcktKWkZEYkZHTzlDUUtBWXQ4bzhJcEU2L2JXdHZjaTFpZFdFdEZoU00NCnNwUksvYWF0UitPTGpnV1VObzdhMjA4bW5XYm9ZNFNRczNLcXNhMTN3MDJTSXJkSHdhaDV5dHRVdG51NE9kdWhaWFVGVzdiRWI0ZVQNCmdaTUlrZGt4MUh6RHJiV3QzZTJicVl0UFFQUDYxUVdyMlVMWGNZOFRXTklJODFIUi9NTWx2Q2x2cU12cDM4bGJtUUhjVWJvYTlLWVcNCjg0KzVIV3VyUGRlWk5QaWpsQWlYMUhlWlNDS2xDQVB4d3RPYkVlRk9mTWVxM21rMkF2RmtpbEhJY0dkUS9Bbm93SGJHblhhYyt1bUYNCldOM00rclcyck1TelFTZW8vY05UY2hoNzVFaDJlbzVKeHEvbnE4dllabHNtK0tOa1I1WmFrQXNOa1gzeEFkZERHU1VYNWZ2VjFLOFoNCjVJMlZiZU5CUHpiNGVoK0ZRTnUzS3VGblBBVXU4eDJsdlByTnY2YnVnRGZESHlxckJ2dEVBKzJSTGJwc1pDQWV5MDY2Z2VKbGRRNVoNCmZVTGtqaWhwNFlIYThRQVMyOTh3V09sMlV1bWFmQkpHMHJLejgvakZQOG52VEpXMStBVEs3Wk5QQlozNjJSZmlmM2tjZzViOXZ2N1kNCjgwRUVTdGkydjNHb3k2bWlmWDRwa2FVOFhpUEJrVW5vT1BoZ3BtQ09xY1hIbGpUeGEvV1plY3M4YTFVc2Fpdnl3MnhqUDFlU0QwVzINCjFTVmZoOUtPMWhsWW9XRlNXL200akcwNWdKQ2xubWVDUzJaNzlqSmN5eXI2YWRsUmowYWc3REN5d3hFUWxGbDVTa3ZZZlh2bUFob0MNClpDUWlqeEp4UmxTdTlpZlNQTVZ1aS9GYk15aHlOMTRmc2tId3hjS2NpbkNhTkVOWWlrU0dTUXFHbGU0YzhrTmVnQjc1SzNLbFhSVnUNCnA0NUw1NEZRSXdUazlLN2tiVkl5SmNEVTNTUjNaWDlPRjJCNExGUS9NLzdXVGk2K1RiRnBXTHQ5bGpVVTY1T2tBcmZpNTlhOGRxOXMNCmFVbHc2RUhyMXdzV2dDVFdsUjQ0cTJ3WUNvVWcrR0t1QVk3MTY5dStDbVZ0Y0dCSkk2OU1hSzI0MUFxZW9IVEJSVzFyRW5ydGtnZ3QNCjBaUlVLUHA2NGxDM29LazduQlpWYW9KNmJyM3hzc2dWM0FnRmwzOXEwdzdyYTFUUlNWQnFjRkxUUU1tNWI0alRhdUVJSWNCVVVJb1QNCnZYdGhRdGFLTXFTUUNRSzlNRk1nVk5vSWpVK21wSThWQnhwYlV6YTJqSGVGUCtCRmE0ZUZiV05wOW5UYU1EL1ZxdjZqandyYTBhZEQNClNxTTZmS1JzZUZiZi85SG5IeTI5c2t5YkEySXIxeFZlb0lGQlFEeDk4Vlc4VDM2NHEzM1BpY1ZkWGNlT0t1WURyMzhNV1FhSU5LRHINCjRZc204V3R3SGhnS3V3SzRBazloODhXWVdxcFZxandwVEVLVjI5ZXVTWU8zOGFZQ3JkSzRGV2hhOU5zV3h5Z29ycXU0SSt6NDc0UUcNCk1saG8xeENRT0lCTlB1eXVVV1VVVGY4QW1PM2hzR3RKSTJNaEJYaW5VZzlDS1pGMjJsbUtBVzZKRHFCU1NlMUxjZ29FVUVnSkJWOXkNCkY4Q01YS21BV2RwcDczT2p3TFBJb2ZnUklBT2h4Y1NVYUtVWGNVbHJaL1YxVDZ6T0ZaaFhwUmZHcHhib3UwUzUweUcza2d1bWpXOWYNCjk3TEdlSUZXK3lCWCtYRnN5SUZZVXZkWW5TMWI2djZhRGl5ZkRXdlFudGxjVzR4c0txbHRPZ1l6eXErb1hGVW1sZWdwOFFFYWhCN2YNCkZYTFhXNU5OWjJSZHVJWlhYVHJlNUxTbFdrZU5DVk1rZ29PQy9zamt4d09YQ1BBTjJPYXY1WjFLSnBZdVFsRHZKQ2FWSk1pN3VBVC8NCkFMNzc0dGtKQW8zeW41YU52WXlYV3BKQXRGa2lkWkU1RWNmc3NyZUp4WW1KNGdrOXRlTnBPdlNYaXFPTXljTnFBNzlLWXRrb292elINCmU2bTlyRjlaUi9UY1ZSeWZzMTJwUWRhMHlKVENnRTQwaUI3eUpQVy9keEdGUUNQRTdGYWRodGdSS1FTaTc4MCtZOU11bjArV2FRMjYNCi9aSzdyVHd4YkRFU1RSTER5OXFHbEZwV3JkRmVURTFWeTM3UFRyU3VMV0FZbEVhUkdxYUxLWGl0Z25wc0JPcEszQ2xUUWNxZHNXT1MNClhyQ0M4dnZhMzhIb0dDTXd4c1Z1NzZkNk9yTWFxSTY3MXhiTTVOcXVwRFdaN2FlT2hodEltTUJtSUhKcWRsSjM0NFF4eFRISlEwdlINCmRXMDRmV3JhN1VTT3l1cXVPU1VJb1FSa2x5SmhmTnJGMzZpckFFdkxhQVRjVVk4U3A4TnVwL2x4cHJ4NVJIWXFlZ2FQcWRxRExjM3ANCjlDNmlFaHRtMjNZL1pZTlhwandzc3B2a3M4NDFqdE9jQ3d5U09CRXp1dkp3cDIySU9MVmhCNnFIbG55eTY2bXhnbVpSYXhxeVNqWloNCkpLYjljSWFOVHFBTmtCNW8xZldTMG1rM1hEZzdxVHhCNU10ZTIvV3VTUmd4am1pOUcwaXhONU5GS3pyRUlsbUNjaUN4QTZaRXR1YUgNCkZ1ajV2cXNNU3lhZmJMSkZDNWxuVlY2dFNrakl4UHhNdGNEWGowNUc2MjUvUk5ndzFDUGUxWkZoamVGeHlvVkpkbVBjaXRNVzhZclUNCjQ3bTcxQzV0cHJkRnQ3V0JTa00wbTlSNDlhMXlNbS9IQVEyS0hhL2kwK1piYTRSdWJLNnE0RlZaeWE5TUNjb3ZreDZlSXo2NEhaQ2sNClJaUVBrT29HTFpIa2pZcDcxTmFna2hrRENLVDkwcEJLZ2RLTmtnNDB1YU0xUkVpbTlXUFRJNG10NVFvYVB1N0NwUDQ0VUp3SnRhbTANCjJTVm8xZ0FYOTFFNUpMZlRYNGNpV0VmcVFtaDNkd3RySUpveEd6c3pLQnZ0M3hET2F6Vjc0ZlVaRmRnRnAwSnBXaDZaSkVZbENUNnENCmRVUm9iU1dOdFBlSVJ6MjdEaVlTQlN2UHZ2aGJSQzB0bTBocnVaQ3MvcXBhUkF6UjFCZmdOZ1J0Z1RVUnpWTk10enFOOUxEQmRUSkYNClpxcGlRbWxDM3NSU21MUkpVbjBpZTExUjU1cGl3ZE9OYUFnYmp3T0pjSFZja28xYUZVMVp3Q1FuQmFPM1E1T0xyeXM1bFR4QUJwc00NCnNZeGlaRUFjeXVLSXBDc1R5clUwNlZ5SUpMdDgrajArQ2ZoNUpUbFAvS1N4OFBCai93Q3FuRC9tTFpFNE9WNm1sUWNJTnVIcnRITFQNCjVPQW5pL2lqTCtmQ1g4UzBvd05LVkdGdzFTTDR2Z1kwSUgwaW1BbW5ZZG5hV0dmSVl5TWg2WlM5UDlBY1RqRXF4cXlrMFBqMXhCYk4NCmRvWVk4ZVBMQW5nekErbWYxeDRQOTYxR3JPL0VzS0hwVHJpVFRWMmRwWVo4dmh5Smp4Q1hEdy8wSThmKzlXdkNRZ2ZsVXRzUWNRZDENCno2V0VjRU1zU2J5R1VaRCtwUzVvVlZRWkRVbmZpTzJOMjM1TkZpd1FpY3hseDVJOGZoNDY5R1ArbktYKzU0Vy9TSmNVYXFPSzErV0QNCmliZjVJQnlRNFpmdWMwWlpJNUs5WERqL0FMeVBEL1Bpc1dKQzlHWWdrZ0FDbjQ0QzQyZzArSE5rNEpHZnJsR09QaHIrTCtmL0FMRmENCnlnR2lscWR4NzF5VVdqVnd4UmxXTXkvcGNmOEFPNGlzTlFRYUQ1RTRYRWNCUThOdmJGc2NWYW5YRmlXNDR5N0xHVFN2ZkFUVGxhRFMNCitQbWpqSjRlUDhmOUlycGtSR0hDdFJRbFQ4aGlDNVBhZW53NDVBUTQ0enFIRmpuL0FFc2NKOFg5YmlsNjRmenZwOUsyU0FwR0dKK0oNCmpRancyeGliS05WMmQ0T0NNNUgxemw5SCtwK25pOVg5TnRiV09rWWNrUEp1Q09nOEs0VEk3K1RsNGV5c1ZZNDVESVpkU09LSERYQmoNCi93QlQ0LzUvSC9XZzRXeUlxaVVrTkl4VVU3ZHE0OFhjbUhaT1BHSURNWlJ5WjV5eHg0Sy9kOEV2RDQ1OFE5WHIvcWVsRFNSbEdaUDINCjFKRk1tRGJwdFRnT0xKS0V1Y0R3di8vUzV5bS9YSk50THFZb0ljUUQxeFl1eFZ4K3lNVmNlVzFSeDhCaXJpT2g3REZOdXJ5UHc3SHgNCnhXM1lzcWNldUtDSFZ4cGk0Z0VVYlllT1JUYTl1dUsyMXhyamFHaXZidjF4VnF2aGlydXB4YkhBa01hYkU3WVFoWSt6UjA2cWFWeXUNClJTbU9pZlZ6ZVNvMEhxemxlU3RzU0F2enlMbmFaT2RKdUlyU2ZqNkpRUHk5Tk9RZGhUcXpVK3prQVRic1NPN21xU2FyY3o2aW1uV1MNCm84c3lsd1hKVUtGM0pOTW0xeUZEZEhUYWRlaXdrTWdoSkxNZVFhcmtENTR1UERVQzJHMmNrMGVySzhOdWs4b0xLUk9uSkNPOWVXM3cNCjR1dzRva2NrL2thQzBoK3Z5bUlmV1lqQnkzOU1zdlZHNC9ZWmNneHhpUjZvZXhzZEJ1TFNDN25uaW1NYkgxbGFSdVZPTEJhZkk1SUYNClpSa0RzcDZkUFpSeHl3V3BCdTVTVnJVMUZmc2xTZDlza3ZDVHpYTExxTnZmUlJYV29wSkw2YmVrakVLUGVqZDJ5Qkt4Z0Fka3ExUFUNCjdpM2ttQjVjSlpBQkNXQlF0MExxUERHMjhNbjBueTVZeVdNalRBUzNiVWRYYm9DTnh4L1ZqYmh6bExpNXBmSmYyMTdkQ004WEZvNGsNCk5EWDk0TnVCQjdZUTVCR3k2WFdMZTJjMTR4UnltdFIwNStBdzB3aGlKUW1wYVlkUlNHZDVSOVhuVmZUSUZEVnVtK1Fib1RBUzJIUXINCnBiNVk3VzZjb3ZJUzhoV3ZId1A4eDdZUXpPU0o1cHhkeGF6Sm9JRWNNYVNTcHhrbUJweUFKQ0lmK0xLL2F5Vk9LU0NiU0xSa09uWFENCnNMeUJaYnAyQUVUMUtCbThhWkV0OGp4QmxzZDdmd1d0ekZxTVNUeU96dEVzTldVQnYyYW51dUJ4NHhxUVUwbHZvTlBGeGMyelJSeGcNCmJFQW1oMnJRZGNOc3BHeWdOTjh5K1lkVG1TMnM1ZzhNakZZWlRRRUNJZnRmdGJZMlVuREc3SVNQekQvaUtPNkRMSTBzdktqc3RXcjMNCjZISGlMZEdNVHlVYmVIV0xxS0tXNW5DUXJJcGtpM1p5cWtjdW15L1RrbXFab2JQVHJ1NzAreGhpRUJDQ1JhUkw3RWJFL1BDSFRlQ1oNCnozWXZxTDZlbDlhM3M4VFNTMnJGNUtBbmx0MitXQWwya01kQkY2cmVhVE5BbHhCS0t5clg0T3BCN1U3WkcyWUI1SmZKcUVWckZGSVENCkpKWkQvbzhDMW9vOXg0NDIyamtyelhHalMyVHBkMnkyMFVUSXFPZ3J5NTFyOFA4QWtzY2JhVHhYc2hsdHIyMGpsc1lPRHhSTUFselgNCjRGU1ExSDA0R3drRTd1dHJMNnNXdUpDWnIyMWNmV0N4NWhvbS9hakhoaXpuSUlYVy9xeFZraGxDM0VkSkxTUkR2VnVneFlHWGNsL2wNCnNhaFBKYzJOek1WblpoTUdVYmxSOW9iKytGcmp6M1RPNHM3OHlGNDUvaUttV0pIQStOazZxZC81Y2JaVWlQOEFFOFVsb2lveStyS0QNCldJbmZrUEVZR0lqUlEzMVVYWTlZM3NobFpPYlc4UFFNUnVCV2gzd2hzb1VtVWRoWXo2Q3pjQVpEQ1EzSWZFSFhzYS90SEpOSEVRVXQNClRSTEN6c0lXaGN4STNFeWhoVnp5KzJhZUs0dVFKbWtKYlgrblc1anNiZVpXblJwQkxxSlExRVROeUNNUDJpMzkzaTBTVGFZcEgrL2wNCmpXR1JnRUZ6R09VTFU2Y3FmWkdLWnBUZExxa09vRkwyT01SU1JlcERMRWVTT0tqY0hFdXUxUEpqK29QeTF1UWY4Vmo4TW5Gd0pMbEgNCkdoN2loeVJYRmtNSkNRNXhQRXF5aU5tNWN3Tjk2OXNBSkR0OWJIRnFjeHl4bkdFY25xbEhKeGNlT1g4WDhQci9BS1BBcWVvckVrQVUNCm9PQlBlbURkMmY1L0RsbEwrN3JIREhpd2VOR1BGKzdQMS9UTGgvaWR5akRNYUN0ZGlkdHZ1eDNVWjlJSlpPSGc0cFpPS0gweGg0UDgNCjJQSGl6UityNjQ4RVZPTjE5VXNLS042WklqWndkRm54RFZ5eUF4d3dxZkR1ZUgxUjRZOEhwakw2dlY5RWVGZnlWMlVzYUZkbVh4d1UNCmVqZWRUZ3pUeDVjMGdaWS8zZWFQMVJ5Y1A5M21qL09qL3FrWERoNmdPM0lWcVI0RVV3RnYwMmZER2NKVG5pOFdKeWNVOFk0SStGT0INCmhDUHBqSGlseHkvellLY2xQU1ZhaW9KcU1JTzdxZFNZRFNRZ0pSbE9FNXlsR1A4QVRyL2lYU2NaQXJjZ0Nvb3dQVENObXpWNUlhc1ENCm1KUng1SVFqaXlSbjZmby9qZ3FST29Db0NDRkIzOFNjQkRzOUpyOFVPREVKUjRNTU1uRmtuSDY4dWIrWnh4NHVHTXY2dkZIK2lvb0MNCkpWTDBBQnJYSkViT2w3UGxHT3FqS1JqRVJueFNsL0I2ZjV2RC9zVnlGQXpFa0ZxMEZlbEsvTEFYTzBjc0VKNUpTTURQaTlIRi9kK0gNCktYcm5IOTNrOVRmQkQ2cEFYWWdxU0J0WDZNSGM1WERqSXp5eCtDQkdXT1dHY29RNFllSnhjY2ZWQ1g4My9OL2hkL285U1ZDOHlSVTkNCks3ZHFnNDBXMzh6by9Wd0NISHhSNHVVTWMvU09Qdy9FeFovUjRuRjZlQ0NpV2k1TlVFZUFVNUxkNTZlVEFjc3BHSjRQNElZNWY3K1UNCmZwLzVKclFVZHgxVlFLRW5jMXBzY2QwWTVZSjVoc2NPT3U4NUpSbnduZ255L3dCVTRmcGlxenpjVkE1QnBCeEttblQ0ZDYxOGNpQTcNCnZ0SFdpRUkzS09YUER3Wlk1Y1BGd1I4S1BIS2M1eDlmaVQvZVIrcG9zWGhWV1plUllsdGgwUHlHRUNpMDVOWDQybWpHVXNYaVN5R1UNCi9USDB4bi9INklmWC9QbDliaEpFV2ljc0I2SW93M3FlUFNtTys3YkRWWUpTdzVKVEEvS3g0Sng5WHI4TCs3OEwrZHgvNXEweUpNWXANCkdiZ1kySmNIdUNhN1lLSVNkYmgxSng1SnlHT1dISk9XU01yOVVKVDhXUEIvdUZHU1NOcFdsTzNJMUdUR3dkRnJjNHpacFpQNTh1Si8NCi85UG5uRlZKNGpZOURrbTVxaHhZbDFEaXhkUTRxdnhWb2I0cTBldUt0OUFLOSttS3VPTE8zS0RpZ2xvZzQyeGJHQlc2R2xjQ3V4VjMNCkpodFRiRlhOMHhWYUI0NHM3VTFXaFg3c1Z0dWNCQXBIZHhYSVNTdGk0cHFrVGwxUUlDeDV0eFZnT3hPUmM3VGJKOWJ5aTVTNUxwR2kNClJLQWlRdHhia2R5U3c2NUNqYnNveUhOSDZYYTJscGZ4VHlBaTdsNG9xZ2gwQ3ZzUitPVGFkU1NSc211b3hwSGQ4akpJSVpxdklPbkENCjFvUzFlMWNYVDRvUzRrbDFEVlZ0NytHR0VFdnlEVEl0QUNwQkNtdjgxY1hlNG9iSmRxSWltdUZzUnloaVp1Zm9qWUduMm1CL21iSU4NCm1PZGMxS1RUZEg5WllZME1TU2JCd3BMRGJ1UmhET1JQTlA4QXlwNWYwblRJcFhTUXltUTBrbGMxSUhnUGJKTk1wRkF6MzBFOGVvUlcNCkVzY1BHUWNwQ29jbGVKWGpYOW40c2lXVVNrVnY1THU1THdwZDNBampaUzRaM29xOGVwUHl3Tmw3TWh0citXR3pFWkx6d3h0Nk0xN0MNCkNZalFVKzBldUxEaENVNm5kYWVrYWpUNDFSVmI5Nll0OWlSVW5KQXRvRzI2WjNrSGw2RkxrM053am83eFRhZktsVFBIUmZqQitadzINCjBBekIyQ1cyVjFxVjZkUitwb2JlakdTRlc2R09teXFQMmF0OFdRWno0UjFUMjExSlYwK0Y1Z3NEbEY1b3hBWU4wYmJKQnBOSkpkYTUNCnFNZXBzTkhsTWtFckw2a0trYk9kcTFPRnlmREFGbEZYczhra1NMRlpDSzVRbDJuWXFXWGgxZGZweUpSRWlrUGNhbGQyRjNiZldMaDUNCm8yK09UZ2lrc2xEdXRPdlRBa2dVbVdwZVpMSzkwNFJXdkltbndqaVFDS1Y4TU5OTVltOTBxOG1hZGRXanlYeGdKamRYQ2dOWDQzYmMNCjc0MFczTElWUTVwaHF0MnQ5cGlTUXp6VUphTTJrVVlKOVVNZHkvN09CamdzZlV2OHUzdGxKcG90blVSeTJ4ck1wSXJVblk1TzJHV00NCnU1UnZvTEs3czVyeVNWeWtUSXNMRnFvYUU4cUQyT0VGY2VQdTVwckpmUW1ORStHaFVMWHhCR0FvQmtEeVlqY3dRM2FTeTZlV2l1NFMNCjBkMUZRdEdRUHNGS2ZEL3dXUnB0c0lEUUpKSUxxUVhNTEdaWk9MeXlLWENpVGRhRG9hWTB6SkZNbXY3dTJTemFTNWpTZElsTEl3TlYNCnF2UTAvWlpUalNBQlNsSHJNVjVvUU1FZEZtaW84cElKNWREVWRhNDB3RWJLUTNYbVBVYmVHS01SZ1hFYUdMMVczTHh0MllZMHluRUoNCmRwa0dxWEZ6SVpJUzg1TlZjTlRnUGZ3eHBvRnNoMDdTZFN0dFRYVWpNalJSS1ZtQ2loSi9qVEdtVXh0c2pXdTJ2cENiZmNLd2VHUk8NCnFzQlJxLzVPQm5Eell6ZW8xcnFYMW1WQVZrNWhnaDZuRmxLazVzdGVzWkxTM2hOMUpITVBnK3J4cjhURmVsRGhEV0RSMzVKem9VTUgNCnJYTE1aaDZiaG80NXFiY3VwMnlUVGw1N0lielpheHBGTHFFQ2w1VW9aQXRDM0VkMXIzeFo0enN3M1R2TUUxazh6ckFrcVQ3eVI5aUQNCjNOTVVFMnlmUnJxRFV4eXNvRUZxemNMMjBuazRxSzlTZy9scGhwSk44bFBXZFZzN3ZXUmEyU2hiU3lnOU9CVkZGQzFIUVlDNjdVOG0NCk0zemY3bkpRUDk5RExJaHdKTnFXQlZXcWRxa0R0a2lHRGJkUURzQWRxZE1GS3FBUmdjVHVldzk4YVZ6VVB3TnNBTWtGV3J6VkR3TlANCmNkY1Zhb2FqeDZFK09LYWJMbHFWTkNvM0dKV2xyU00zdlRvQjJ5TkljNWZqMDY5TWtyUk5HSlhldlhGSVdyeUkrSmRsTkYrV0MyVnUNCklGUHM0V0pjV2JZQnFMMXBpejhXWER3WDZMNHVIK2s3WWdrK05hWW90b2tGVzQ3SENBZ2xwbkZmaEdOTVZqQnlLMXA4dXVCWGZ2QW8NCm9BQjNPS3RNbFNLamNiMXcwcTNrMVFld1BYR21RY1ZIVnRqM3hway8vOVRuM1lESk56ZkUweFlsM0U0c1hjVGlyZ09oN0hGWEJhbW8NCjI4Y1ZiS2dZcTBWcVI0REZYRmE3MXBURlhGU0QvSEFWYW9hSDN3SzJRYTVJSzJQczB4S3RBVXlLdDhhOThWYUsweFYxQ01WVXdvTWcNCkovWGdMS0xkMmhvaDZKVVZQeXdNa05lYWVMNjlqdGE4Rm1BRE4xK0h2a1MzWXBGUDBYUXRKaE5valBIRmRLWXA1bjNjY0JWVHRYN1MNCjRIYTQ0bWxmVDdZeWVsSkNwVU9XOUZEU3BqQStCdXYydHNVWkQwVHEyalNlTjVyOStTM01Zamt0NVQ5bjAzcURXdjZzWEhoanBMSjQNCm9XMWE0bTAyQmJsMlpHZHVXeEhFcFJSMEZBY1hPeHlwTGJtelNQV2tPcVM4UkF2cXhRcTVCTG5wVTVCc25FSGtxWDNtYU96dmJaWVkNCnZWWm00SWlqN1hOaHQ3a25KQkVaZENsSG1HNDF5MjFPNGtzQWJVUFUrZ2k4dWcrNm1GdEVRVVpwVU1TNmN0K0dwZXp3T3prVUNlb2oNCmZ5OU52ZklsaElVVXl2OEFXTHBHUWkycXNUZWs4amdIMUN3QlpRdTlhY3NERmRmUXhTYVM0dUE0a1Z5eVcxcXJKSDhaSEg0ZHZwMngNClk4VzZYNlpwVmhhNlk5emNySW9sTExKRVc0aFNUUUdvcmkyemtpenAraTIxcUplQzNEMHFIWTE0Ky96eFpSa2dkTTgyVzFxOXhIR0QNCnplQ1JZM0swNHV2MlR2aW1XTVNWVTh3YWJxY2xzMTdDV3RCYThKMnB5WnBpS1ZKL1o4Y0lZQ0FURnJUU2Jxd2E2c0dTenVJUUpJNUkNCi9zaEE1VkZrL3dBcmJjNUpwaktSbFI1TjZ0THFzWWhsTUVYck9WUnAxRlZBY2JuYklsdUlBT3lHNGFqY0JyZUNDTTNLb1FiaHRoOEkNCklUaC9JR0RmWnhDQ2FWYnZVWmJTRDBKNEZReElpU0lpZzA1QURyODhrbzNTWFRQTVY0bE5QRWc5Y1NGWW9lNTNKQXlRWkdBRzVaQXUNCmpuVGJxV1ZyOW9kUm5pVzVrc1VIN21TTDlvMS8zNE1yazFTa1R5UXQ3UGJteXVJbzZRUE90RmNBQnFqcHYxT0JsRWs4MUhRckJyMlANCjlFWHBrdFliZE9aWmFWa0xNTzUrZUVNcDdEWk1kZTBtd3M5UFAxUzZjekpRVkxoajkxTWswUUpKM1F0cHEybVd1aUswWDk0MWVSWmUNCkxTT1RScWtiZkxGdE1kMEZiM1NjN20wdmh3aGxpOVZHQm9mWWhzV2RVRmVMU05KbHQwbVFTU0M0VlJLcjFBcVBFZE1XSEVtRVhsL1ENCm9JbWtXSXJJUVNhRTdrYmRPbUs4VEVOUnMwanVwSkpBU2t3cVQxSzhmbGl6RWJUNnp0VlNZMjhVaXZJVjV5OFNPUUgrV2ZERmdSU0oNCnQ0bWlRclBJR1NwNFNWSEZrSjN3RmlKV2c3UjdXWDFMZU5pNGhaZ1lZeUZENzFCSkg3TytSUzZlMnNuMUMzaXVWVm5veFMzakE0ZysNCjU4Y1ZVMmlFV3V4ZWxiQmpBck1wVURZZlpxY0lSTGttT3IzOGR0YkM3QVpia1VWa29hT0RrbXJoU0s3MTFyK040SVdNRUxnbzBnNjcNCjl2bGl5clpqQjBLOCtQMEkvVWk2STFHQk5NV09ORFd3azArK1Eza1VrY01qZ0NWcWdkZTFPMlNjWVNNV1J4YVo5VjFLV1ZITHd6eGcNCnFUNGc5dnZ5TW1qVVhWcFRlaXV0ekFDcDlNQ3VYWTNCa3ZVTDZvQkJCcFFudGtqellOMHJzT29iZkFxcUNlWUIyOENNVldzcnN4L2ENCkl4VmNxOUFEOHhpcXlqVVlrR2xhcWNXeG9zU0FDdGNXSmNHb1NxamlPcHhZdHFRU0tudlFWMnJpclRBdFJxOHFmUVI4OFZYR0phN3UNCk9WT2dyVEkwN0Q4bmlyKzloeDdlbXAveGZ6c2xlSDZQNTNGSnBrQUZRM0kwclNoNmZUaERWbnc0NEFjT1FaWmYwWXpqL3dCTkJCWVQNCktONjBGSzRYRVd1RHlBWEZXbEpBTysvU2h5Y0ZhcUQxSDNZSkszeDZlQjZiNUZWdkFFQUxVanZoQ3JlTEwxQnJrMWR4cHZzYTlxVngNClpCYnhJSkFOVDc5TVdULy8xWUJ4MzI2WkpzdHNDdTJLQ1crTzIzWEZEUkJydDB4VmNCUWdNUHBHS3VBRkQ0ZHNWYUk4Y1ZhSFVqRlcNCnl0ZWhvY1ZiSzAzTzVwZ0tyUTFlMkJYY3ZiSkJXcU5pcllGZm93VXJYVUU3N2UySlZzQ3FqQXJYUUFucmlyZnA4bUFIWHJpa0ZVVzANCkZ4YlRzMGdRUWhXUkNQdG12UWUrQ2syakxXYU8ybStzRUFMSXZHaEEyWlJ2a0M1ZW1JU205c3BMdlVBYktONVhmNG1XdFFCU2c2NEgNCmRESUFLVG0zdWIrMW5hS2UzcndpVll6R2VRQkZBUlhzY1dneEJOb2JWYlM3Ukl5OTBVZGdaRXR6VWpZalluNmNXZkRhYzZKSTlwTk0NCkpZMWphaXNwTktHdmJiRmdRUWtubTViblV0YXRrdEhVdEp4RWpkU29IVS9MSU4wQVlybThwVHlTeFBKcUxMTmJLSmJFcWdJYVNwSTUNCmZka2dqSnVuMDl2WjM5OGtia21BUmhwQ3BLOGl4SUkvbXBVWkpFWmtJVjlKbWZWMnRiY2lHd1NNL3VnQVRSNlY0bjNwa0N6a2RyVEMNCk8wdDFFdG9seURkcEx6Y01Rb1JRQUE3RHZTbE5zRENKSlNDLzFuV0paWGpza0NLS0EzWFBmZXZSZm94YlJoQ1Z4WDJ0eDIzNkxsZzkNCldOaVhlWmp1eWc4aU1XVTRCazZycEN3SkswZm9Ub29hS0p6dFVEY2dmdDVNUkRWZExOV3Q3R0xUeWVLZXJSWXk5RjZxdFdQMzVCdXgNCm5aclQ1NytEVFlpbGx3RFFTaVkra3BWM082UDdDbVNEVEtPNlhyY3JQcmtFTm84VnRJSTBhN0hGakN6S0JWWFVBOWNMTXlzVW1tc3oNCnJCQkl5S3k4UlhoRTlVSWI3SEFEQlNJamFtS3hhOXFOcEhETkRJc2tNaWxKSVdXcmdyMFYvd0RLeHBzbEFVbkZ0ZmZYYmE0bHZyWm8NCloyQ3U2R29xcWtBRCtPQzJNQlNWeFhXbWFacThPb3RaMW5pazVLMzJpNGVvUHcvekw0NGVKbmtGb2lhK2h1OVJ0YmFhYVJySDFlSVcNClNxeXhvMjVIUHd3RXNZUkVWTFdyQmRMMWUzbmtLelF3U0hsWXhGcEpHaUhTVGZiamhvSUVyVGUxc3pxOXVMMjF0dlRna3F0cy9xK24NCnlQY0N2aGhBWXhsUlNTNmkxTFJicU80MUsyNXdJKzdjaTNJVitlS2RrZnFsL1p0WVRQYndCazQ4Z25nVHZ0dGl5Q1hRV0Q2ekhBNEwNCk5EVlVDcWFPckExcVIvTGltWjlKVHJVdjB0YXhpT2FJa0p1SmszakNydnZUdmkxUmlDb1czbTNUN21Sb0lSS1pndjJBaGJjajI3WUMNClVUZ0FVUGMyWXRvTFptRWozbDF6YVJXRzZnOUEyOU1GdHNaVXFhZFlUdzNRanY1RWgrdFJFUWNHSHFNdi9GZy9WanhNWlN0VXViVmwNClZkT21JVk9CU0dFQTFQTG8vTGVndzgyRVlnRkI2ZnBjMm02bEJFczd4eHVyR1FCYXR4UWI4U1B0MXhwRWllaXJyRjlwMWlJNVlWYWENCjVSbFpTTmlBZDl5Y2FTQWF0Tkk3YTZsVkxtTms1UEdvTVZhSHJYcmpTYkNVNi9mVFMyL29PT1QwTVROWFpmcEhmQ25oQ1Q2VHBuSXQNCmJ3OHBoYkFOKzhGQlViOWNJREV4WlBITzB2MUtXSWNBWkdxbzZBVXAvRElrb2pDa0xKcEVlcFEya2x6SkhIQmFDU1NWM0cyemNSeXkNClZ0V1dBNHFTOUVpUzVOdERkcGRXNktXaWFJMUNnL3M3NEM0ZXJOZWxJTHN0K25wd0Qrd3Y0NVpBdXVrcWh6dnlIeGRpTXQ1c0ZRTUENClFUOE8xRDFKT05KRFkzN2ZIMk5hQ21SVFRlNEcxUWU1SFE0dGtzTWdBU0NCSnBodFhjcjIrZUxId3pWMXMwWFBMaWRoNEhGamJSQkoNCjJOQU42OWNWNXJlSXFXcVNlbUswM3NlSUkzNjFPS0M1dGlTUWFucWNLR2p5SytGZnZvY1BFeWxFam1LV2xaT05BZkFiOWR2YkFTeFgNCnN0U0ZPN0VVOXNDcldiYXZjWXFzWk9SNURwVENEU1FHeFZRUGZFbE5MV1lrOXR2YkFndTRPR3FBUXArN0NEdTIrQms0ZUxobHcvenENCjlMaWxLTURYa0svTEp0UUJLeXRHNkUxeUpLZ3JkMUZUOXJ3eHROdi8xb0hRS0taSms2bUt1NllxN0ZWL0ZxVS9ERldpcEZLaWdyaXINCmdGUFUwcDB4Vm9nS2E5c1ZheFYxSzRDcmhHUmdWM0VqOW5KQlhIRlhNQlRGWEVDcEhiQVZXOU1DdWJqOE85Y1ZiVWtTaW0yMktyWm0NCm9raTc3ajRhZGlOOFZSS3BEZHBIYXlIMDBsQTVOV3A1RHZUS3p6YjhFZDB6anRoWTJpcmFyOVpEMWo5UVBRN2JqclJzRHRRS1ExaTkNCis4dDVITWhSbzZIaW9xQUQzcjQ0c3VKZlBaZW9oZVdUMVpDaEtDdEdCR0xPT1NrdWlzZk1EbTN2T1ByMnZKV2FNc0trVnBpM0NRS2UNCjNXbDIwTWphaVNWa2pWcUw0VWNBTC9zbCtMSU1ZNU9KTHJmVzdwN3Vkb1lVTU1LSXo4aVZJazVEaVZwL0wxUCtUZ0piSllxRnJ0UzENCkdHeDVUSTMra3pMU1dUY2t1QlZGVmZzOE45amc0bUhDaFBMZDM1aGt2bnVXU2tSQUU4a2pEWW5vUlR3eVNaY2txdnJiVjIxWWlWaVoNCkpYVVJUcVRVZ252VHNPdUxPTWZUYktyZTFodG9HWTgyQkNndlRtU1VxR2txT2kveTRyR1NDMC9WTEs3dVpvRlgwN2lQOTVidWR3L0gNCnFwcmtKSGRsSXAzYWxQVGtmaXRHQUN3a0J1QlAyZ0s3aXVUakpva2t1cVdscE5QYnFKR2dqa2tXRm9nZmhidDNyMnhUamttMHcxT0sNCjJtVlI2c1VDY0tjd0dLYnFyQ3UyRUlCM1FPaFhJMDdUcHJob3VkMjA1RFBHUTlPQUk0T0I4WEZsYm5rbVNqcGQ1QXNSYUtKRExGSnkNCmpsYXV4K25iajlHTExvaXhvT2tpOEdvUjNEbVphelhWdktvNG1qK25WU3V5dHkvWk9KWUdhdnJNc01peHlnL3ZTd2pmaUFTd093MjINCnlDWWxMTHk5dGRJdVlKSkxOemJBbEpaMmpZcEg3c3gyR0xLVWtUcVZ0WjZnMFU5aVZlWXFHZHR1UEhzY1VjU0YxTFVyK0cyUzNhRlINCktpOEZtZGFQd08zRVArMWtlSnN4aFpMZDNtbTZUcGx1MEE0V054NjZrbWdZUFdvL0hKUktPQ3lWYlZVbTFmeXhNWW1TS1Mva2VlVloNCjVBR29tdzlKZkNneWJqUWlUS2t1MDJ5MUNYU3hGTW9vaU12ckJsQW9CdFZlK0xseWpTZCtWSlJjMmFYYzBLd1hHNlVVMHFGNzdZdUoNCk9hWWE3ZlJwWmxJeXF6ei9BQXhvMndOVHZpeXh5U0h5L3B0emF5MzFrMHlRUzZnZ0VWekdvNUx4TldVSHIweUpaek82M1Rsa1RVNW0NClhuTGFzbnBRVFNyUUY0L3RnQTljREtLcGQzMm5HT1cxdTVGdHBReW1XUUxXV1JGK3o2VGR1UDdXTEZUZzFwSXJlUzRsWmxOR1J4T1ANCmlNZmIvZ2h2aGl2RGFJdGtoMTJ6dHJ1MmNSU1cwa3pYQWoya1FJcEVhS3Y4cmZ6WkpnVHdzVjgwK3JCOVgrc0QwWjdoVWN3c2VVaEINCi9tN0xpMmpKWVJFT3BlYkxkWTBleEVrUUFUa1NBZXRPb1lqRnBuR3QyUzNkamVhYllNYldhM211Rm85M0N4Vm1IUGZhdllZcGhLMG8NCnY3R1cxUjcrQzVwY3pyKy9qb1docFRvT1BUSkJ0a2pOTzA2M2xzMDVQL3BLam1WVmo4TmZESzVNVlhUYlVXRnJxYVIyclhvbU1ZZTENCkxWTEoxMnlUVmsrdGpWbmJ4Vyt0WFpqc21zVWtvUkE1M0ZmSEV1djFmTkxMZ2c2L2MrSUNnSDZjbkZ3SksxV3F0U05xOWZuazdZTGsNCkxDVUFENGZIRzBoZVFTMVYyQjJJd3MxNktQVDRueE5NZ2RqYjAranhqUG94Zy95azVaSllmNitIdy9SL254bkp0MEhCVkd3cnRpT2INCkhXd0U5UGl4WXR4NGs4Zi9BQXlVZUhpeWY2YmlsL1VXTkNLZ0ExTGRDZmJEeE9ETHNQSU1rSWNVUDN2Rnd5OVgrUytyK0hpL3EvemwNCm9nWUExSUlCK2VQRWc5anpqQ1V6S0hEamx3UytyeS9vL3dDNmJlT3JsVUsvQ0J5WHZpSk4rZnNrSExPTUNJK0Z3K2oxNUpjTTR3OWMNCmZSL1Qvd0ExM29rbXFrRURidWQ4ZUpyajJGa2tUVW95akdYaDhVWTVKL3ZQOHlIMHgvaW45S3dSa3lsZStHOXJjQ0dnbWRSNEIybHgNCmNNdjk5TC9TK3BkUEdIUUVVQlEwTktIYnRrWTgzZGRyWW81Y0F5UUVmM0I4SDBUaGsvY2Y1Q2N2RGxKWVlBRSsyQnR5cjdmZGg0bkINCkhZc3VIaThUSFhBTTM4ZjkxSThQSDlEZnBNdGFzQUswNUh1U0s0OFRISjJSTEdaY2M0UWpHVWNmSDZ2WE9jUmsvbS96SmVwd2dQSS8NCkVPdkdudjhBZGp4S094Y3QwWlFpZkU4RCtQOEF2T0hqai9COU00L1QvV2FTQWtiTUJVa2Qrb3hNbVdsN0dubEZpZU1ldVdMK1BpOFMNCkgrWi9OOVRYcDhvbFBJY2lhYjE5c2IzWlIwRUphZkdSS1BpWmNrbytyaS9vUjRmcDRmUjlVdjYzcDRtMWlVWEtvMVBIL091Sk96WnANCmV6aGkxOE1XVWlRK3I3RHd4OVg5Sllzc3B1YW5iZW45bUdoVFJEWFovd0E1eEVuaThUaDRQNlBGdytIdy93QTMrRmZMR3FSU0JDQVANClUzQXI0ZE1ZbXlIWmRwYWJIaXdaZkRNUVB6QTlQcS9tSDl6OVA4NzEvd0F6K2x4K2xDTlE3ZUgzWk12S05keFhmSXEvLzllQ2lQZkoNCk1sMUFNVmFPNDJ4VmRRMHIyR0t0VU9LdTQxeFYxS1lxNmh4VmFFQU5jVmJZRWpiQVZjb0lPK0JWckNoM3hWY1ZBR0tyUUcvbTI3QVkNCnE0Zysvd0JPS3JoMHIzSFRGVmhERHFldmhpclZDSEo2YmRlK0t0VG9XaE5XMkZUOTR4VmJIVllJankrUDRlUDBaRnR4ODA1MHBYMW8NCnpBY3JKSUY1eFR5aXRaUDk5b1J1dlBzV3dGMjJJMExSRnZKTFBLc2RwRVBXWThKcEgybzdBcDhSOGFpbVJid2VKTDdtZVdlM2tqU08NClV2YUV4dnlPNU9LZUNsa2w3Y1JXa2NZcXNaQ2dJZG1yM0FQZkZxbEZNN0RURjFPOVZidzhvWWxxaWcwNUh3SXhib1pLUmQvTGE2YmUNClcxalpJdnFYRlVkUHNrQmxOZml3bytvcE5xVnU3M3NVc3NLZWhIOE1kdHVTVlUwSy9Oc0RiRTBqclRUNzFYSmtrV0pKQnpTTW1pclQNCjdLcVQrMWtTbU10MXRyb2xycXYxdTVhVlhqdFZIcUs3R05VSkgyengzZmZ3d0xPVzZoTk5NOW90dkxLdHJLS1JNWEpVOGdQaG92VS8NCkNjSEN3NGxEVHZMV253YWxaM1A2UU55YmN0NjBZUXFBVDBKUHNjbEVLU21FZGhZL3BQVUxtM2tBaWFOSVNVYllTOHFscWZ0ZkQ4T1MNClFwMk9sU1Q2dEU4N0xQYjJ0SmZUYzhSVjI0VnAvcWl1UVJraTdWSjVSZXhXa1lJdHhMeFp4L0x5TkEyR215STJUalY3SFRVMGU0Q1INCnJHZ2pZOGtGR0ZDS2tlK1BDd2p6V1cxOW9UMlNwQW9lcUVVTkNXTk1rRVN2aVN5R3p0MjB1UzZJYU9XOTNFWXJ1RDlsYWUyTE9PUkINCjJsdHFXbHJlMnNoS1gxK0VGamNINHlwMm9LOXNXVXBXbWQ3bytxenpTU1hyckxhM1Z1TGE1aDVsVkRydVpCWDRhMXhZam1nTENjTHENClM2ZHB0dU9VY1BLWXFRRmIwOXVTMDJyaXptbzY1QnFlcHdUUVd5dkdhRGtEeFVVOWhpbllCbHVuYURhRFNvVXV3a3pNaW1WcE81MjcNCjlzQmNNek5zYjFwcldheFMyUVd6MjhDR0tFc1Q2c2JnbjRkdnRaRnlOT0xLRjBxZTNrMGVTMW5TbkgrNytMNHl4WWZFb08vd3FDdUwNCmRPQTRpbDgxMWMyVCtqYXlGVFVGWDdqZm8yTEx3OWwydHByc3M5cDlaaWpra0pvaFEwb3hGVjM4TVdvamhSMnN3eERnNlNOYjNVU0sNCjRlTnZoU1VBY2lweUpaaklUQXBocVdpWDB5VzJwUzNqelJhZkF0ekpjbis1bWMvN3FpVmVoL3lzc2k0Y0pFRkx0Y3Y3U3plQjc2d1cNCkM4WUpOSEpRT09EOW15TGVCVGEybGpxVjRrdDdjUlNpTUJvN2NMV3Fub29BeFpTeVV1dXRFTTkwaHNaUHFraW5abzZBOGZrTVdQakkNCmZVOUF0clNTRFU3NHRkdXJVWnBQaUk0OU5zbUV5bHhCcWJXTE9lMVo3Y0t4WGlhS3U5VjM3WVdzUjZLaWE5cE9xWGx0TmYyRWFTc3cNClNXNFhtZ2JhZ0RLTm13SjhGRXplWWRUZXdsanM0V0YzWXpxWlJBbk9Nd0E5cWRPUzlzTFhPRkZqMHQxQmZlWm5Odkk5cGIzQ2xwUVANCmdZTW9yU2g2WUVnMGl0WGdsV3psdUlyK1pab29pb2srelVMdUs1TmdaVUdQZVZMeTd1elBMZFNHV1VjQnpZMU8rK1FrNnJMTWswMDYNCjExcTdKM1VjYWpMc2JqeVZkbG9TU0JranpZTktnOVFmTEFxcTBmeEFqcXYwNHF1Wm5JNmIvS21OT1orYmtjWWh0VURjZjUzcStyMUwNCmtOS0RZQWRQSDZNSEM1T0x0VExqakVSNFI0ZjArbi9UL3dDbi9pYVZ6VGFncDBxTWVGUjJ4bEFFUU1mREhpOVBBUDhBS2ZXczlhUWcNCnFTRFhDSWhaOXM2aVFJSmo2Ny9oajlNdlZ3ZjFGeGRnNVlIY2RjbHdpcWEvNVV6K040d0lHU3VIMHhIOVg4ZjVyUUpwVHFEdlFpdVINCkkzYXNPdXlRZ1llbWNETGo0Y2tlUDEvemxxczRxd0FIc2NCQ05Qclo0cEdVZUc1K21YcGovRi9OL21mNXJsWWdIcDhYVUhFaGxwdGQNClBER1VZaU5aUHI0bzhYRTBabTZFQUwwUFRIaGNqK1Y4dFZVSzRQQytqL0pmekdqT2FuZW85d08yUENzKzJNMGlUTGdsZFM0ZUFjUEgNCkQ2Wi8xdUgwZjFYR2QxYW14M3F4QS9Wandoamo3WHp4Sk5pUmxQeGZWR012M244NkxoY1B5SVVBTFdvSkcrUEN5eGRzWm9jaEQ2amsNCitnZjNrdjRscG1sV3REVVZxS0RIaGE4WGFtV0VSR1BEVVorSkgweDlQL0hYU09TZW5RQUtSdFNtRUJwMUdzeVpaaVJOR0FFWWNQbzQNCkl3K25oYU1zZ0pOQURTdktnclhCd3QvOHFaZUxpOUhpZjZyd1I4VCt0L1gvQUtmMUxESTdRbEtBNzFOY2FhVHJabkVjUnJoTXVQbDYNCitQOEFuOFN3a2hpUVFPMjJGdzJxTDRxZmZGWC8wSVRUcjc1Sms0SlhyaXEwRHd4VmQ0QWIrSXhWd05UMHhWMWQ2WXEweTE4RGlyWUINCkFBcDBOY1ZiS3IwSFhCYXRBYm5FbFZwb1dyU3VCVytQTFlEZkRTdTQrT05LMVFWRkJnVnhvS2luM1lxME9tS3RzVHNvcGlxeVJTQ0cNCjdkQ01WWHFxT0NyVitYMFlxcEtnRnF0ZGlwcHY4OGlXekVkMDlHcnFsdTkxWnNIaGVNcHdrYW9TUURZQlZGVHh3TzV3aHF5MVNmOEENClIwTGxvZzAzcVNPUUNwOU5IclExN2l0Y2dTNUp4N1dFUnFkczBHb3hQRTdUTmMxSVdKYVJMRFNxbGo0ajlyRUcyR09XMjZqZFBKR2sNCnJHM0p0Z2pCWHB5Mllia1ZIdy81T0ZKQ1ZXNjMwc2tVRFNQR1NSU1kvQ3dCK1dGbGtpRit1NmMzcVJTck83WFd3ajJQT3FucXZqZ1QNCkNRNUlNVDY3YTZsSEpxS2lTNFZoUldweVlOMEpHS1pCSHg2OFo1YnlPV2tNS0FjWGRlWU5EOFczN0oveXNCRFhIWXEzbFNTM0szRXQNCnRjRkxtWldWeTRIRXF1NmpnZmg3WTBraXluQzN2bCthVVcwc0VGek8vbzhKcEtobWtiL2VoK2ZZTHR3VURHd2d3S2xUUm9CcVVzQmENCmEwV1FCblpDZUsxSTVLYWpueG9PWEwrWmNiUURScGpyNjlGelczdDRaSmZXa1BEaFF0U2xkOWg5bnZqYk1pa3cwaUxVR3ZycVIwV08NCko0WWdzY3hJNUl6R2hya1dVemFaUnBjUWFxR3VKbzRrbVZna0NqbnoyNlZQVHh3ZzB5NHRrajFQekpxamY2TkJiSTRlc2FqcnlIMmUNCm1TNGd4NEszWTNQWmFocGthM043REpIR3pHZ1E4UnYyMndjVGJFeExJL0p1dFhFaXpSM0c4VUhINnFHRlNuSWZFSzQyMDVCU2E2bGQNClJTeHNHZWxQaURBL1pJNzFPTnNJQkJhZmM2aklndldsTnphT0hRd09vY3NQRlF3T050bklxUGxhRllyK1RVaHlnaVpqYW1LU2dvVDgNClJPMUIweHRNamFmeVhNYzBrd3M0emNTRlJWMUk0QnE4YXM1SXBoYVlpUTVwYk5yWG1TeXRqYlhkc0NYL0FIVWNrRGg2TWRnRGlRengNCmdFb2V6aHRMclIxdEx4VmcxUzNMcXJ5YklDNXFUSnhCMy9sT0NtWXhrSFlxNXRyWWFWQmRXMXRGZWZBNGE0TEdKMUtIajhJUFhmSXMNCmVNOFJDQ2gwTm5nTWswTXFUdUJWbWxSUjErOXZ1eGJTVFRJN2pSYnRvcmN2TWhlTUF1cFdvSS8xdjdNWEhHWVhSWWJkVzk3ZFhjMWgNCkVWcVN3RGxnS2p1TjhCRG1nQ01iVEhTYkR6bnBwUzFnZU1XcEhJckpJcm90UDhtcmRQbGtnWEh5RVZkTE5YMDNXN2k4aHU3OHh6MnMNCnhDRzVoUEplSkh3MDIyeHBqNG9LdmQyZHRiNlhHMXF2RzR0RHNnQUxTTDRFNGtLQmFEMHpWdFkrdVRYWDFTc0t4QWxVSVlxYWtiOTgNCkNlQU9uOHdEVjJhMWFBdklsS0FWVXFjbGFJd0lOcmJYeW9JRmE0YVJvWFljbklwU251T21Oc3BIZFIwcTRzNzdTcnF6ZGdKcmFSbmoNCmJvU1ZOVkkrZUZiS20vbUQ5SCtuQ2xZWVd1RW11aEVTR0xyK3lUL0szaGhwRldpdFlpczlWTnRxRWtVRVdvTkt6UlJqbFQwU0tVdUMNCnRkeWNhYWNrYVJCMGZTYjE0QXBqTXR2RC9wUWhMRkRKTHZUYzl2bGh0aENQZWxvMGkyMCsrbkVLaFZmaVRRZHgweU1pNGVxaUFOa2cNCkladFV2dHdQaVhjNWRqZGZKVkkvWjdkc2tlYkJ5VjVCaUtiVTN3SlJLaGFWQTZuYzVBeWVrMEdoeFpNVUpTaHhjVS9EeVM0NVI0SWYNCjZwL05hS25sdHVNa0M2UFVZNHd5U0VEeHdqSThNLzZLeFJUcDlvZU80eHRxaWVwRmh6cW9ieEhoakUyNXZhZW5oaXpjTVBwNFlTLzANCjhSSnBvNlZJQXI3Wk9MaVo4RXNjdUUxZjlFOFgxTktxa0VOWGtkc0xISGlsT1FqRVhJL3d0aU1BOGRpdjdYWElFdVRwOFBEbmpESUwNCjlRaEtOL3p2NlVHNUZRQTBwMW9OOGlDN0R0SFJ3eGlmREdNZURMNGNUREo0ay84QUtmWGo0cDhQMGZ4Y0V2OEFlb2dlQW9jazZUaE4NCjExY3lraXZjZTJLS2FLanFSMXhTWWtOc1U2SHIzT0xGZEdrWlVsUnpZZnMxcHRrU1hkZG42T0dUREtZajQyV012N25pNFAzUDgrUEQNCjZwdTJDa2dWQk5CWENDNFdmVGpoT1dGakY0bmh3NC9yL25MVlZDVld2d254NjRTeDBtSUhMQVRGeG5JRCtiOVRUcVBWWmFWQVBUeHgNCkM2dkJ3WnB3ajlNSnlqL214a3Bpb3FLRURzTWxUaWdFdGNkeFh2Z0lRMXYycFQ1WUZmL1JoSjJOTWt5YjNxTVZjQnVhN0RGVytQMEgNCkZYQUd2VEZYY1J5T0t0RVV4VnJmdWFERld3dGFOOXd5S3RiOGp0VEZYY1JpcnFBYnFOKytTQ3VxQ0IxM3hLdENuTGF1MlJWZFFHcHgNClZaUW5mRld5Ty9jWXF0WmpUNGh0aXE5VkJvd05EM0hmRlZpMjh0d1BSV2l1WG9PUnBrUzI0eGFiK1hORnQ5TG51cmk3ZU5wM29JNHoNClFBQTlUUTl6Z2RuaXhTVzZqRExMR0toRWpqaVoxV25aMnAyQUZhNVhKemNjdWhVdkx4dmJ5TzdhWjMvUjdFeFJ3MTJMZ0F0djlxaHgNCmcyWlFBZGtmcVJTWFI1WWZUTGhWMlFIaWFwK3pYSk5hUzZMSUwvVUVFcnRFbGlpL0FHK05tOGQraTRzNXB2cWlTWEVzTnc5MlZNTWkNCktva0JWVkJiWm1jZGdjWEZOb3J6cjVZbTBwb1pMcU5JWG5LbTBlTDk0c3BmZHBQV1dxSDJ3cHc1dDZLV3gyc1Z2d2doWU1HK0thUngNClVBZStCeXBqcTdXZERUNmg5WnRDMFpIMm4yMnAzb0FNUzBpVzZTUVF4UEdGa0xTdTRwNm8yNHZVZEIyeURLVWsybmhTQzNtYU4zTGUNCmtVOUZ5ZUlCQXFhRHJXbUxVRHVzMHl3dDdPT0MrampsK3RQKzg1b0FWQk5RYWV4eGI1Y2xiV2JuVjJpRFEzaW9MZGtqOVBnSzhRdEENCkNTVHNNVTRoeEtxM0NRYWE5Njh2TzZTTWlPb0hFc1NLMDJ4WUU3MHcrMzF1NnRkVmllN2lEU0J1VEVkQU91dzhjVytYSm5kemVXV3INCjJmb21QMW1tWGFPblVERnhkd2JRVUdnRFRvUzNFcS9Vc04xeGNtTWhJSlhhUnpYRTkzWjNLdlBHbzV4T29vcmJiZzRxWTBqbzc2Rk4NCklEd084SzA5TThGcXlVTkQ4c1dCVjdHMHU0cktEZ1V1RVNReW5pUTVabUZQaTk2WW9TeTN1cjY5a3VkSDA2RlpIVU9aT1pFZkNyN04NCnkyNlpOc2tRbWxxZGEwMjlsazFtMUE5Vmc2eVI3b0R4QzcwTk8yMkxTVDNJcG0waHRSaGVYZ0ROeVdHU1UwajlWaDhCay9tV3VLTjENCkhWOUZTd3RKM051czBWcVZNTFN0d0VzamtHWDArUHc4VS8xY2lXVUNMU084ODlXa1ZZM2oycUtvSXhVSDU0R1JpU1UxaDg0MkVzUUoNCm1LTHR1MndIdGkxeTA1dTBMQnBZdDlXdGJ1NGs5VjdsM0loSTJVRVZCT0xaSUhnVjU0N2FYV0pCYnUzcXV2cDFWdU1hL3dBd1A5bUwNCklrREh1cTJ0NURadmIyalhjTnhiUEg5V3Q0cmRtS01TMU9VZ0lIQjQ2ZE1tNDVqU01HZ3dTMjRpa3VwT1pCOVJsK0J1cDIzR0FyNGwNCkpJM2wyU0hXbWp0WldXM1NNT0dZMUpZbllFOStoeUxiQTJsMzF5MHN2TUp1Q0JXTlNuV25JdDArbkZ0S2R0cUExaGtpdEdBdFkvaXYNCmE5ZVA4ZytlRU5Na3Z1L0xsbitrWXJyVFl3a2lzSkh0K1JDbFY2QSsrU1k4U25xdXZlVzdZTytxVzl4RmR2ZExjTlZGYWhIN0cxQngNCjJ5UWE1NUtWdkxtdTIrcnlYUXRMWDBiZFpESWVlMzJ2c2o2TUxFVDQwZmRRUldsdzF4YlVFMG9wT29OTmdOdHVtUWJJOGtrVFVIdkwNCnVibkdZM1Npa1ZyZ0xnYXBJT05kUXZTVFNyS0JUeHkvRzYrU3JJRkpXb1ArVWNrZWJCdUlnTlJtSjk4QlNGVmlPRzVPeHJrT3J0Y20NCmJHZExIR0Q2NHpNK1g4LzAvVXZqTkZHL3p3RU94N083U3dZY2NZa21QMStMSGhsUGpsUDB4bDlYRDlQOUQvaWxwY1VCQjJwUTQwMFQNCjE4RGpqR001WTR4eCtEUEh3Y1dPZisyL1YvSDlYOCtIOEx1YXNCUW5iOFRqRU9UcmUxTUdYSHdSbGt4OElIMEQrOTlJaCs4anhmMEkNCjhQOEFSL3BPWjZFbXBBSW9OdStTSW9KUGEyTGpuS001eDhUSENIMC81U0grVStyOGNiWEt2SjFxYWtEYkl0OGRTSnl6WjhJbmw4UXcNCmp3WS8zZWFIOVBpanhTNGZUd2VscGdCTnlyc2FWR1NISjF1dk1ZYTRUa2VFRXd6VGovRmovaThLWDlOcW9FdGV6QWtId3JqV3pYRFcNCjRvYXVlVUU4T1R4T0dmRDY4TXN2OFhEL0FMVzM2c1pBSXFXSHdnK05jSENYTmgybmdBQUpsTExDQmpIVVNCL2psOVBEeDhmREdIb2kNCjJYSEo5K0pPM3ZVWThMT2ZhdUx4TWtvemxIeFlSajlKL3ZZLzVUNnY2UDhBVzlTMldSV1FnRW10S0Q1WWlOTmZhUGFtTE5qbkVUa2UNCk13bENQRDlQaHg5Zi9LeVhxVWVZRzNBRmozNjVLblRRMWNZdzRSanhrMTljK0tjLzYzMWVIL1Y5QzVlSW94cXJnOVI0WWxzMGs4TVkNCmdtVXNXYUUrTGppT0w5M3Q2ZnErcVBxL3JjUzh6QXJYZGZqcng4UjRaSGhkcmw3Wng1SUcrTWZ2L0Y4TCtESmk5UDd1ZS84QUZMankNCmZ4ZXVUakpIWHFXK0t0VFRiR21VdTBzTWllS2M1OFdXT2FQRkgrNGpEMWNNZlYvRi9kK24wTzlSZmlBQkJMY3E3L3dJeDRTeWgycmgNCmp4MGVFenplTngxUDFSbC9CTHdzbUtYby9yY0VscTNTRFp1UU5TU0tiVU9Fd0xacCsyc1lBNHBHUHJ5U2xDTVBSd1pJOE1ZL1ZMK1ANCjk1d29abVViQTFyMjc1WVhrVHphcVR2eEl5S0gvOUtHVUdTWk9vTVZkUUhZZGV0TVZkVTFCUFE5c1ZicWNWYXhWeEFwaXJWQWFCdGgNCjQ0cTMrejhPOVRRbjJ4cFd1RkRVNDByampTcmR3ZGg5T0t0bnFBcmZMRXE0QW1wK2p3eUt0RHYvQUZyaXE0YmlnMm9NVmFVR2xUa3ENClZhMzJRZmFwd0ZYTHVLK0kvREFxMlNSbzRtbEE1ZW0xZUo3aklFdDJJN3RYT3AyMnNYVUVWbkhKRXloUFZNbENGS240dUpIMmxiOW4NCkFIY1lzaHJtbjJvWEZwYjJyV1VnZXFJakFqazZsUzNVK0gwWUpCdURkbEE4VnJMQmJlbkJid09KSldjMWNCd0NXNEg0OWgzd1JaU0sNCnhySFRIdTFqaXVUZFRUaHZWa1ZpRUMvNUNucWNMSUJxZXdoMDFhUnFzVVkzTXhIeFBRZHljamJXQ1NnYnk5amdzcEZsdUduVzRVTkcNCkJUZ0NOKzJOdGtZQlBQTE92NnBvaFRTNzZPUFZkQnVFVnpZU2xxUTg5MmFFaXZFL3pMOW5HMmpMaEEzSE5NOVQwRFJKOUx1OVU4dVQNCk5MSEMzcTNWZ3hYMVlnTzQvd0Fud3J4Vi93Q2Rza0dxR1NRMkpTRzNhQ2VkUHJVemlCRVJuajJRYzVSVkVvZmo1TCszWEZ2dUpHMzENCklpVTZlMTE5U2dvYm5pU1RIMVgzeHBtTWREZFMxdXpqdGRPQ1JYWEs3SzFlQ2FNOG5CNjhXWDRmdnhwcmlBZ2JHOTBzNlhHMHJQRTANCmE4SlRHL0lCcTkxN1kwejNRMEZ0ZVh0MlJGSWwxWnNRU3hBRGdmTVkwd25sNGZwMlRpQ0N4V3crclR3endORXpMSElVV1dNZzkrTmUNClhYR21Ba1R1bGtlbDZLZFNtTTAwVThpaXNNZTZNQ1JSaTZucDB4cHRqTTlWWjlKVkN0eWx3OFVBVm96RkVwZXErSVpkMXhwczQ0MVINClN5ejFIV2RSMUtYVFlENmtTUjhqelBHaXIwYi9BR1dOSmpLSU93UmVsVzEvQnF3dHAvVWgrRm1EMHJ2MkErbkdtVXBwZGM2RGV3NjYNCjBVc3NzZW5YTXRQV1k4QXlzZjNnK2pJbGlCWVRPeXNORWppOUd3RTBUcEpMems5UjJvNnIrNkIyNC9IN1lFUWllcUUwVHl2cTEzTk4NCmV0T3RuRXl0REpESXBNa205ZVEzK0hmSnNNdmtqOVRPb1cwczltWm1sZ3ZJNDFpSkk0cncyWVU3Tml1TUlHeGtqc0xaYkxVeDZ5c3gNCkVjN2psc1RVRDJJOGNXVXJSOXpwdDlkaU5iRw0KDQotLS0tLS0tLS0tLS0tLThCNTMzQTgyOTIyNDA3RDdDM0QzNUE5OS0tDQoNCg0KLS0tLS0tLS0tLS0tLS00Q0VCNUU0NDhEQzA3N0YzNTA1MEM0QkUtLQ0KDQo=", + "mimeType": "message/rfc822", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "Return-Path: \nReceived: from virtual.mrf.mail.rcn.net ([207.172.4.103])\r\n by mta05.mrf.mail.rcn.net\r\n (InterMail vM.4.01.02.27 201-229-119-110) with ESMTP\r\n id <20000524184032.XKFS1688.mta05.mrf.mail.rcn.net@virtual.mrf.mail.rcn.net>\r\n for ; Wed, 24 May 2000 14:40:32 -0400\nReceived: from mail.desktop.com ([166.90.128.242])\r\n\tby virtual.mrf.mail.rcn.net with esmtp (Exim 2.12 #3)\r\n\tid 12ug50-00059f-00\r\n\tfor eryq@zeegee.com; Wed, 24 May 2000 14:40:30 -0400\nReceived: from mailandnews.com (jumpgate.desktop.com [166.90.128.243])\r\n\tby mail.desktop.com (8.9.2/8.9.2) with ESMTP id LAA31102\r\n\tfor ; Wed, 24 May 2000 11:40:29 -0700 (PDT)\r\n\t(envelope-from omrec@mailandnews.com)\nMessage-ID: <392C2385.4C402C55@mailandnews.com>\nDate: Wed, 24 May 2000 11:46:29 -0700\nFrom: Sven \nReply-To: omrec@mailandnews.com\nX-Mailer: Mozilla 4.7 [en] (WinNT; I)\nX-Accept-Language: en\nMIME-Version: 1.0\nTo: Eryq \nSubject: [Fwd: [Fwd: [Fwd: FW: Another Priceless Moment]]]\nContent-Type: multipart/mixed;\r\n boundary=\"------------ABE49921AF9E83E8F9A7667E\"\nX-Mozilla-Status: 8001", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/frag.msg b/packages/node-mimimi/test/mimetools-testmsgs/frag.msg new file mode 100644 index 00000000000..bdf696c6591 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/frag.msg @@ -0,0 +1,1229 @@ +Return-Path: +Received: from virtual.mrf.mail.rcn.net ([207.172.4.103]) + by mta05.mrf.mail.rcn.net + (InterMail vM.4.01.02.27 201-229-119-110) with ESMTP + id <20000524184032.XKFS1688.mta05.mrf.mail.rcn.net@virtual.mrf.mail.rcn.net> + for ; Wed, 24 May 2000 14:40:32 -0400 +Received: from mail.desktop.com ([166.90.128.242]) + by virtual.mrf.mail.rcn.net with esmtp (Exim 2.12 #3) + id 12ug50-00059f-00 + for eryq@zeegee.com; Wed, 24 May 2000 14:40:30 -0400 +Received: from mailandnews.com (jumpgate.desktop.com [166.90.128.243]) + by mail.desktop.com (8.9.2/8.9.2) with ESMTP id LAA31102 + for ; Wed, 24 May 2000 11:40:29 -0700 (PDT) + (envelope-from omrec@mailandnews.com) +Message-ID: <392C2385.4C402C55@mailandnews.com> +Date: Wed, 24 May 2000 11:46:29 -0700 +From: Sven +Reply-To: omrec@mailandnews.com +X-Mailer: Mozilla 4.7 [en] (WinNT; I) +X-Accept-Language: en +MIME-Version: 1.0 +To: Eryq +Subject: [Fwd: [Fwd: [Fwd: FW: Another Priceless Moment]]] +Content-Type: multipart/mixed; + boundary="------------ABE49921AF9E83E8F9A7667E" +X-Mozilla-Status: 8001 + +This is a multi-part message in MIME format. +--------------ABE49921AF9E83E8F9A7667E +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + + + +--------------ABE49921AF9E83E8F9A7667E +Content-Type: message/rfc822 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +Received: from mail.vynce.org [63.198.43.13] (vynce@vynce.org); Tue, 23 May 2000 22:00:16 -0400 +X-Envelope-To: omrec +Received: from vynce.org (166.90.128.243) by mail.vynce.org + with ESMTP (Eudora Internet Mail Server 1.3.1); Tue, 23 May 2000 19:05:52 -0700 +Message-ID: <392B389A.1968998B@vynce.org> +Date: Tue, 23 May 2000 19:04:10 -0700 +From: Vynce +Organization: Desktop.com +X-Mailer: Mozilla 4.61 [en] (Win98; U) +X-Accept-Language: en +MIME-Version: 1.0 +To: omrec@mailandnews.com +Subject: [Fwd: [Fwd: FW: Another Priceless Moment]] +Content-Type: multipart/mixed; + boundary="------------4CEB5E448DC077F35050C4BE" +X-Mozilla-Status2: 00000000 + +This is a multi-part message in MIME format. +--------------4CEB5E448DC077F35050C4BE +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +just to add to your personal hell. + + +--------------4CEB5E448DC077F35050C4BE +Content-Type: message/rfc822 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +Return-Path: +Received: from iglou.com (192.107.41.3) by mail.vynce.org + with SMTP (Eudora Internet Mail Server 1.3.1); Thu, 18 May 2000 16:10:02 -0700 +Received: from [204.255.234.19] (helo=ntserver2.snesystems.com) + by iglou.com with esmtp (8.9.3/8.9.3) + id 12sZKw-0007JK-00; Thu, 18 May 2000 19:04:15 -0400 +Received: from snesystems.com (sne-30.snesystems.com [204.255.234.30]) by ntserver2.snesystems.com with SMTP (Microsoft Exchange Internet Mail Service Version 5.5.2650.21) + id LGJH8AYQ; Thu, 18 May 2000 19:03:40 -0400 +Sender: root@mail.vynce.org +Message-ID: <39247724.AF25EF83@snesystems.com> +Date: Thu, 18 May 2000 19:05:08 -0400 +From: root +Reply-To: jasonc@snesystems.com +Organization: SNE Systems, Inc. +X-Mailer: Mozilla 4.72 [en] (X11; I; Linux 2.2.12-20 i686) +X-Accept-Language: ja, en +MIME-Version: 1.0 +To: vynce@vynce.org +Subject: [Fwd: FW: Another Priceless Moment] +Content-Type: multipart/mixed; + boundary="------------8B533A82922407D7C3D35A99" +X-Mozilla-Status2: 00000000 + +This is a multi-part message in MIME format. +--------------8B533A82922407D7C3D35A99 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + + + +--------------8B533A82922407D7C3D35A99 +Content-Type: message/rfc822 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +Received: by ntserver2 + id <01BFC0CA.C31F7A10@ntserver2>; Thu, 18 May 2000 09:12:47 -0400 +Message-ID: <01D476341BDBD211B7C500A0CC209BA03DF5C6@ntserver2> +From: Shawn Morgan +To: Wayne Price , Tim Spayner , + Gary Jones , Jason Chelliah +Subject: FW: Another Priceless Moment +Date: Thu, 18 May 2000 09:12:47 -0400 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----_=_NextPart_000_01BFC0CA.C32A4450" + +This message is in MIME format. Since your mail reader does not understand +this format, some or all of this message may not be legible. + +------_=_NextPart_000_01BFC0CA.C32A4450 +Content-Type: text/plain; + charset="iso-8859-1" + + + +-----Original Message----- +From: Shawn Morgan [mailto:cephalos@home.com] +Sent: Wednesday, May 17, 2000 8:18 PM +To: Shawn Morgan +Subject: Fw: Another Priceless Moment + + + +----- Original Message ----- +From: Michele Morgan +To: +Sent: Tuesday, May 16, 2000 10:31 PM +Subject: Fw: Another Priceless Moment + + +> +> + + +------_=_NextPart_000_01BFC0CA.C32A4450 +Content-Type: image/jpeg; + name="aprilfools.jpg" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="aprilfools.jpg" + +/9j/4AAQSkZJRgABAgEASABIAAD/7Q4uUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA +AQBIAAAAAQABOEJJTQQNAAAAAAAEAAAAeDhCSU0D8wAAAAAACAAAAAAAAAAAOEJJTQQKAAAAAAAB +AAA4QklNJxAAAAAAAAoAAQAAAAAAAAACOEJJTQP1AAAAAABIAC9mZgABAGxmZgAGAAAAAAABAC9m +ZgABAKGZmgAGAAAAAAABADIAAAABAFoAAAAGAAAAAAABADUAAAABAC0AAAAGAAAAAAABOEJJTQP4 +AAAAAABwAAD/////////////////////////////A+gAAAAA//////////////////////////// +/wPoAAAAAP////////////////////////////8D6AAAAAD///////////////////////////// +A+gAADhCSU0EAAAAAAAAAgACOEJJTQQCAAAAAAAGAAAAAAAAOEJJTQQIAAAAAAAQAAAAAQAAAkAA +AAJAAAAAADhCSU0EFAAAAAAABAAAAAQ4QklNBAwAAAAADH4AAAABAAAAcAAAAFQAAAFQAABuQAAA +DGIAGAAB/9j/4AAQSkZJRgABAgEASABIAAD/7gAOQWRvYmUAZIAAAAAB/9sAhAAMCAgICQgMCQkM +EQsKCxEVDwwMDxUYExMVExMYEQwMDAwMDBEMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAQ0L +Cw0ODRAODhAUDg4OFBQODg4OFBEMDAwMDBERDAwMDAwMEQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM +DAwMDAz/wAARCABUAHADASIAAhEBAxEB/90ABAAH/8QBPwAAAQUBAQEBAQEAAAAAAAAAAwABAgQF +BgcICQoLAQABBQEBAQEBAQAAAAAAAAABAAIDBAUGBwgJCgsQAAEEAQMCBAIFBwYIBQMMMwEAAhED +BCESMQVBUWETInGBMgYUkaGxQiMkFVLBYjM0coLRQwclklPw4fFjczUWorKDJkSTVGRFwqN0NhfS +VeJl8rOEw9N14/NGJ5SkhbSVxNTk9KW1xdXl9VZmdoaWprbG1ub2N0dXZ3eHl6e3x9fn9xEAAgIB +AgQEAwQFBgcHBgU1AQACEQMhMRIEQVFhcSITBTKBkRShsUIjwVLR8DMkYuFygpJDUxVjczTxJQYW +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 +AAEBAP/iDFhJQ0NfUFJPRklMRQABAQAADEhMaW5vAhAAAG1udHJSR0IgWFlaIAfOAAIACQAGADEA +AGFjc3BNU0ZUAAAAAElFQyBzUkdCAAAAAAAAAAAAAAAAAAD21gABAAAAANMtSFAgIAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEWNwcnQAAAFQAAAAM2Rlc2MA +AAGEAAAAbHd0cHQAAAHwAAAAFGJrcHQAAAIEAAAAFHJYWVoAAAIYAAAAFGdYWVoAAAIsAAAAFGJY +WVoAAAJAAAAAFGRtbmQAAAJUAAAAcGRtZGQAAALEAAAAiHZ1ZWQAAANMAAAAhnZpZXcAAAPUAAAA +JGx1bWkAAAP4AAAAFG1lYXMAAAQMAAAAJHRlY2gAAAQwAAAADHJUUkMAAAQ8AAAIDGdUUkMAAAQ8 +AAAIDGJUUkMAAAQ8AAAIDHRleHQAAAAAQ29weXJpZ2h0IChjKSAxOTk4IEhld2xldHQtUGFja2Fy +ZCBDb21wYW55AABkZXNjAAAAAAAAABJzUkdCIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAAEnNSR0Ig +SUVDNjE5NjYtMi4xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAABYWVogAAAAAAAA81EAAQAAAAEWzFhZWiAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAG+i +AAA49QAAA5BYWVogAAAAAAAAYpkAALeFAAAY2lhZWiAAAAAAAAAkoAAAD4QAALbPZGVzYwAAAAAA +AAAWSUVDIGh0dHA6Ly93d3cuaWVjLmNoAAAAAAAAAAAAAAAWSUVDIGh0dHA6Ly93d3cuaWVjLmNo +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGRlc2MAAAAAAAAA +LklFQyA2MTk2Ni0yLjEgRGVmYXVsdCBSR0IgY29sb3VyIHNwYWNlIC0gc1JHQgAAAAAAAAAAAAAA +LklFQyA2MTk2Ni0yLjEgRGVmYXVsdCBSR0IgY29sb3VyIHNwYWNlIC0gc1JHQgAAAAAAAAAAAAAA +AAAAAAAAAAAAAABkZXNjAAAAAAAAACxSZWZlcmVuY2UgVmlld2luZyBDb25kaXRpb24gaW4gSUVD +NjE5NjYtMi4xAAAAAAAAAAAAAAAsUmVmZXJlbmNlIFZpZXdpbmcgQ29uZGl0aW9uIGluIElFQzYx +OTY2LTIuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdmlldwAAAAAAE6T+ABRfLgAQzxQAA+3M +AAQTCwADXJ4AAAABWFlaIAAAAAAATAlWAFAAAABXH+dtZWFzAAAAAAAAAAEAAAAAAAAAAAAAAAAA +AAAAAAACjwAAAAJzaWcgAAAAAENSVCBjdXJ2AAAAAAAABAAAAAAFAAoADwAUABkAHgAjACgALQAy +ADcAOwBAAEUASgBPAFQAWQBeAGMAaABtAHIAdwB8AIEAhgCLAJAAlQCaAJ8ApACpAK4AsgC3ALwA +wQDGAMsA0ADVANsA4ADlAOsA8AD2APsBAQEHAQ0BEwEZAR8BJQErATIBOAE+AUUBTAFSAVkBYAFn +AW4BdQF8AYMBiwGSAZoBoQGpAbEBuQHBAckB0QHZAeEB6QHyAfoCAwIMAhQCHQImAi8COAJBAksC +VAJdAmcCcQJ6AoQCjgKYAqICrAK2AsECywLVAuAC6wL1AwADCwMWAyEDLQM4A0MDTwNaA2YDcgN+ +A4oDlgOiA64DugPHA9MD4APsA/kEBgQTBCAELQQ7BEgEVQRjBHEEfgSMBJoEqAS2BMQE0wThBPAE +/gUNBRwFKwU6BUkFWAVnBXcFhgWWBaYFtQXFBdUF5QX2BgYGFgYnBjcGSAZZBmoGewaMBp0GrwbA +BtEG4wb1BwcHGQcrBz0HTwdhB3QHhgeZB6wHvwfSB+UH+AgLCB8IMghGCFoIbgiCCJYIqgi+CNII +5wj7CRAJJQk6CU8JZAl5CY8JpAm6Cc8J5Qn7ChEKJwo9ClQKagqBCpgKrgrFCtwK8wsLCyILOQtR +C2kLgAuYC7ALyAvhC/kMEgwqDEMMXAx1DI4MpwzADNkM8w0NDSYNQA1aDXQNjg2pDcMN3g34DhMO +Lg5JDmQOfw6bDrYO0g7uDwkPJQ9BD14Peg+WD7MPzw/sEAkQJhBDEGEQfhCbELkQ1xD1ERMRMRFP +EW0RjBGqEckR6BIHEiYSRRJkEoQSoxLDEuMTAxMjE0MTYxODE6QTxRPlFAYUJxRJFGoUixStFM4U +8BUSFTQVVhV4FZsVvRXgFgMWJhZJFmwWjxayFtYW+hcdF0EXZReJF64X0hf3GBsYQBhlGIoYrxjV +GPoZIBlFGWsZkRm3Gd0aBBoqGlEadxqeGsUa7BsUGzsbYxuKG7Ib2hwCHCocUhx7HKMczBz1HR4d +Rx1wHZkdwx3sHhYeQB5qHpQevh7pHxMfPh9pH5Qfvx/qIBUgQSBsIJggxCDwIRwhSCF1IaEhziH7 +IiciVSKCIq8i3SMKIzgjZiOUI8Ij8CQfJE0kfCSrJNolCSU4JWgllyXHJfcmJyZXJocmtyboJxgn +SSd6J6sn3CgNKD8ocSiiKNQpBik4KWspnSnQKgIqNSpoKpsqzysCKzYraSudK9EsBSw5LG4soizX +LQwtQS12Last4S4WLkwugi63Lu4vJC9aL5Evxy/+MDUwbDCkMNsxEjFKMYIxujHyMioyYzKbMtQz +DTNGM38zuDPxNCs0ZTSeNNg1EzVNNYc1wjX9Njc2cjauNuk3JDdgN5w31zgUOFA4jDjIOQU5Qjl/ +Obw5+To2OnQ6sjrvOy07azuqO+g8JzxlPKQ84z0iPWE9oT3gPiA+YD6gPuA/IT9hP6I/4kAjQGRA +pkDnQSlBakGsQe5CMEJyQrVC90M6Q31DwEQDREdEikTORRJFVUWaRd5GIkZnRqtG8Ec1R3tHwEgF +SEtIkUjXSR1JY0mpSfBKN0p9SsRLDEtTS5pL4kwqTHJMuk0CTUpNk03cTiVObk63TwBPSU+TT91Q +J1BxULtRBlFQUZtR5lIxUnxSx1MTU19TqlP2VEJUj1TbVShVdVXCVg9WXFapVvdXRFeSV+BYL1h9 +WMtZGllpWbhaB1pWWqZa9VtFW5Vb5Vw1XIZc1l0nXXhdyV4aXmxevV8PX2Ffs2AFYFdgqmD8YU9h +omH1YklinGLwY0Njl2PrZEBklGTpZT1lkmXnZj1mkmboZz1nk2fpaD9olmjsaUNpmmnxakhqn2r3 +a09rp2v/bFdsr20IbWBtuW4SbmtuxG8eb3hv0XArcIZw4HE6cZVx8HJLcqZzAXNdc7h0FHRwdMx1 +KHWFdeF2Pnabdvh3VnezeBF4bnjMeSp5iXnnekZ6pXsEe2N7wnwhfIF84X1BfaF+AX5ifsJ/I3+E +f+WAR4CogQqBa4HNgjCCkoL0g1eDuoQdhICE44VHhauGDoZyhteHO4efiASIaYjOiTOJmYn+imSK +yoswi5aL/IxjjMqNMY2Yjf+OZo7OjzaPnpAGkG6Q1pE/kaiSEZJ6kuOTTZO2lCCUipT0lV+VyZY0 +lp+XCpd1l+CYTJi4mSSZkJn8mmia1ZtCm6+cHJyJnPedZJ3SnkCerp8dn4uf+qBpoNihR6G2oiai +lqMGo3aj5qRWpMelOKWpphqmi6b9p26n4KhSqMSpN6mpqhyqj6sCq3Wr6axcrNCtRK24ri2uoa8W +r4uwALB1sOqxYLHWskuywrM4s660JbSctRO1irYBtnm28Ldot+C4WbjRuUq5wro7urW7LrunvCG8 +m70VvY++Cr6Evv+/er/1wHDA7MFnwePCX8Lbw1jD1MRRxM7FS8XIxkbGw8dBx7/IPci8yTrJuco4 +yrfLNsu2zDXMtc01zbXONs62zzfPuNA50LrRPNG+0j/SwdNE08bUSdTL1U7V0dZV1tjXXNfg2GTY +6Nls2fHadtr724DcBdyK3RDdlt4c3qLfKd+v4DbgveFE4cziU+Lb42Pj6+Rz5PzlhOYN5pbnH+ep +6DLovOlG6dDqW+rl63Dr++yG7RHtnO4o7rTvQO/M8Fjw5fFy8f/yjPMZ86f0NPTC9VD13vZt9vv3 +ivgZ+Kj5OPnH+lf65/t3/Af8mP0p/br+S/7c/23////uAA5BZG9iZQBkAAAAAAH/2wCEAAYEBAQF +BAYFBQYJBgUGCQsIBgYICwwKCgsKCgwQDAwMDAwMEAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM +DAwBBwcHDQwNGBAQGBQODg4UFA4ODg4UEQwMDAwMEREMDAwMDAwRDAwMDAwMDAwMDAwMDAwMDAwM +DAwMDAwMDAwMDP/AABEIAeACgAMBEQACEQEDEQH/3QAEAFD/xAGiAAAABwEBAQEBAAAAAAAAAAAE +BQMCBgEABwgJCgsBAAICAwEBAQEBAAAAAAAAAAEAAgMEBQYHCAkKCxAAAgEDAwIEAgYHAwQCBgJz +AQIDEQQABSESMUFRBhNhInGBFDKRoQcVsUIjwVLR4TMWYvAkcoLxJUM0U5KismNzwjVEJ5OjszYX +VGR0w9LiCCaDCQoYGYSURUaktFbTVSga8uPzxNTk9GV1hZWltcXV5fVmdoaWprbG1ub2N0dXZ3eH +l6e3x9fn9zhIWGh4iJiouMjY6PgpOUlZaXmJmam5ydnp+So6SlpqeoqaqrrK2ur6EQACAgECAwUF +BAUGBAgDA20BAAIRAwQhEjFBBVETYSIGcYGRMqGx8BTB0eEjQhVSYnLxMyQ0Q4IWklMlomOywgdz +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 +q4g+/wBOKrh0r3HTFVhDDqevhirVCHJ6bde+KtToWhNW2FT94xVbHVYIjy+P4eP0ZFtx8050pX1o +zAcrJIF5xTyitZP99oRuvPsWwF22I0LRFvJLPKsdpEPWY8JpH2o7Ap8R8aimRbweJL7meWe3kjSO +UvaExvyO5OKeClkl7cRWkcYqsZCgIdmr3APfFqlFM7DTF1O9Vbw8oYlqig05HwIxboZKRd/La6be +W1jZIvqXFUdPskBlNfiwo+opNqVu73sUssKehH8MdtuSVU0K/NsDbE0jrTT71XJkkWJJBzSMmirT +7KqT+1kSmMt1trolrqv1u5aVXjtVHqK7GNUJH2zx3ffwwLOW6hNNM9otvLKtrKKRMXJU8gPhovU/ +CcHCw4lDTvLWnwalZ3P6QNybct60YQqAT0JPsclEKSmEdhY/pPULm3kAiaNISUbYS8qlqftfD8OS +Qp2OlST6tE87LPb2tJfTc8RV24Vp/qiuQRki7VJ5RexWkYItxLxZx/LyNA2GmyI2TjV7HTU0e4CR +rGgjY8kFGFCKke+PCwjzWW19oT2SpAoeqEUNCWNMkESviSyGzt20uS6IaOW93EYruD9lae2LOORB +2ltqWlre2shKX1+EFjcH4yp2oK9sWUpWmd7o+qzzSSXrrLa3VuLa5h5lVDruZBX4a1xYjmgLCcLq +S6dptuOUcPKYqQFb09uS02rizmo65BqepwTQWyvGaDkDxUU9hinYBlunaDaDSoUuwkzMimVpO527 +9sBcMzNsb1prWaxS2QWz28CGKEsT6sbgn4dvtZFyNOLKF0qe3k0eS1nSnH+7+L4yxYfEoO/wqCuL +dOA4il811c2T+jayFTUFX7jfo2LLw9l2tprss9p9ZijkkJohQ0oxFV38MWojhR2swxDg6SNb3USK +4eNvhSUAcipyJZjITAphqWiX0yW2pS3jzRafAtzJcn+5mc/7qiVeh/yssi4cJEFLtcv7SzeB76wW +C8YJNHJQOOD9myLeBTa2ljqV4kt7cRSiMBo7cLWqnooAxZSyUuutEM90hsZPqkinZo6A8fkMWPjI +fU9AtrSSDU74tdurUZpPiI49NsmEylxBqbWLOe1Z7cKxXiaKu9V37YWsR6Kia9pOqXltNf2EaSsw +SW4XmgbagDKNmwJ8FEzeYdTewljs4WF3YzqZRAnOMwA9qdOS9sLXOFFj0t1BfeZnNvI9pb3ClpQP +gYMorSh6YEg0itXglWzluIr+ZZooiok+zULuK5NgZUGPeVLy7uzPLdSGWUcBzY1O++Qk6rLMk006 +11q7J3UcajLsbjyVdloSSBkjzYNKg9QfLAqq0fxAjqv04quZnI6b/KmNOZ+bkcYhtUDcf53q+r1L +kNKDYAdPH6MHC5OLtTLjjER4R4f0+n/T/wCn/iaVzTagp0qMeFR2xlAEQMfDHi9PAP8AKfWs9aQg +qSDXCIhZ9s6iQIJj67/hj9MvVwf1Fxdg5YHcdclwiqa/5Uz+N4wIGSuH0xH9X8f5rQJpTqDvQiuR +I3asOuyQgYemcDLj4ckeP1/zlqs4qwAHscBCNPrZ4pGUeG5+mXpj/F/N/mf5rlYgHp8XUHEhlptd +PDGUYiNZPr4o8XE0Zm6EAL0PTHhcj+V8tVUK4PC+j/JfzGjOaneo9wO2PCs+2M0iTLgldS4eAcPH +D6Z/1uH0f1XGd1amx3qxA/Vjwhjj7XzxJNiRlPxfVGMv3n86LhcPyIUALWoJG+PCyxdsZochD6jk ++gf3kv4lpmlWtDUVqKDHha8XamWERGPDUZ+JH0x9P/HXSOSenQAKRtSmEBp1GsyZZiRNGAEYcPo4 +Iw+nhaMsgJNADSvKgrXBwt/8qZeLi9Hif6rwR8T+t/X/AKf1LDI7QlKA71NcaaTrZnEcRrhMuPl6 ++P8An8SwkhiQQO22Fw2qL4qffFX/0ITTr75Jk4JXriq0DwxVd4Ab+IxVwNT0xV1d6Yq0y18DirYB +AAp0NcVbKr0HXBatAbnElVpoWrSuBW+PLYDfDSu4+ONK1QVFBgVxoKin3Yq0OmKtsTsopiqyRSCG +7dCMVXqqOCrV+X0YqpKgFqtdippv88iWzEd09Grqlu91ZsHheMpwkaoSQDYBVFTxwO5whqy1Sf8A +R0Llog03qSOQCp9NHrQ17itcgS5Jx7WERqds0GoxPE7TNc1IWJaRLDSqlj4j9rEG2GOW26jdPJGk +rG3JtgjBXpy2YbkVHw/5OFJCVW630skUDSPGSRSY/CwB+WFlkiF+u6c3qRSrO7XWwj2POqnqvjgT +CQ5IMT67a6lHJqKiS4VhRWpyYN0JGKZBHx68Z5byOWkMKAcXdeYND8W37J/ysBDXHYq3lSS3K3Et +tcFLmZWVy4HEqu6jgfh7Y0kiynC3vl+aUW0sEFzO/o8JpKhmkb/eh+fYLtwUDGwgwKlTRoBqUsBa +a0WQBnZCeK1I5KajnxoOXL+ZcbQDRpjr69FzW3t4ZJfWkPDhQtSld9h9nvjbMikw0iLUGvrqR0WO +J4YgscxI5IzGhrkWUzaZRpcQaqGuJo4kmVgkCjnz26VPTxwg0y4tkj1PzJqjf6NBbI4esajryH2e +mS4gx4K3Y3PZahpka3N7DJHGzGgQ8Rv22wcTbExLI/JutXEizR3G8UHH6qGFSnIfEK4205BSa6ld +RSxsGelPiDA/ZI71ONsIBBafc6jIgvWlNzaOHQwOocsPFQwONtnIqPlaFYr+TUhygiZjamKSgoT8 +RO1B0xtMjafyXMc0kws4zcSFRV1I4Bq8as5IphaYiQ5pbNrXmSytjbXdsCX/AHUckDh6MdgDiQzx +gEoezhtLrR1tLxVg1S3LqrybIC5qTJxB3/lOCmYxkHYq5trYaVBdW1tFefA4a4LGJ1KHj8IPXfIs +eM8RCCh0NngMk0MqTuBVmlRR1+9vuxbSTTI7jRbtorcvMheMAupWoI/1v7MXHGYXRYbdW97dXc1h +EVqSwDlgKjuN8BDmgCMbTHSbDznppS1geMWpHIrJIrotP8mrdPlkgXHyEVdLNX03W7i8hu78xz2s +xCG5hPJeJHw022xpj4oKvd2dtb6XG1qvG4tDsgALSL4E4kKBaD0zVtY+uTXX1SsKxAlUIYqakb98 +CeAOn8wDV2a1aAvIlKAVUqclaIwINrbXyoIFa4aRoXYcnIpSnuOmNspHdR0q4s77SrqzdgJraRnj +boSVNVI+eFbKm/mD9H+nClYYWuEmuhESGLr+yT/K3hhpFWitYis9VNtqEkUEWoNKzRRjlT0SKUuC +tdycaackaRB0fSb14ApjMtvD/pQhLFDJLvTc9vlhthCPelo0i20++nEKhVfiTQdx0yMi4eqiANkg +IZtUvtwPiXc5djdfJVI/Z7dskebByV5BiKbU3wJRKhaVA6nc5Ayek0GhxZMUJShxcU/DyS45R4If +6p/NaKnltuMkC6PUY4wySEDxwjI8M/6KxRTp9oeO4xtqiepFhzqobxHhjE25vaenhizcMPp4YS/0 +8RJpo6VIAr7ZOLiZ8EscuE1f9E8X1NKqkENXkdsLHHilOQjEXI/wtiMA8div7XXIEuTp8PDnjDIL +9QhKN/zv6UG5FQA0p1oN8iC7DtHRwxifDGMeDL4cTDJ4k/8AKfXj4p8P0fxcEv8AeogeAock6ThN +11cykivce2KKaKjqR1xSYkNsU6Hr3OLFdGkZUlRzYfs1ptkSXddn6OGTDKYj42WMv7ni4P3P8+PD +6pu2CkgVBNBXCC4WfTjhOWFjF4nhw4/r/nLVVCVWvwnx64Sx0mIHLATFxnID+b9TTqPVZaVAPTxx +C6vBwZpwj9MJyj/mxkpioqKEDsMlTigEtcdxXvgIQ1v2pT5YFf/RhJ2NMkyb3qMVcBua7DFW+P0H +FXAGvTFXcRyOKtEUxVrfuaDFWwtaN9wyKtb8jtTFXcRirqAbqN++SCuqCB13xKtCnLau2RVdQGpx +VZQnfFWyO/cYqtZjT4htiq9VBowND3HfFVi28twPRWiuXoORpkS24xab+XNFt9Lnuri7eNp3oI4z +QAA9TQ9zgdnixSW6jDLLGKhEjjiZ1WnZ2p2AFa5XJzccuhUvLxvbyO7aZ3/R7ExRw12LgAtv9qhx +g2ZQAdkfqRSXR5YfTLhV2QHiap+zXJNaS6LIL/UEErtElii/AG+Nm8d+i4s5pvqiSXEsNw92VMMi +KokBVVBbZmcdgcXFNorzr5Ym0poZLqNIXnKm0eL94spfdpPWWqH2wpw5t6KWx2sVvwghYMG+KaRx +UAe+Bypjq7WdDT6h9ZtC0ZH2n22p3oAMS0iW6SQQxPGFkLSu4p6o24vUdB2yDKUk2nhSC3maN3Le +kU9FyeIBAqaDrWmLUDus0ywt7OOC+jjl+tP+85oAVBNQaexxb5clbWbnV2iDQ3ioLdkj9PgK8QtA +CSTsMU4hxKq3CQaa968vO6SMiOoHEsSK02xYE70w+31u6tdVie7iDSBuTEdAOuw8cW+XJndzeWWr +2fomP1mmXaOnUDFxdwbQUGgDToS3Eq/UsN1xcmMhIJXaRzXE93Z3KvPGo5xOoorbbg4qY0jo76FN +IDwO8K09M8FqyUND8sWBV7G0u4rKDgUuESQyniQ5ZmFPi96YoSy3ur69kudH06FZHUOZOZEfCr7N +y26ZNskQmlqda029lk1m1A9Vg6yR7oDxC70NO22LST3Ipm0htRheXgDNyWGSU0j9Vh8Bk/mWuKN1 +HV9FSwtJ3Nus0VqVMLStwEsjkGX0+Pw8U/1ciWUCLSO889WkVY3j2qKoIxUH54GRiSU1h842EsQJ +mKLtu2wHti1y05u0LBpYt9Wtbu4k9V7l3IhI2UEVBOLZIHgV547aXWJBbu3quvp1VuMa/wAwP9mL +IkDHuq2t5DZvb2jXcNxbPH9Wt4rdmKMS1OUgIHB46dMm45jSMGgwS24ikupOZB9Rl+Bup23GAr4l +JI3l2SHWmjtZWW3SMOGY1JYnYE9+hyLbA2l31y0svMJuCBWNSnWnIt0+nFtKdtqA1hkitGAtY/iv +a9eP8g+eENMkvu/Lln+kYrrTYwkisJHt+RClV6A++SY8SnquveW7YO+qW9xFdvdLcNVFahH7G1Bx +2yQa55KVvLmu2+ryXQtLX0bdZDIee32vsj6MLET40fdQRWlw1xbUE0opOoNNgNtumQbI8kkTUHvL +ubnGY3SikVrgLgapIONdQvSTSrKBTxy/G6+SrIFJWoP+UckebBuIgNRmJ98BSFViOG5OxrkOrtcm +bGdLHGD64zM+X8/0/UvjNFG/zwEOx7O7SwYccYkmP1+LHhlPjlP0xl9XD9P9D/ilpcUBB2pQ400T +18DjjGM5Y4xx+DPHwcWOf+2/V/H9X8+H8LuasBQnb8TjEOTre1MGXHwRlkx8IH0D+99Ih+8jxf0I +8P8AR/pOZ6EmpAIoNu+SIoJPa2LjnKM5x8THCH0/5SH+U+r8cbXKvJ1qakDbIt8dSJyzZ8Inl8Qw +jwY/3eaH9PijxS4fTwelpgBNyrsaVGSHJ1uvMYa4TkeEEwzTj/Fj/i8KX9NqoEtezAkHwrjWzXDW +4oaueUE8OTxOGfD68Msv8XD/ALW36sZAIqWHwg+NcHCXNh2ngAAJlLLCBjHUSB/jl9PDx8fDGHoi +2XHJ9+JO3vUY8LOfauLxMkozlHxYRj9J/vY/5T6v6P8AW9S2WRWQgEmtKD5YiNNfaPamLNjnETke +MwlCPD9Phx9f/KyXqUeYG3AFj365KnTQ1cYw4Rjxk19c+Kc/631eH/V9C5eIoxqrg9R4Yls0k8MY +gmUsWaE+LjiOL93t6fq+qPq/rcS8zArXdfjrx8R4ZHhdrl7Zx5IG+Mfv/F8L+DJi9P7ue/8AFLjy +fxeuTjJHXqW+KtTTbGmUu0sMieKc58WWOaPFH+4jD1cMfV/F/d+n0O9RfiABBLcq7/wIx4Syh2rh +jx0eEzzeNx1P1Rl/BLwsmKXo/rcElq3SDZuQNSSKbUOEwLZp+2sYA4pGPrySlCMPRwZI8MY/VL+P +95woZmUbA1r275YXkTzaqTvxIyKH/9KGUGSZOoMVdQHYdetMVdU1BPQ9sVbqcVaxVxApirVAaBth +44q3+z8O9TQn2xpWuFDU40rjjSrdwdh9OKtnqArfLEq4Amp+jwyKtDv/AFriq4big2oMVaUGlTkq +Va32QfapwFXLuK+I/DAq2SRo4mlA5em1eJ7jIEt2I7tXOp22sXUEVnHJEyhPVMlCFKn4uJH2lb9n +AHcYshrmn2oXFpb2rWUgeqIjAjk6lS3U+H0YJBuDdlA8VrLBbenBbwOJJWc1cBwCW4H49h3wRZSK +xrHTHu1jiuTdTThvVkViEC/5CnqcLIBqewh01aRqsUY3MxHxPQdycjbWCSgby9jgspFluGnW4UNG +BTgCN+2NtkYBPPLOv6pohTS76OPVdBuEVzYSlqQ892aEivE/zL9nG2jLhA3HNM9T0DRJ9Lu9U8uT +NLHC3q3VgxX1YgO4/wAnwrxV/wCdskGqGSQ2JSG3aCedPrUziBERnj2Qc5RVEofj5L+3XFvuJG31 +IiU6e119SgobniSTH1X3xpmMdDdS1uzjtdOCRXXK7K1eCaM8nB68WX4fvxpriAgbG90s6XG0rPE0 +a8JTG/IBq917Y0z3Q0FteXt2RFIl1ZsQSxADgfMY0wnl4fp2TiCCxWw+rTwzwNEzLHIUWWMg9+Ne +XXGmAkTulkel6KdSmM00U8iisMe6MCRRi6np0xptjM9VZ9JVCtylw8UAVozFEpeq+IZd1xps441R +Syz1HWdR1KXTYD6kSR8jzPGir0b/AGWNJjKIOwRelW1/Bqwtp/Uh+FmD0rv2A+nGmUppdc6Dew66 +0UsssenXMtPWY8Aysf3g+jIliBYTOysNEji9GwE0TpJLzk9R2o6r+6B24/H7YEQieqE0Tyvq13NN +etOtnEytDJDIpMkm9eQ3+HfJsMvkj9TOoW0s9mZmlgvI41iJI4rw2YU7NiuMIGxkjsLZbLUx6ysx +Ec7jlsTUD2I8cWUrR9zpt9diNbG + +--------------8B533A82922407D7C3D35A99-- + + +--------------4CEB5E448DC077F35050C4BE-- + + +--------------ABE49921AF9E83E8F9A7667E-- + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/german-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/german-expected.json new file mode 100644 index 00000000000..1a82904897b --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/german-expected.json @@ -0,0 +1,36 @@ +{ + "exception": null, + "result": { + "id": "199606191814.SAA30680@ULM02.mnh.telekom.de", + "boundary": null, + "alternativeBoundary": null, + "sender": { + "name": "Guenter Deuschle", + "mailAddress": "deuschle@mnh.telekom.de", + "valid": true + }, + "toRecipients": [ + { + "name": "Juergen Specht", + "mailAddress": "specht@kulturbox.de", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 835200722000, + "subject": "Re: 34Mbit/s Netz", + "plainBodyText": "Hallo Herr Specht,\r\nentschuldigen Sie vorab, dass ich Ihnen nicht telefonisch zur Verfuegung\r\nstehe, ich Praesentationen gehalten/ noch zu halten und viele\r\nKundennachfragen zu projektieren. Nach Informationen des Produkt-Managers\r\nTemme steht der POP schon zur VerfΓΌgung! Standort: voraussichtlich:\r\nWinterfeldstr. 21, 10781 Berlin.\r\nDer POP hat zur Zeit direkte 34M-Anbindungen zu folgenden Orten: Rostock,\r\nHamburg, Hannover & Leipzig. 4 weitere werden in kuerze in Betrieb gehen.\r\nDamit haben Sie einen Besonderen Sicherheitsstandard verfuegbar!\r\nKontakt muessen Sie ueber Ihre oerltliche Vertriebseinheit aufnehmen:\r\nentweder den Geschaefts-Kunden-Vertrieb oder das GrossKundenManagement.\r\nDiese Vertriebseinheiten greifen auf den oertlichen Technischen\r\nVertriebs-Support zu. Die Informationen werden ueber TVS zur Vertriebseiheit\r\ngegeben und dann zu Ihnen.\r\n Sie benoetigen eine Standleitung von Ihrer Lokation zum Internet-POP\r\nUebergabepunkt zu Ihrem Info-Server ist ein CISCO 1000-Router. Dann zahlen\r\nSie neben den monatlichen Kosten fuer die Standleitung die Kosten fuer den\r\nInternet-Zugang: zB bei 64k: 1500DM bei 2GByte Freivolumen. 128K: 3000 DM\r\nbei 5 GB Freivolumen & 2M: 30.000 DM bei 50GB Freivolumen.\r\nFreundliche Gruesse \r\nGuenter Deuschle\r\n\r\n\r\n>Sehr geehrter Herr Deuschle,\r\n>Sie sind mir von Herrn Meyendriesch empfohlen worden.\r\n>Ich versuche Informationen ueber das T-eigene 34Mbit/s Netz und den \r\n>lokalen Pop-Berlin rauszufinden, bzw. was ein Anschluss kostet und \r\n>wo man ihn herbekommt. Laut Herrn Schnick in Berlin gibt es den \r\n>T-Pop nicht, laut Traceroute von Herrn Meyendriesch sehrwohl. Auch \r\n>ist dies Netz in der IX vom Mai 96 erwaehnt.\r\n>Koennen Sie mir helfen?\r\n>\r\n>MfG\r\n>-- \r\n>Juergen Specht - KULTURBOX\r\n>\r\n>\r\n\r\n===================================================\r\nDipl.-Ing. Guenter D E U S C H L E\r\nDeutsche Telekom AG Niederlassung 3 Hannover\r\nGrossKundenManagement - Techn. Vertriebs-Support:\r\nTeam-Leiter Internet Online-Dienste\r\n---------------------------------------------------\r\nGrKM-TVS-IOD Tel: +49-511-333-2772\r\nVahrenwalder-Str. 245 FAX: +49-511-333-2751\r\n30179 Hannover eMail: deuschle@mnh.telekom.de \r\n===================================================\r\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [], + "mailHeaders": "X-POP3-Rcpt: specht@trachea\nReturn-Path: hermes\nReceived: (from hermes@localhost) by kulturbox.netmbx.de (8.7.1/8.7.1) id SAA04513 for specht; Wed, 19 Jun 1996 18:30:12 +0200\nReceived: by netmbx.netmbx.de (/\\==/\\ Smail3.1.28.1)\r\n\t from mail.cs.tu-berlin.de with smtp\r\n\t id ; Wed, 19 Jun 96 18:12 MES\nReceived: (from nobody@localhost) by mail.cs.tu-berlin.de (8.6.12/8.6.12) id SAA12413; Wed, 19 Jun 1996 18:26:28 +0200\nResent-Date: Wed, 19 Jun 1996 18:26:28 +0200\nResent-Message-Id: <199606191626.SAA12413@mail.cs.tu-berlin.de>\nResent-From: nobody@cs.tu-berlin.de\nResent-To: kultur@kulturbox.netmbx.de\nReceived: from gatekeeper.telekom.de ([194.25.15.11]) by mail.cs.tu-berlin.de (8.6.12/8.6.12) with SMTP id SAA11678 for ; Wed, 19 Jun 1996 18:11:29 +0200\nReceived: from ULM02.mnh.telekom.de by gatekeeper.telekom.de; (5.65v3.0/1.1.8.2/02Aug95-0132PM)\r\n\tid AA01376; Wed, 19 Jun 1996 18:11:27 +0200\nReceived: from ulm02.mnh.telekom.de (deuschle@mnh.telekom.de) by ULM02.mnh.telekom.de (8.6.10/3) with SMTP id SAA30680 for ; Wed, 19 Jun 1996 18:14:40 GMT\nMessage-Id: <199606191814.SAA30680@ULM02.mnh.telekom.de>\nX-Sender: deuschle@ulm02.mnh.telekom.de\nX-Mailer: Windows Eudora Version 1.4.4\nMime-Version: 1.0\nContent-Type: text/plain; charset=\"iso-8859-1\"\nContent-Transfer-Encoding: quoted-printable\nDate: Wed, 19 Jun 1996 18:12:02 +0200\nTo: Juergen Specht \nFrom: deuschle@mnh.telekom.de (Guenter Deuschle)\nSubject: Re: 34Mbit/s Netz\nX-Mozilla-Status: 0011", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/german-qp-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/german-qp-expected.json new file mode 100644 index 00000000000..cca3a015cda --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/german-qp-expected.json @@ -0,0 +1,36 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": null, + "alternativeBoundary": null, + "sender": { + "name": "JΓΆrn Reder", + "mailAddress": "joern@zyn.de", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "joern@zyn.de", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 1135198964000, + "subject": "Testnachricht", + "plainBodyText": "\nHallo,\n\ndas ist eine Testnachricht mit 8 Bit SΓΆnderzΓ€ichen, und obendrein noch \nquoted-printable kodiert.\n\nGrüße,\n\nJΓΆrn\n\n-- \n .''`. JΓΆrn Reder \n: :' : http://www.exit1.org/ http://www.zyn.de/\n`. `'\n `- Debian GNU/Linux -- The power of freedom\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [], + "mailHeaders": "Content-Type: text/plain; charset=\"iso-8859-15\"\nContent-Disposition: inline\nContent-Transfer-Encoding: quoted-printable\nMIME-Version: 1.0\nX-Mailer: JaM - Just A Mailer\nSubject: Testnachricht\nReturn-Path: \nDate: Wed, 21 Dec 2005 22:02:44 +0100\nTo: joern@zyn.de\nFrom: \"JΓΆrn\" Reder ", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/german-qp.msg b/packages/node-mimimi/test/mimetools-testmsgs/german-qp.msg new file mode 100644 index 00000000000..687c85e526b --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/german-qp.msg @@ -0,0 +1,27 @@ +Content-Type: text/plain; charset="iso-8859-15" +Content-Disposition: inline +Content-Transfer-Encoding: quoted-printable +MIME-Version: 1.0 +X-Mailer: JaM - Just A Mailer +Subject: Testnachricht +Return-Path: +Date: Wed, 21 Dec 2005 22:02:44 +0100 +To: joern@zyn.de +From: =?ISO-8859-15?Q?J=F6rn?= Reder + +=0A= +Hallo,=0A= +=0A= +das ist eine Testnachricht mit 8 Bit S=F6nderz=E4ichen, und obendrein noch= +=20=0A= +quoted-printable kodiert.=0A= +=0A= +Gr=FC=DFe,=0A= +=0A= +J=F6rn=0A= + +--=20=0A= + .''`. J=F6rn Reder =0A= +: :' : http://www.exit1.org/ http://www.zyn.de/=0A= +`. `'=0A= + `- Debian GNU/Linux -- The power of freedom=0A= diff --git a/packages/node-mimimi/test/mimetools-testmsgs/german.msg b/packages/node-mimimi/test/mimetools-testmsgs/german.msg new file mode 100644 index 00000000000..fd743729dd1 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/german.msg @@ -0,0 +1,79 @@ +X-POP3-Rcpt: specht@trachea +Return-Path: hermes +Received: (from hermes@localhost) by kulturbox.netmbx.de (8.7.1/8.7.1) id SAA04513 for specht; Wed, 19 Jun 1996 18:30:12 +0200 +Received: by netmbx.netmbx.de (/\==/\ Smail3.1.28.1) + from mail.cs.tu-berlin.de with smtp + id ; Wed, 19 Jun 96 18:12 MES +Received: (from nobody@localhost) by mail.cs.tu-berlin.de (8.6.12/8.6.12) id SAA12413; Wed, 19 Jun 1996 18:26:28 +0200 +Resent-Date: Wed, 19 Jun 1996 18:26:28 +0200 +Resent-Message-Id: <199606191626.SAA12413@mail.cs.tu-berlin.de> +Resent-From: nobody@cs.tu-berlin.de +Resent-To: kultur@kulturbox.netmbx.de +Received: from gatekeeper.telekom.de ([194.25.15.11]) by mail.cs.tu-berlin.de (8.6.12/8.6.12) with SMTP id SAA11678 for ; Wed, 19 Jun 1996 18:11:29 +0200 +Received: from ULM02.mnh.telekom.de by gatekeeper.telekom.de; (5.65v3.0/1.1.8.2/02Aug95-0132PM) + id AA01376; Wed, 19 Jun 1996 18:11:27 +0200 +Received: from ulm02.mnh.telekom.de (deuschle@mnh.telekom.de) by ULM02.mnh.telekom.de (8.6.10/3) with SMTP id SAA30680 for ; Wed, 19 Jun 1996 18:14:40 GMT +Message-Id: <199606191814.SAA30680@ULM02.mnh.telekom.de> +X-Sender: deuschle@ulm02.mnh.telekom.de +X-Mailer: Windows Eudora Version 1.4.4 +Mime-Version: 1.0 +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable +Date: Wed, 19 Jun 1996 18:12:02 +0200 +To: Juergen Specht +From: deuschle@mnh.telekom.de (Guenter Deuschle) +Subject: Re: 34Mbit/s Netz +X-Mozilla-Status: 0011 + +Hallo Herr Specht, +entschuldigen Sie vorab, dass ich Ihnen nicht telefonisch zur Verfuegung +stehe, ich Praesentationen gehalten/ noch zu halten und viele +Kundennachfragen zu projektieren. Nach Informationen des Produkt-Managers +Temme steht der POP schon zur Verf=FCgung! Standort: voraussichtlich: +Winterfeldstr. 21, 10781 Berlin. +Der POP hat zur Zeit direkte 34M-Anbindungen zu folgenden Orten: Rostock, +Hamburg, Hannover & Leipzig. 4 weitere werden in kuerze in Betrieb gehen. +Damit haben Sie einen Besonderen Sicherheitsstandard verfuegbar! +Kontakt muessen Sie ueber Ihre oerltliche Vertriebseinheit aufnehmen: +entweder den Geschaefts-Kunden-Vertrieb oder das GrossKundenManagement. +Diese Vertriebseinheiten greifen auf den oertlichen Technischen +Vertriebs-Support zu. Die Informationen werden ueber TVS zur Vertriebseiheit +gegeben und dann zu Ihnen. + Sie benoetigen eine Standleitung von Ihrer Lokation zum Internet-POP +Uebergabepunkt zu Ihrem Info-Server ist ein CISCO 1000-Router. Dann zahlen +Sie neben den monatlichen Kosten fuer die Standleitung die Kosten fuer den +Internet-Zugang: zB bei 64k: 1500DM bei 2GByte Freivolumen. 128K: 3000 DM +bei 5 GB Freivolumen & 2M: 30.000 DM bei 50GB Freivolumen. +Freundliche Gruesse=20 +Guenter Deuschle + + +>Sehr geehrter Herr Deuschle, +>Sie sind mir von Herrn Meyendriesch empfohlen worden. +>Ich versuche Informationen ueber das T-eigene 34Mbit/s Netz und den=20 +>lokalen Pop-Berlin rauszufinden, bzw. was ein Anschluss kostet und=20 +>wo man ihn herbekommt. Laut Herrn Schnick in Berlin gibt es den=20 +>T-Pop nicht, laut Traceroute von Herrn Meyendriesch sehrwohl. Auch=20 +>ist dies Netz in der IX vom Mai 96 erwaehnt. +>Koennen Sie mir helfen? +> +>MfG +>--=20 +>Juergen Specht - KULTURBOX +> +> + +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= +=3D +Dipl.-Ing. Guenter D E U S C H L E +Deutsche Telekom AG Niederlassung 3 Hannover +GrossKundenManagement - Techn. Vertriebs-Support: +Team-Leiter Internet Online-Dienste +--------------------------------------------------- +GrKM-TVS-IOD Tel: +49-511-333-2772 +Vahrenwalder-Str. 245 FAX: +49-511-333-2751 +30179 Hannover eMail: deuschle@mnh.telekom.de=20 +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= +=3D diff --git a/packages/node-mimimi/test/mimetools-testmsgs/hdr-fakeout-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/hdr-fakeout-expected.json new file mode 100644 index 00000000000..c38b06cc253 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/hdr-fakeout-expected.json @@ -0,0 +1,36 @@ +{ + "exception": null, + "result": { + "id": "20000519215502.A24482@quist.on.ca", + "boundary": null, + "alternativeBoundary": null, + "sender": { + "name": "Russell P. Sutherland", + "mailAddress": "russ@quist.on.ca", + "valid": true + }, + "toRecipients": [ + { + "name": "Russell P. Sutherland", + "mailAddress": "russ@quist.on.ca", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 958787702000, + "subject": "test message 1", + "plainBodyText": "", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [], + "mailHeaders": "Received: (qmail 24486 invoked by uid 501); 20 May 2000 01:55:02 -0000\nDate: Fri, 19 May 2000 21:55:02 -0400\nFrom: \"Russell P. Sutherland\" \nTo: \"Russell P. Sutherland\" \nSubject: test message 1\nMessage-ID: <20000519215502.A24482@quist.on.ca>\nMime-Version: 1.0\nContent-transfer-encoding: 7BIT\nContent-Type: text/plain; charset=us-ascii\nX-Mailer: Mutt 1.0us\nOrganization: Quist Consulting\r\n \nThe header is not properly terminated; the \"blank line\" \nactually has a space in it.", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/hdr-fakeout.msg b/packages/node-mimimi/test/mimetools-testmsgs/hdr-fakeout.msg new file mode 100644 index 00000000000..612a9bc4537 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/hdr-fakeout.msg @@ -0,0 +1,15 @@ +Received: (qmail 24486 invoked by uid 501); 20 May 2000 01:55:02 -0000 +Date: Fri, 19 May 2000 21:55:02 -0400 +From: "Russell P. Sutherland" +To: "Russell P. Sutherland" +Subject: test message 1 +Message-ID: <20000519215502.A24482@quist.on.ca> +Mime-Version: 1.0 +Content-transfer-encoding: 7BIT +Content-Type: text/plain; charset=us-ascii +X-Mailer: Mutt 1.0us +Organization: Quist Consulting + +The header is not properly terminated; the "blank line" +actually has a space in it. + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/infinite-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/infinite-expected.json new file mode 100644 index 00000000000..6bb6ba2734d --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/infinite-expected.json @@ -0,0 +1,30 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": null, + "alternativeBoundary": null, + "sender": { + "name": "", + "mailAddress": "", + "valid": false + }, + "toRecipients": [], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "", + "plainBodyText": "Return-Path: \r\nReceived: from eiffel.base.com.br by mailbr1.mailbr.com.br ; Thu, 05 Nov 1998 22:58:23 +000\r\nReceived: from marconi.base.com.br ([200.240.10.55]) by eiffel.base.com.br\r\n (Netscape Mail Server v2.0) with ESMTP id ADH491\r\n for ; Thu, 5 Nov 1998 21:55:24 -0200\r\nReceived: from kepler.base.com.br ([200.240.10.104]) by marconi.base.com.br\r\n (Netscape Mail Server v2.0) with ESMTP id AAE686;\r\n Wed, 4 Nov 1998 14:00:10 -0200\r\nReceived: from cyrius.linf.unb.br ([164.41.12.4]) by kepler.base.com.br\r\n (Post.Office MTA v3.5 release 215 ID# 0-0U10L2S100) with SMTP\r\n id br; Wed, 4 Nov 1998 13:53:47 -0200\r\nReceived: from sendmail by cyrius.linf.unb.br with esmtp\r\n\tid 0zb5I9-0003oM-00; Wed, 4 Nov 1998 13:56:17 -0200\r\nReceived: from localhost (majordom@localhost)\r\n\tby cyrius.linf.unb.br (8.8.7/8.8.7) with SMTP id NAA14639;\r\n\tWed, 4 Nov 1998 13:54:54 -0200 (EDT)\r\nReceived: by linf.unb.br (bulk_mailer v1.6); Wed, 4 Nov 1998 13:54:54 -0200\r\nReceived: (from majordom@localhost)\r\n\tby cyrius.linf.unb.br (8.8.7/8.8.7) id NAA14630\r\n\tfor cacomp-l-outter; Wed, 4 Nov 1998 13:54:53 -0200 (EDT)\r\nReceived: (from sendmail@localhost)\r\n\tby cyrius.linf.unb.br (8.8.7/8.8.7) id NAA14623\r\n\tfor cacomp-l@linf.unb.br; Wed, 4 Nov 1998 13:54:50 -0200 (EDT)\r\nReceived: from brasilia.mpdft.gov.br [200.252.85.2] \r\n\tby cyrius.linf.unb.br with esmtp\r\n\tid 0zb5Fz-0003lF-00; Wed, 4 Nov 1998 13:54:28 -0200\r\nReceived: from localhost (lbecker@localhost)\r\n\tby brasilia.mpdft.gov.br (8.8.5/8.8.8) with ESMTP id NAA02571\r\n\tfor ; Wed, 4 Nov 1998 13:36:14 -0200 (EDT)\r\n\t(envelope-from lbecker@brasilia.mpdft.gov.br)\r\nDate: Wed, 4 Nov 1998 13:36:14 -0200 (EDT)\r\nFrom: Lula Becker \r\nTo: cacomp-l@linf.unb.br\r\nSubject: [cacomp-l] =??Q?Re=3A_=5Bcacomp-l=5D_E_n=3A_Bras=EDlia_cobre?=\r\nIn-Reply-To: \r\nMessage-ID: \r\nMIME-Version: 1.0\r\nContent-Type: TEXT/PLAIN; charset=\r\nX-MIME-Autoconverted: from 8bit to quoted-printable by brasilia.mpdft.gov.br id NAA02571\r\nContent-Transfer-Encoding: 8bit\r\nX-MIME-Autoconverted: from quoted-printable to 8bit by cyrius.linf.unb.br id NAB14623\r\nSender: owner-cacomp-l@linf.unb.br\r\nReply-To: cacomp-l@linf.unb.br\r\nPrecedence: bulk\r\nX-Rcpt-To: Porque eu sou eh foda.... \r\n> Prego eh o caralho, Vc nao me conhece para ficar falando assim... Nao \r\n> sabe de onde eu vim, quem sou, ou seja, PORRA NENHUMA...\r\n> \r\n> VAI TOMAR NO CU E ME DEIXA EM PAZ!!!!!!!\r\n> \r\n> On Tue, 3 Nov 1998, Guilherme Olivieri Caixeta Borges wrote:\r\n> \r\n> > SΓ³crates,\r\n> > \r\n> > porque vocΓͺ faz tanta questΓ£o de se indispor com TODO MUNDO!!! JΓ‘ nΓ£o bastasse\r\n> > meia computação te achar um prego, vocΓͺ resolve ampliar este universo para outras\r\n> > pessoas... ManΓ©!\r\n> > \r\n> > Socrates Arantes Teixeira Filho (97/18443) wrote:\r\n> > \r\n> > > VocΓͺ tem que ver que paciΓͺncia tem limite. Eu agΓΌentei o mΓ‘ximo\r\n> > > possΓ­vel ele ficar escrevendo essas burrices na nossa sala de discursΓ£o.\r\n> > > Com gente ignorante como ele, que um rorizista cego, nΓ³s sΓ³ conseguimos\r\n> > \r\n> > Em se falando de ignorΓ’ncia: Γ© bem verdade que os mails da cacomp estΓ£o atingindo\r\n> > proporçáes dantescas, mas o certo Γ© DISCUSSΓƒO.\r\n> > \r\n> > Sem mais,\r\n> > \r\n> > --\r\n> > Guilherme Olivieri Caixeta Borges\r\n> > **************************\r\n> > Web Design - Via Internet\r\n> > (061) 315-9657 / 964-9199\r\n> > guiborges@brasilia.com.br\r\n> > guiborges@via-net.com.br\r\n> > **************************\r\n> > \r\n> > \r\n> > \r\n> > \r\n> > \r\n> \r\n> \r\n\r\n\r\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [], + "mailHeaders": "Content-Type: TEXT/PLAIN; name=109f53c446c8882f4318316ecf4480ce\nContent-Transfer-Encoding: BASE64\nContent-ID: \nContent-Description: ", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/infinite.msg b/packages/node-mimimi/test/mimetools-testmsgs/infinite.msg new file mode 100644 index 00000000000..c760b262baa --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/infinite.msg @@ -0,0 +1,92 @@ +Content-Type: TEXT/PLAIN; name=109f53c446c8882f4318316ecf4480ce +Content-Transfer-Encoding: BASE64 +Content-ID: +Content-Description: + +UmV0dXJuLVBhdGg6IDxvd25lci1jYWNvbXAtbEBsaW5mLnVuYi5icj4NClJl +Y2VpdmVkOiBmcm9tIGVpZmZlbC5iYXNlLmNvbS5iciBieSBtYWlsYnIxLm1h +aWxici5jb20uYnIgOyBUaHUsIDA1IE5vdiAxOTk4IDIyOjU4OjIzICswMDAN +ClJlY2VpdmVkOiBmcm9tIG1hcmNvbmkuYmFzZS5jb20uYnIgKFsyMDAuMjQw +LjEwLjU1XSkgYnkgZWlmZmVsLmJhc2UuY29tLmJyDQogICAgICAgICAgKE5l +dHNjYXBlIE1haWwgU2VydmVyIHYyLjApIHdpdGggRVNNVFAgaWQgQURINDkx +DQogICAgICAgICAgZm9yIDxhY2VjaWxpYUBtYWlsYnIuY29tLmJyPjsgVGh1 +LCA1IE5vdiAxOTk4IDIxOjU1OjI0IC0wMjAwDQpSZWNlaXZlZDogZnJvbSBr +ZXBsZXIuYmFzZS5jb20uYnIgKFsyMDAuMjQwLjEwLjEwNF0pIGJ5IG1hcmNv +bmkuYmFzZS5jb20uYnINCiAgICAgICAgICAoTmV0c2NhcGUgTWFpbCBTZXJ2 +ZXIgdjIuMCkgd2l0aCBFU01UUCBpZCBBQUU2ODY7DQogICAgICAgICAgV2Vk +LCA0IE5vdiAxOTk4IDE0OjAwOjEwIC0wMjAwDQpSZWNlaXZlZDogZnJvbSBj +eXJpdXMubGluZi51bmIuYnIgKFsxNjQuNDEuMTIuNF0pIGJ5IGtlcGxlci5i +YXNlLmNvbS5icg0KICAgICAgICAgIChQb3N0Lk9mZmljZSBNVEEgdjMuNSBy +ZWxlYXNlIDIxNSBJRCMgMC0wVTEwTDJTMTAwKSB3aXRoIFNNVFANCiAgICAg +ICAgICBpZCBicjsgV2VkLCA0IE5vdiAxOTk4IDEzOjUzOjQ3IC0wMjAwDQpS +ZWNlaXZlZDogZnJvbSBzZW5kbWFpbCBieSBjeXJpdXMubGluZi51bmIuYnIg +d2l0aCBlc210cA0KCWlkIDB6YjVJOS0wMDAzb00tMDA7IFdlZCwgNCBOb3Yg +MTk5OCAxMzo1NjoxNyAtMDIwMA0KUmVjZWl2ZWQ6IGZyb20gbG9jYWxob3N0 +IChtYWpvcmRvbUBsb2NhbGhvc3QpDQoJYnkgY3lyaXVzLmxpbmYudW5iLmJy +ICg4LjguNy84LjguNykgd2l0aCBTTVRQIGlkIE5BQTE0NjM5Ow0KCVdlZCwg +NCBOb3YgMTk5OCAxMzo1NDo1NCAtMDIwMCAoRURUKQ0KUmVjZWl2ZWQ6IGJ5 +IGxpbmYudW5iLmJyIChidWxrX21haWxlciB2MS42KTsgV2VkLCA0IE5vdiAx +OTk4IDEzOjU0OjU0IC0wMjAwDQpSZWNlaXZlZDogKGZyb20gbWFqb3Jkb21A +bG9jYWxob3N0KQ0KCWJ5IGN5cml1cy5saW5mLnVuYi5iciAoOC44LjcvOC44 +LjcpIGlkIE5BQTE0NjMwDQoJZm9yIGNhY29tcC1sLW91dHRlcjsgV2VkLCA0 +IE5vdiAxOTk4IDEzOjU0OjUzIC0wMjAwIChFRFQpDQpSZWNlaXZlZDogKGZy +b20gc2VuZG1haWxAbG9jYWxob3N0KQ0KCWJ5IGN5cml1cy5saW5mLnVuYi5i +ciAoOC44LjcvOC44LjcpIGlkIE5BQTE0NjIzDQoJZm9yIGNhY29tcC1sQGxp +bmYudW5iLmJyOyBXZWQsIDQgTm92IDE5OTggMTM6NTQ6NTAgLTAyMDAgKEVE +VCkNClJlY2VpdmVkOiBmcm9tIGJyYXNpbGlhLm1wZGZ0Lmdvdi5iciBbMjAw +LjI1Mi44NS4yXSANCglieSBjeXJpdXMubGluZi51bmIuYnIgd2l0aCBlc210 +cA0KCWlkIDB6YjVGei0wMDAzbEYtMDA7IFdlZCwgNCBOb3YgMTk5OCAxMzo1 +NDoyOCAtMDIwMA0KUmVjZWl2ZWQ6IGZyb20gbG9jYWxob3N0IChsYmVja2Vy +QGxvY2FsaG9zdCkNCglieSBicmFzaWxpYS5tcGRmdC5nb3YuYnIgKDguOC41 +LzguOC44KSB3aXRoIEVTTVRQIGlkIE5BQTAyNTcxDQoJZm9yIDxjYWNvbXAt +bEBsaW5mLnVuYi5icj47IFdlZCwgNCBOb3YgMTk5OCAxMzozNjoxNCAtMDIw +MCAoRURUKQ0KCShlbnZlbG9wZS1mcm9tIGxiZWNrZXJAYnJhc2lsaWEubXBk +ZnQuZ292LmJyKQ0KRGF0ZTogV2VkLCA0IE5vdiAxOTk4IDEzOjM2OjE0IC0w +MjAwIChFRFQpDQpGcm9tOiBMdWxhIEJlY2tlciA8bGJlY2tlckBicmFzaWxp +YS5tcGRmdC5nb3YuYnI+DQpUbzogY2Fjb21wLWxAbGluZi51bmIuYnINClN1 +YmplY3Q6IFtjYWNvbXAtbF0gPT8/UT9SZT0zQV89NUJjYWNvbXAtbD01RF9F +X249M0FfQnJhcz1FRGxpYV9jb2JyZT89DQpJbi1SZXBseS1UbzogPFBpbmUu +U1VOLjMuOTEuOTgxMTAzMjM1OTQzLjIyNDFFLTEwMDAwMEBhbnRhcmVzLmxp +bmYudW5iLmJyPg0KTWVzc2FnZS1JRDogPFBpbmUuQlNGLjMuOTYuOTgxMTA0 +MTMzNTQ3LjI1MzJCLTEwMDAwMEBicmFzaWxpYS5tcGRmdC5nb3YuYnI+DQpN +SU1FLVZlcnNpb246IDEuMA0KQ29udGVudC1UeXBlOiBURVhUL1BMQUlOOyBj +aGFyc2V0PQ0KWC1NSU1FLUF1dG9jb252ZXJ0ZWQ6IGZyb20gOGJpdCB0byBx +dW90ZWQtcHJpbnRhYmxlIGJ5IGJyYXNpbGlhLm1wZGZ0Lmdvdi5iciBpZCBO +QUEwMjU3MQ0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogOGJpdA0KWC1N +SU1FLUF1dG9jb252ZXJ0ZWQ6IGZyb20gcXVvdGVkLXByaW50YWJsZSB0byA4 +Yml0IGJ5IGN5cml1cy5saW5mLnVuYi5iciBpZCBOQUIxNDYyMw0KU2VuZGVy +OiBvd25lci1jYWNvbXAtbEBsaW5mLnVuYi5icg0KUmVwbHktVG86IGNhY29t +cC1sQGxpbmYudW5iLmJyDQpQcmVjZWRlbmNlOiBidWxrDQpYLVJjcHQtVG86 +IDxhY2VjaWxpYUBtYWlsYnIuY29tLmJyDQpYLURQT1A6IERQT1AgVmVyc2lv +biAyLjNkDQpYLVVJREw6IDkxMDM4NjgxNi4wMDQNClN0YXR1czogUk8NCg0K +DQoNCglDb21vIGFzc2ltLCBkYWggdW0gZXhlbXBsby4gRGVpeGFuZG8gYSBj +b250YSBhYmVydGEsIG5laCwgRnJlZC4uLg0KDQoJDQoNCk9uIFdlZCwgNCBO +b3YgMTk5OCwgRnJlZGVyaWNvIE5hcmRvdHRvIHdyb3RlOg0KDQo+IFBvcnF1 +ZSBldSBzb3UgZWggZm9kYS4uLi4gDQo+IFByZWdvIGVoIG8gY2FyYWxobywg +VmMgbmFvIG1lIGNvbmhlY2UgcGFyYSBmaWNhciBmYWxhbmRvIGFzc2ltLi4u +IE5hbyANCj4gc2FiZSBkZSBvbmRlIGV1IHZpbSwgcXVlbSBzb3UsIG91IHNl +amEsIFBPUlJBIE5FTkhVTUEuLi4NCj4gDQo+IFZBSSBUT01BUiBOTyBDVSBF +IE1FIERFSVhBIEVNIFBBWiEhISEhISENCj4gDQo+IE9uIFR1ZSwgMyBOb3Yg +MTk5OCwgR3VpbGhlcm1lIE9saXZpZXJpIENhaXhldGEgQm9yZ2VzIHdyb3Rl +Og0KPiANCj4gPiBT82NyYXRlcywNCj4gPiANCj4gPiAgICAgcG9ycXVlIHZv +Y+ogZmF6IHRhbnRhIHF1ZXN0428gZGUgc2UgaW5kaXNwb3IgY29tIFRPRE8g +TVVORE8hISEgSuEgbuNvIGJhc3Rhc3NlDQo+ID4gbWVpYSBjb21wdXRh5+Nv +IHRlIGFjaGFyIHVtIHByZWdvLCB2b2PqIHJlc29sdmUgYW1wbGlhciBlc3Rl +IHVuaXZlcnNvIHBhcmEgb3V0cmFzDQo+ID4gcGVzc29hcy4uLiBNYW7pIQ0K +PiA+IA0KPiA+IFNvY3JhdGVzIEFyYW50ZXMgVGVpeGVpcmEgRmlsaG8gKDk3 +LzE4NDQzKSB3cm90ZToNCj4gPiANCj4gPiA+ICAgICAgVm9j6iB0ZW0gcXVl +IHZlciBxdWUgcGFjaepuY2lhIHRlbSBsaW1pdGUuIEV1IGFn/GVudGVpIG8g +beF4aW1vDQo+ID4gPiBwb3Nz7XZlbCBlbGUgZmljYXIgZXNjcmV2ZW5kbyBl +c3NhcyBidXJyaWNlcyBuYSBub3NzYSBzYWxhIGRlIGRpc2N1cnPjby4NCj4g +PiA+IENvbSBnZW50ZSBpZ25vcmFudGUgY29tbyBlbGUsIHF1ZSB1bSByb3Jp +emlzdGEgY2VnbywgbvNzIHPzIGNvbnNlZ3VpbW9zDQo+ID4gDQo+ID4gICAg +IEVtIHNlIGZhbGFuZG8gZGUgaWdub3LibmNpYTog6SBiZW0gdmVyZGFkZSBx +dWUgb3MgbWFpbHMgZGEgY2Fjb21wIGVzdONvIGF0aW5naW5kbw0KPiA+IHBy +b3Bvcuf1ZXMgZGFudGVzY2FzLCBtYXMgbyBjZXJ0byDpIERJU0NVU1PDTy4N +Cj4gPiANCj4gPiBTZW0gbWFpcywNCj4gPiANCj4gPiAtLQ0KPiA+IEd1aWxo +ZXJtZSBPbGl2aWVyaSBDYWl4ZXRhIEJvcmdlcw0KPiA+ICoqKioqKioqKioq +KioqKioqKioqKioqKioqDQo+ID4gV2ViIERlc2lnbiAtIFZpYSBJbnRlcm5l +dA0KPiA+ICgwNjEpIDMxNS05NjU3IC8gOTY0LTkxOTkNCj4gPiBndWlib3Jn +ZXNAYnJhc2lsaWEuY29tLmJyDQo+ID4gZ3VpYm9yZ2VzQHZpYS1uZXQuY29t +LmJyDQo+ID4gKioqKioqKioqKioqKioqKioqKioqKioqKioNCj4gPiANCj4g +PiANCj4gPiANCj4gPiANCj4gPiANCj4gDQo+IA0KDQoNCg== diff --git a/packages/node-mimimi/test/mimetools-testmsgs/intl-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/intl-expected.json new file mode 100644 index 00000000000..e447adaa7c5 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/intl-expected.json @@ -0,0 +1,48 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": null, + "alternativeBoundary": null, + "sender": { + "name": "Keith Moore", + "mailAddress": "moore@cs.utk.edu", + "valid": true + }, + "toRecipients": [ + { + "name": "Keld JΓΈrn Simonsen", + "mailAddress": "keld@dkuug.dk", + "valid": true + } + ], + "ccRecipients": [ + { + "name": "AndrΓ© Pirard", + "mailAddress": "PIRARD@vm1.ulg.ac.be", + "valid": true + } + ], + "bccRecipients": [ + { + "name": "Patrik FΓ€ltstrΓΆm", + "mailAddress": "paf@nada.kth.se", + "valid": true + } + ], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "If you can read this you understand the example... so, cool!", + "plainBodyText": "How's this?\n\n\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [], + "mailHeaders": "From: \"Keith Moore\" \nTo: =?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?= \nCC: =?ISO-8859-1?Q?Andr=E9_?= Pirard \nBCC: =?ISO-8859-1?Q?Patrik_F=E4ltstr=F6m?= \nSubject: =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\r\n =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\r\n =?US-ASCII?Q?.._so,_cool!?=\nContent-type: text/plain", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/intl.msg b/packages/node-mimimi/test/mimetools-testmsgs/intl.msg new file mode 100644 index 00000000000..4c32deda87c --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/intl.msg @@ -0,0 +1,12 @@ +From: =?US-ASCII?Q?Keith_Moore?= +To: =?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?= +CC: =?ISO-8859-1?Q?Andr=E9_?= Pirard +BCC: =?ISO-8859-1?Q?Patrik_F=E4ltstr=F6m?= +Subject: =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?= + =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?= + =?US-ASCII?Q?.._so,_cool!?= +Content-type: text/plain + +How's this? + + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/jt-0498-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/jt-0498-expected.json new file mode 100644 index 00000000000..8d996452459 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/jt-0498-expected.json @@ -0,0 +1,45 @@ +{ + "exception": null, + "result": { + "id": "c8384e50.354898b3@aol.com", + "boundary": "part0_893950130_boundary", + "alternativeBoundary": null, + "sender": { + "name": "Gaffneydp", + "mailAddress": "Gaffneydp@aol.com", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "PC800@hpc.uh.edu", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 893950130000, + "subject": "Fwd: PC800: Tall Hondaline Windshield Distortion", + "plainBodyText": "Hello to all, \n\nI purchased a tall windshield about two weeks ago from Waynesville Cycle\nCenter in NC. The entire length and width of the windshield has optical waves\nor ripples which distort the view through it. It was very uncomfortable to\nride with and I would be afraid to ride at night with it.\n\nI contacted the manager at WCC. He in turn contacted Honda Customer Service\n(310-532-9811). They replied to him that there were no bulletins concerning\nthis problem and that they inspected several windshields. They claim that all\nthe windshields had distortions. They have offered to give me a full refund.\n\nWhat bothers me is two things. First, the stock windshield has no optical\ndistortion. Second, it appears that Honda knows that it is selling a less\nthan perfect product and is apparently unconcerned about it (seems like a\nstrange way to do business). \n\nPerhaps my windshield is the worst one ever made, but they made no offer to\ninspect mine and compare to others that they have in stock.\n\nI am going to call Honda on Monday and raise the issues of safety and quality.\nI will ask them if they have a problem with me forwarding their position to\npublications such as Cycle World, Rider, etc.\n\nDennis Gaffney\nMarlboro, NY\ngaffneydp@aol.com\n1994 PC800\nBought used in 1997 (2000 miles)\nModifications: tall windshield?\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "Re_ PC800_ Tall Hondaline Windshield Distortion.eml", + "data": "UmV0dXJuLVBhdGg6IDxvd25lci1wYzgwMEBocGMudWguZWR1PgpSZWNlaXZlZDogZnJvbSAgcmx5LXphMDQubXguYW9sLmNvbSAocmx5LXphMDQubWFpbC5hb2wuY29tIFsxNzIuMzEuMzYuMTAwXSkgYnkKICAgICAgICBhaXItemEwNC5tYWlsLmFvbC5jb20gKHY0Mi40KSB3aXRoIFNNVFA7IFdlZCwgMjkgQXByIDE5OTggMDk6MTQ6MTgKICAgICAgICAtMDQwMApSZWNlaXZlZDogZnJvbSBzaW5hLmhwYy51aC5lZHUgKFNpbmEuSFBDLlVILkVEVSBbMTI5LjcuMy41XSkKICAgICAgICAgIGJ5IHJseS16YTA0Lm14LmFvbC5jb20gKDguOC41LzguOC41L0FPTC00LjAuMCkKICAgICAgICAgIHdpdGggRVNNVFAgaWQgSkFBMjc2MjM7CiAgICAgICAgICBXZWQsIDI5IEFwciAxOTk4IDA5OjE0OjA4IC0wNDAwIChFRFQpClJlY2VpdmVkOiBmcm9tIHNpbmEuaHBjLnVoLmVkdSAobGlzdHNAU2luYS5IUEMuVUguRURVIFsxMjkuNy4zLjVdKSBieQogICAgICAgIHNpbmEuaHBjLnVoLmVkdSAoOC43LjMvOC43LjMpIHdpdGggRVNNVFAgaWQgSUFBMjUyOTQ7IFdlZCwgMjkgQXByCiAgICAgICAgMTk5OCAwODoxNDoyMyAtMDUwMCAoQ0RUKQpSZWNlaXZlZDogYnkgc2luYS5ocGMudWguZWR1IChUTEIgdjAuMDlhICgxLjIwIHRpYmJzIDE5OTYvMTAvMDkgMjI6MDM6MDcpKTsKICAgICAgICBXZWQsIDI5IEFwciAxOTk4IDA4OjE0OjIwIC0wNTAwIChDRFQpClJlY2VpdmVkOiBmcm9tIGRvbmFsZC5jeWJlcmNvbW0ubmwgKGRvbmFsZC5jeWJlcmNvbW0ubmwgWzE5NC4yMzUuMTEzLjVdKSBieQogICAgICAgIHNpbmEuaHBjLnVoLmVkdSAoOC43LjMvOC43LjMpIHdpdGggRVNNVFAgaWQgSUFBMjUyNzUgZm9yCiAgICAgICAgPHBjODAwQGhwYy51aC5lZHU+OyBXZWQsIDI5IEFwciAxOTk4IDA4OjE0OjExIC0wNTAwIChDRFQpClJlY2VpdmVkOiBmcm9tIGRlZmF1bHQgKHBvb3J0MjItaXAteDIuZW5lcnRlbC5jeWJlcmNvbW0ubmwgWzE5NC4yMzUuMTE4LjIyXSkKICAgICAgICBieSBkb25hbGQuY3liZXJjb21tLm5sICg4LjguNi84LjguNikgd2l0aCBFU01UUCBpZCBPQUEyNTY3NgogICAgICAgIGZvciA8cGM4MDBAaHBjLnVoLmVkdT47IFdlZCwgMjkgQXByIDE5OTggMTQ6MTI6MTAgLTAxMDAgKE1FVCkKTWVzc2FnZS1JZDogPDE5OTgwNDI5MTUxMi5PQUEyNTY3NkBkb25hbGQuY3liZXJjb21tLm5sPgpGcm9tOiAiRW1pbGUgTm9zc2luIiA8RW1pbGVAQ3liZXJDb21tLm5sPgpUbzogIlBDODAwIiA8cGM4MDBAaHBjLnVoLmVkdT4KU3ViamVjdDogUmU6IFBDODAwOiBUYWxsIEhvbmRhbGluZSBXaW5kc2hpZWxkIERpc3RvcnRpb24KRGF0ZTogV2VkLCAyOSBBcHIgMTk5OCAxNToxMzoyMCArMDIwMApYLU1TTWFpbC1Qcmlvcml0eTogTm9ybWFsClgtUHJpb3JpdHk6IDMKWC1NYWlsZXI6IE1pY3Jvc29mdCBJbnRlcm5ldCBNYWlsIDQuNzAuMTE1NQpTZW5kZXI6IG93bmVyLXBjODAwQGhwYy51aC5lZHUKUHJlY2VkZW5jZTogbGlzdApYLU1ham9yZG9tbzogMS45NC5qbHQ3Ck1pbWUtVmVyc2lvbjogMS4wCkNvbnRlbnQtdHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD1JU08tODg1OS0xCkNvbnRlbnQtdHJhbnNmZXItZW5jb2Rpbmc6IHF1b3RlZC1wcmludGFibGUKCkhpIFBhdCwKSSBkb24ndCBzZWUgYW55IGRpc3RvcnRpb24gaW4gbXkgdGFsbCBIb25kYSBzY3JlZW4sIG5vciBhbnkgbWFnaW5maWNhdGlvbj0KLS0KVmlzaXQgdGhlIFBDODAwIHdlYiBwYWdlIGF0IDxVUkw6aHR0cDovL21lbWJlcnMuYW9sLmNvbS93d3dwYzgwMC8+ClRvIHVuc3Vic2NyaWJlIGZyb20gdGhlIGxpc3QsIHNlbmQgInVuc3Vic2NyaWJlIHBjODAwIiBpbiB0aGUgYm9keSBvZiBhCm1lc3NhZ2UgdG8gbWFqb3Jkb21vQGhwYy51aC5lZHUuClRvIHJlcG9ydCBwcm9ibGVtcywgc2VuZCBtYWlsIHRvIHBjODAwLW93bmVyQGhwYy51aC5lZHUuCgo=", + "mimeType": "message/rfc822", + "charset": null, + "contentId": "0_893950130@inet_out.mail.aol.com.2", + "calendarMethod": null + } + ], + "mailHeaders": "From owner-funnel-pc@hpc.uh.edu Thu Apr 30 14:27:51 1998\nReceived: from farabi.hpc.uh.edu (farabi.hpc.uh.edu [129.7.102.2]) by sina.hpc.uh.edu (8.7.3/8.7.3) with ESMTP id OAA27026; Thu, 30 Apr 1998 14:27:51 -0500 (CDT)\nReceived: from sina.hpc.uh.edu (lists@[10.1.1.1]) by farabi.hpc.uh.edu (8.7.3/8.7.3) with ESMTP id WAD14645; Mon, 27 Apr 1998 22:20:19 -0500 (CDT)\nReceived: by sina.hpc.uh.edu (TLB v0.09a (1.20 tibbs 1996/10/09 22:03:07)); Thu, 30 Apr 1998 14:26:26 -0500 (CDT)\nReceived: (from tibbs@localhost) by sina.hpc.uh.edu (8.7.3/8.7.3) id OAA26968 for pc800@hpc.uh.edu; Thu, 30 Apr 1998 14:26:17 -0500 (CDT)\nReceived: from imo11.mx.aol.com (imo11.mx.aol.com [198.81.17.33]) by sina.hpc.uh.edu (8.7.3/8.7.3) with ESMTP id KAA22560 for ; Thu, 30 Apr 1998 10:29:47 -0500 (CDT)\nReceived: from Gaffneydp@aol.com\r\n by imo11.mx.aol.com (IMOv14.1) id QICDa02864\r\n for ; Thu, 30 Apr 1998 11:28:50 -0400 (EDT)\nFrom: Gaffneydp \nMessage-ID: \nDate: Thu, 30 Apr 1998 11:28:50 EDT\nTo: PC800@hpc.uh.edu\nMime-Version: 1.0\nSubject: Fwd: PC800: Tall Hondaline Windshield Distortion\nContent-type: multipart/mixed;\r\n boundary=\"part0_893950130_boundary\"\nX-Mailer: AOL 3.0 16-bit for Windows sub 41\nSender: owner-pc800@hpc.uh.edu\nPrecedence: list\nX-Majordomo: 1.94.jlt7", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/jt-0498.msg b/packages/node-mimimi/test/mimetools-testmsgs/jt-0498.msg new file mode 100644 index 00000000000..27a55a35ab9 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/jt-0498.msg @@ -0,0 +1,107 @@ +From owner-funnel-pc@hpc.uh.edu Thu Apr 30 14:27:51 1998 +Received: from farabi.hpc.uh.edu (farabi.hpc.uh.edu [129.7.102.2]) by sina.hpc.uh.edu (8.7.3/8.7.3) with ESMTP id OAA27026; Thu, 30 Apr 1998 14:27:51 -0500 (CDT) +Received: from sina.hpc.uh.edu (lists@[10.1.1.1]) by farabi.hpc.uh.edu (8.7.3/8.7.3) with ESMTP id WAD14645; Mon, 27 Apr 1998 22:20:19 -0500 (CDT) +Received: by sina.hpc.uh.edu (TLB v0.09a (1.20 tibbs 1996/10/09 22:03:07)); Thu, 30 Apr 1998 14:26:26 -0500 (CDT) +Received: (from tibbs@localhost) by sina.hpc.uh.edu (8.7.3/8.7.3) id OAA26968 for pc800@hpc.uh.edu; Thu, 30 Apr 1998 14:26:17 -0500 (CDT) +Received: from imo11.mx.aol.com (imo11.mx.aol.com [198.81.17.33]) by sina.hpc.uh.edu (8.7.3/8.7.3) with ESMTP id KAA22560 for ; Thu, 30 Apr 1998 10:29:47 -0500 (CDT) +Received: from Gaffneydp@aol.com + by imo11.mx.aol.com (IMOv14.1) id QICDa02864 + for ; Thu, 30 Apr 1998 11:28:50 -0400 (EDT) +From: Gaffneydp +Message-ID: +Date: Thu, 30 Apr 1998 11:28:50 EDT +To: PC800@hpc.uh.edu +Mime-Version: 1.0 +Subject: Fwd: PC800: Tall Hondaline Windshield Distortion +Content-type: multipart/mixed; + boundary="part0_893950130_boundary" +X-Mailer: AOL 3.0 16-bit for Windows sub 41 +Sender: owner-pc800@hpc.uh.edu +Precedence: list +X-Majordomo: 1.94.jlt7 + +This is a multi-part message in MIME format. + +--part0_893950130_boundary +Content-ID: <0_893950130@inet_out.mail.aol.com.1> +Content-type: text/plain; charset=US-ASCII + +Hello to all, + +I purchased a tall windshield about two weeks ago from Waynesville Cycle +Center in NC. The entire length and width of the windshield has optical waves +or ripples which distort the view through it. It was very uncomfortable to +ride with and I would be afraid to ride at night with it. + +I contacted the manager at WCC. He in turn contacted Honda Customer Service +(310-532-9811). They replied to him that there were no bulletins concerning +this problem and that they inspected several windshields. They claim that all +the windshields had distortions. They have offered to give me a full refund. + +What bothers me is two things. First, the stock windshield has no optical +distortion. Second, it appears that Honda knows that it is selling a less +than perfect product and is apparently unconcerned about it (seems like a +strange way to do business). + +Perhaps my windshield is the worst one ever made, but they made no offer to +inspect mine and compare to others that they have in stock. + +I am going to call Honda on Monday and raise the issues of safety and quality. +I will ask them if they have a problem with me forwarding their position to +publications such as Cycle World, Rider, etc. + +Dennis Gaffney +Marlboro, NY +gaffneydp@aol.com +1994 PC800 +Bought used in 1997 (2000 miles) +Modifications: tall windshield? + +--part0_893950130_boundary +Content-ID: <0_893950130@inet_out.mail.aol.com.2> +Content-type: message/rfc822 +Content-transfer-encoding: 7bit +Content-disposition: inline + +Return-Path: +Received: from rly-za04.mx.aol.com (rly-za04.mail.aol.com [172.31.36.100]) by + air-za04.mail.aol.com (v42.4) with SMTP; Wed, 29 Apr 1998 09:14:18 + -0400 +Received: from sina.hpc.uh.edu (Sina.HPC.UH.EDU [129.7.3.5]) + by rly-za04.mx.aol.com (8.8.5/8.8.5/AOL-4.0.0) + with ESMTP id JAA27623; + Wed, 29 Apr 1998 09:14:08 -0400 (EDT) +Received: from sina.hpc.uh.edu (lists@Sina.HPC.UH.EDU [129.7.3.5]) by + sina.hpc.uh.edu (8.7.3/8.7.3) with ESMTP id IAA25294; Wed, 29 Apr + 1998 08:14:23 -0500 (CDT) +Received: by sina.hpc.uh.edu (TLB v0.09a (1.20 tibbs 1996/10/09 22:03:07)); + Wed, 29 Apr 1998 08:14:20 -0500 (CDT) +Received: from donald.cybercomm.nl (donald.cybercomm.nl [194.235.113.5]) by + sina.hpc.uh.edu (8.7.3/8.7.3) with ESMTP id IAA25275 for + ; Wed, 29 Apr 1998 08:14:11 -0500 (CDT) +Received: from default (poort22-ip-x2.enertel.cybercomm.nl [194.235.118.22]) + by donald.cybercomm.nl (8.8.6/8.8.6) with ESMTP id OAA25676 + for ; Wed, 29 Apr 1998 14:12:10 -0100 (MET) +Message-Id: <199804291512.OAA25676@donald.cybercomm.nl> +From: "Emile Nossin" +To: "PC800" +Subject: Re: PC800: Tall Hondaline Windshield Distortion +Date: Wed, 29 Apr 1998 15:13:20 +0200 +X-MSMail-Priority: Normal +X-Priority: 3 +X-Mailer: Microsoft Internet Mail 4.70.1155 +Sender: owner-pc800@hpc.uh.edu +Precedence: list +X-Majordomo: 1.94.jlt7 +Mime-Version: 1.0 +Content-type: text/plain; charset=ISO-8859-1 +Content-transfer-encoding: quoted-printable + +Hi Pat, +I don't see any distortion in my tall Honda screen, nor any maginfication= +-- +Visit the PC800 web page at +To unsubscribe from the list, send "unsubscribe pc800" in the body of a +message to majordomo@hpc.uh.edu. +To report problems, send mail to pc800-owner@hpc.uh.edu. + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/lennie-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/lennie-expected.json new file mode 100644 index 00000000000..4b75a8d5535 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/lennie-expected.json @@ -0,0 +1,51 @@ +{ + "exception": null, + "result": { + "id": "33D89532.29EA@atl.mindspring.com", + "boundary": "------------52E03A8932B4", + "alternativeBoundary": null, + "sender": { + "name": "Lennie Jarratt", + "mailAddress": "lbj_ccsi@mindspring.com", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "lbj_ccsi@atl.mindspring.com", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [ + { + "name": "", + "mailAddress": "lbj_ccsi@mindspring.com", + "valid": true + } + ], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 869831986000, + "subject": "Test Mail Again", + "plainBodyText": null, + "htmlBodyText": "This is the message body.

A picture should also be displayed.

\n\n\n\nClient Pull Rolling Page Demo>\n\n\n\n\n\n\nThis is Page 3 of my rolling web page demo.\n\n\n\n", + "attachedMessages": [], + "attachedFiles": [ + { + "name": "WWWIcon.gif", + "data": "R0lGODdhPAA8AOYAAP///+/v7+fv7+/39/f//9be3r3O1rXGzsbW3qW1va29xoyltXOMnGuElClSa4ScrXuUpUJje1p7lFJzjEprhDlacxhCYylScyFKaxA5WggxUgApSt7n773GzpytvZSltWuEnM7W3sbO1q29zqW1xoycrYSUpWN7lFpzjFJrhEJjhDlaezFScylKayFCYwgxWgApUtbe5yFCaxg5YxAxWggpUpSlvXOEnFpzlFJrjEpjhEJaezlSc3uMpYyctYSUrb3G1sbO3qWtvbW9zq21xufn79bW3t7e5wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAPAA8AAAH/4AAgoOEhYaHiImKi4yNjo+QkZKTlJWWl5iZmpucnZ6foKGinSIfJxEtMzMtEScfIqOKRwsUMy4YLQ4sLA4tGC4zFAtHsYQBCysWLSwrKhQ5Eyg5FCorLC0WKwsBxUAoyhU6KA0MEA8lJQ8QDCcoOhXYKECjQhUyLDo4DA8fQkRDAIcQEfLhAQMcOljIqCAklI0WLVZMAPHAA5EDBoKE2BgCgYEDRDw8ADFhBUQbnzxgwLACBQMfCYYAQRCiQIybMYx0NDAkgQ8GKFas9NCpAwsLLChQ9KAAowiaHDcGEfFRgUgQFI6y6LBJQA4aLnjkAPHjgwcSRBSMGLI24AgFRP9IePjwA0SOCi5o5BCgqQQMsEkl3Ohh4seDc+gSJz5sAsKNE1nzwiiRKUYLGBoy3Nuhg0KKHNAmiB6NYjRoCjp2KMygAUaLGJj8woDxIoMFGRBz6drFu7eDXhAxWMjwYvZkTCuMY35BI4Pz59CjR6fxorXxFZg4bNgxCIYMQydqwALwQ0OCQkVgROjuQtOGE4NccBc0/gcMDoKEwBg/HgAM+ILIpwkLJgyyA4AAjCDICBsMYsAG+CU4CIEGInjJDkQJckKBABjAoQHtCaKdgQZmCMCGmphgwCAmKNghgvMJwoKBEarIoouYmBDCIAusCMACMaZAiJCC0OCjjjz6iIn/j/gpaQINDgIQoZIbLCAIkx1usmOWWwKQQoOC7OjjliFswOGWK3a5iYUA7LCBkj+q+WCMGoJCJwA0bIAjADbyuMGMhNypyYOFbGBmoBwKYoKhhBDqyQgwDDAIB7PdQAgLlg5ywmySLhjpIASkuN8gIsy2XnenChLBbP3ZB+clshlXgwYvMEcDdbXmquutuGpQg3LHXWKZcbRmMIMFFriA7LLMLqusBTMQZ51rsMU2Ww21WYBLb7v89hu318gw3Au/BouJABRgRoO2LFSwQwTwxiuvvDtUwIJwNLRGAV+adNCCBjTMgAEzEajQGQUIJ5ywDipEYC8GM+TbAleceDBDkmbKtPuuDgcrzHEEO1iDDWszmMjJBxcb68IyzKyww7sgh2zNNcCQ/EEoHvxb27G4QbQbcC2IG211LZj8SQcpYEZrc6ocC60qGVCnQWspUDyKACVcIOvUXHNdLgwXlMBvMQDEUMIOwKYNww4lVEt2IQaUgAILM8w2AwsolPDq23z37fffgAcu+OCEF2744YgnXnggADs=", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "Return-Path: \nReceived: from brickbat8.mindspring.com (brickbat8.mindspring.com [207.69.200.11])\r\n by camel10.mindspring.com (8.8.5/8.8.5) with ESMTP id GAA27894\r\n for ; Fri, 25 Jul 1997 06:58:07 -0400 (EDT)\nReceived: from lennie (user-2k7i8oq.dialup.mindspring.com [168.121.35.26])\r\n by brickbat8.mindspring.com (8.8.5/8.8.5) with SMTP id GAA22488\r\n for ; Fri, 25 Jul 1997 06:58:05 -0400 (EDT)\nMessage-ID: <33D89532.29EA@atl.mindspring.com>\nDate: Fri, 25 Jul 1997 06:59:46 -0500\nFrom: Lennie Jarratt \nReply-To: lbj_ccsi@mindspring.com\nOrganization: Custom Computer Services Inc.\nX-Mailer: Mozilla 3.01Gold (Win95; I)\nMIME-Version: 1.0\nTo: lbj_ccsi@atl.mindspring.com\nSubject: Test Mail Again\nContent-Type: multipart/mixed; boundary=\"------------52E03A8932B4\"", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/lennie.msg b/packages/node-mimimi/test/mimetools-testmsgs/lennie.msg new file mode 100644 index 00000000000..d5f6d3d577c --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/lennie.msg @@ -0,0 +1,84 @@ +Return-Path: +Received: from brickbat8.mindspring.com (brickbat8.mindspring.com [207.69.200.11]) + by camel10.mindspring.com (8.8.5/8.8.5) with ESMTP id GAA27894 + for ; Fri, 25 Jul 1997 06:58:07 -0400 (EDT) +Received: from lennie (user-2k7i8oq.dialup.mindspring.com [168.121.35.26]) + by brickbat8.mindspring.com (8.8.5/8.8.5) with SMTP id GAA22488 + for ; Fri, 25 Jul 1997 06:58:05 -0400 (EDT) +Message-ID: <33D89532.29EA@atl.mindspring.com> +Date: Fri, 25 Jul 1997 06:59:46 -0500 +From: Lennie Jarratt +Reply-To: lbj_ccsi@mindspring.com +Organization: Custom Computer Services Inc. +X-Mailer: Mozilla 3.01Gold (Win95; I) +MIME-Version: 1.0 +To: lbj_ccsi@atl.mindspring.com +Subject: Test Mail Again +Content-Type: multipart/mixed; boundary="------------52E03A8932B4" + +This is a multi-part message in MIME format. + +--------------52E03A8932B4 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +This is the message body. + +A picture should also be displayed. + + +--------------52E03A8932B4 +Content-Type: text/html; charset=us-ascii; name="Pull3.html" +Content-Transfer-Encoding: 7bit +Content-Disposition: inline; filename="Pull3.html" +Content-Base: "file:///E|/ChckFree/Html/Pull3.html" + + + + + +Client Pull Rolling Page Demo> + + + + + + +This is Page 3 of my rolling web page demo. + + + + +--------------52E03A8932B4 +Content-Type: image/gif; name="WWWIcon.gif" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="WWWIcon.gif" + +R0lGODdhPAA8AOYAAP///+/v7+fv7+/39/f//9be3r3O1rXGzsbW3qW1va29xoyltXOMnGuE +lClSa4ScrXuUpUJje1p7lFJzjEprhDlacxhCYylScyFKaxA5WggxUgApSt7n773GzpytvZSl +tWuEnM7W3sbO1q29zqW1xoycrYSUpWN7lFpzjFJrhEJjhDlaezFScylKayFCYwgxWgApUtbe +5yFCaxg5YxAxWggpUpSlvXOEnFpzlFJrjEpjhEJaezlSc3uMpYyctYSUrb3G1sbO3qWtvbW9 +zq21xufn79bW3t7e5wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAPAA8AAAH/4AAgoOEhYaHiImKi4yNjo+QkZKTlJWW +l5iZmpucnZ6foKGinSIfJxEtMzMtEScfIqOKRwsUMy4YLQ4sLA4tGC4zFAtHsYQBCysWLSwr +KhQ5Eyg5FCorLC0WKwsBxUAoyhU6KA0MEA8lJQ8QDCcoOhXYKECjQhUyLDo4DA8fQkRDAIcQ +EfLhAQMcOljIqCAklI0WLVZMAPHAA5EDBoKE2BgCgYEDRDw8ADFhBUQbnzxgwLACBQMfCYYA +QRCiQIybMYx0NDAkgQ8GKFas9NCpAwsLLChQ9KAAowiaHDcGEfFRgUgQFI6y6LBJQA4aLnjk +APHjgwcSRBSMGLI24AgFRP9IePjwA0SOCi5o5BCgqQQMsEkl3Ohh4seDc+gSJz5sAsKNE1nz +wiiRKUYLGBoy3Nuhg0KKHNAmiB6NYjRoCjp2KMygAUaLGJj8woDxIoMFGRBz6drFu7eDXhAx +WMjwYvZkTCuMY35BI4Pz59CjR6fxorXxFZg4bNgxCIYMQydqwALwQ0OCQkVgROjuQtOGE4Nc +cBc0/gcMDoKEwBg/HgAM+ILIpwkLJgyyA4AAjCDICBsMYsAG+CU4CIEGInjJDkQJckKBABjA +oQHtCaKdgQZmCMCGmphgwCAmKNghgvMJwoKBEarIoouYmBDCIAusCMACMaZAiJCC0OCjjjz6 +iIn/j/gpaQINDgIQoZIbLCAIkx1usmOWWwKQQoOC7OjjliFswOGWK3a5iYUA7LCBkj+q+WCM +GoJCJwA0bIAjADbyuMGMhNypyYOFbGBmoBwKYoKhhBDqyQgwDDAIB7PdQAgLlg5ywmySLhjp +IASkuN8gIsy2XnenChLBbP3ZB+clshlXgwYvMEcDdbXmquutuGpQg3LHXWKZcbRmMIMFFriA +7LLMLqusBTMQZ51rsMU2Ww21WYBLb7v89hu318gw3Au/BouJABRgRoO2LFSwQwTwxiuvvDtU +wIJwNLRGAV+adNCCBjTMgAEzEajQGQUIJ5ywDipEYC8GM+TbAleceDBDkmbKtPuuDgcrzHEE +O1iDDWszmMjJBxcb68IyzKyww7sgh2zNNcCQ/EEoHvxb27G4QbQbcC2IG211LZj8SQcpYEZr +c6ocC60qGVCnQWspUDyKACVcIOvUXHNdLgwXlMBvMQDEUMIOwKYNww4lVEt2IQaUgAILM8w2 +AwsolPDq23z37fffgAcu+OCEF2744YgnXnggADs= +--------------52E03A8932B4-- + + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/mp-msg-rfc822-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/mp-msg-rfc822-expected.json new file mode 100644 index 00000000000..b2a0ada4c2d --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/mp-msg-rfc822-expected.json @@ -0,0 +1,55 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "------------70522FC73543", + "alternativeBoundary": null, + "sender": { + "name": "Juergen Specht", + "mailAddress": "specht@kulturbox.de", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "andreas.koenig@mind.de", + "valid": true + }, + { + "name": "", + "mailAddress": "kun@pop.combox.de", + "valid": true + }, + { + "name": "", + "mailAddress": "101762.2307@compuserve.com", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 835252517000, + "subject": "[Fwd: Re: 34Mbit/s Netz]", + "plainBodyText": "-- \nJuergen Specht - KULTURBOX\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "Re_ 34Mbit_s Netz.eml", + "data": "WC1QT1AzLVJjcHQ6IHNwZWNodEB0cmFjaGVhClJldHVybi1QYXRoOiBoZXJtZXMKUmVjZWl2ZWQ6IChmcm9tIGhlcm1lc0Bsb2NhbGhvc3QpIGJ5IGt1bHR1cmJveC5uZXRtYnguZGUgKDguNy4xLzguNy4xKSBpZCBTQUEwNDUxMyBmb3Igc3BlY2h0OyBXZWQsIDE5IEp1biAxOTk2IDE4OjMwOjEyICswMjAwClJlY2VpdmVkOiBieSBuZXRtYngubmV0bWJ4LmRlICgvXD09L1wgU21haWwzLjEuMjguMSkKCSAgZnJvbSBtYWlsLmNzLnR1LWJlcmxpbi5kZSB3aXRoIHNtdHAKCSAgaWQgPG0wdVdQck8tMDAwNHdwQz47IFdlZCwgMTkgSnVuIDk2IDE4OjEyIE1FUwpSZWNlaXZlZDogKGZyb20gbm9ib2R5QGxvY2FsaG9zdCkgYnkgbWFpbC5jcy50dS1iZXJsaW4uZGUgKDguNi4xMi84LjYuMTIpIGlkIFNBQTEyNDEzOyBXZWQsIDE5IEp1biAxOTk2IDE4OjI2OjI4ICswMjAwClJlc2VudC1EYXRlOiBXZWQsIDE5IEp1biAxOTk2IDE4OjI2OjI4ICswMjAwClJlc2VudC1NZXNzYWdlLUlkOiA8MTk5NjA2MTkxNjI2LlNBQTEyNDEzQG1haWwuY3MudHUtYmVybGluLmRlPgpSZXNlbnQtRnJvbTogbm9ib2R5QGNzLnR1LWJlcmxpbi5kZQpSZXNlbnQtVG86IGt1bHR1ckBrdWx0dXJib3gubmV0bWJ4LmRlClJlY2VpdmVkOiBmcm9tIGdhdGVrZWVwZXIudGVsZWtvbS5kZSAoWzE5NC4yNS4xNS4xMV0pIGJ5IG1haWwuY3MudHUtYmVybGluLmRlICg4LjYuMTIvOC42LjEyKSB3aXRoIFNNVFAgaWQgU0FBMTE2NzggZm9yIDxzcGVjaHRAa3VsdHVyYm94LmRlPjsgV2VkLCAxOSBKdW4gMTk5NiAxODoxMToyOSArMDIwMApSZWNlaXZlZDogZnJvbSBVTE0wMi5tbmgudGVsZWtvbS5kZSBieSBnYXRla2VlcGVyLnRlbGVrb20uZGU7ICg1LjY1djMuMC8xLjEuOC4yLzAyQXVnOTUtMDEzMlBNKQoJaWQgQUEwMTM3NjsgV2VkLCAxOSBKdW4gMTk5NiAxODoxMToyNyArMDIwMApSZWNlaXZlZDogZnJvbSB1bG0wMi5tbmgudGVsZWtvbS5kZSAoZGV1c2NobGVAbW5oLnRlbGVrb20uZGUpIGJ5IFVMTTAyLm1uaC50ZWxla29tLmRlICg4LjYuMTAvMykgd2l0aCBTTVRQIGlkIFNBQTMwNjgwIGZvciA8c3BlY2h0QGt1bHR1cmJveC5kZT47IFdlZCwgMTkgSnVuIDE5OTYgMTg6MTQ6NDAgR01UCk1lc3NhZ2UtSWQ6IDwxOTk2MDYxOTE4MTQuU0FBMzA2ODBAVUxNMDIubW5oLnRlbGVrb20uZGU+ClgtU2VuZGVyOiBkZXVzY2hsZUB1bG0wMi5tbmgudGVsZWtvbS5kZQpYLU1haWxlcjogV2luZG93cyBFdWRvcmEgVmVyc2lvbiAxLjQuNApNaW1lLVZlcnNpb246IDEuMApDb250ZW50LVR5cGU6IHRleHQvcGxhaW47IGNoYXJzZXQ9Imlzby04ODU5LTEiCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IHF1b3RlZC1wcmludGFibGUKRGF0ZTogV2VkLCAxOSBKdW4gMTk5NiAxODoxMjowMiArMDIwMApUbzogSnVlcmdlbiBTcGVjaHQgPHNwZWNodEBrdWx0dXJib3guZGU+CkZyb206IGRldXNjaGxlQG1uaC50ZWxla29tLmRlIChHdWVudGVyIERldXNjaGxlKQpTdWJqZWN0OiBSZTogMzRNYml0L3MgTmV0egpYLU1vemlsbGEtU3RhdHVzOiAwMDExCgpIYWxsbyBIZXJyIFNwZWNodCwKZW50c2NodWxkaWdlbiBTaWUgdm9yYWIsIGRhc3MgaWNoIElobmVuIG5pY2h0IHRlbGVmb25pc2NoIHp1ciBWZXJmdWVndW5nCnN0ZWhlLCBpY2ggUHJhZXNlbnRhdGlvbmVuIGdlaGFsdGVuLyBub2NoIHp1IGhhbHRlbiB1bmQgdmllbGUKS3VuZGVubmFjaGZyYWdlbiB6dSBwcm9qZWt0aWVyZW4uIE5hY2ggSW5mb3JtYXRpb25lbiBkZXMgUHJvZHVrdC1NYW5hZ2VycwpUZW1tZSBzdGVodCBkZXIgUE9QIHNjaG9uIHp1ciBWZXJmPUZDZ3VuZyEgU3RhbmRvcnQ6IHZvcmF1c3NpY2h0bGljaDoKV2ludGVyZmVsZHN0ci4gMjEsIDEwNzgxIEJlcmxpbi4KRGVyIFBPUCBoYXQgenVyIFplaXQgZGlyZWt0ZSAzNE0tQW5iaW5kdW5nZW4genUgZm9sZ2VuZGVuIE9ydGVuOiBSb3N0b2NrLApIYW1idXJnLCBIYW5ub3ZlciAmIExlaXB6aWcuIDQgd2VpdGVyZSB3ZXJkZW4gaW4ga3VlcnplIGluIEJldHJpZWIgZ2VoZW4uCkRhbWl0IGhhYmVuIFNpZSBlaW5lbiBCZXNvbmRlcmVuIFNpY2hlcmhlaXRzc3RhbmRhcmQgdmVyZnVlZ2JhciEKS29udGFrdCBtdWVzc2VuIFNpZSB1ZWJlciBJaHJlIG9lcmx0bGljaGUgVmVydHJpZWJzZWluaGVpdCBhdWZuZWhtZW46CmVudHdlZGVyIGRlbiBHZXNjaGFlZnRzLUt1bmRlbi1WZXJ0cmllYiBvZGVyIGRhcyBHcm9zc0t1bmRlbk1hbmFnZW1lbnQuCkRpZXNlIFZlcnRyaWVic2VpbmhlaXRlbiBncmVpZmVuIGF1ZiBkZW4gb2VydGxpY2hlbiBUZWNobmlzY2hlbgpWZXJ0cmllYnMtU3VwcG9ydCB6dS4gRGllIEluZm9ybWF0aW9uZW4gd2VyZGVuIHVlYmVyIFRWUyB6dXIgVmVydHJpZWJzZWloZWl0CmdlZ2ViZW4gdW5kIGRhbm4genUgSWhuZW4uCiBTaWUgYmVub2V0aWdlbiBlaW5lIFN0YW5kbGVpdHVuZyB2b24gSWhyZXIgTG9rYXRpb24genVtIEludGVybmV0LVBPUApVZWJlcmdhYmVwdW5rdCB6dSBJaHJlbSBJbmZvLVNlcnZlciBpc3QgZWluIENJU0NPIDEwMDAtUm91dGVyLiBEYW5uIHphaGxlbgpTaWUgbmViZW4gZGVuIG1vbmF0bGljaGVuIEtvc3RlbiBmdWVyIGRpZSBTdGFuZGxlaXR1bmcgZGllIEtvc3RlbiBmdWVyIGRlbgpJbnRlcm5ldC1adWdhbmc6IHpCIGJlaSA2NGs6IDE1MDBETSBiZWkgMkdCeXRlIEZyZWl2b2x1bWVuLiAxMjhLOiAzMDAwIERNCmJlaSA1IEdCIEZyZWl2b2x1bWVuICYgMk06IDMwLjAwMCBETSBiZWkgNTBHQiBGcmVpdm9sdW1lbi4KRnJldW5kbGljaGUgR3J1ZXNzZT0yMApHdWVudGVyIERldXNjaGxlCgoKPlNlaHIgZ2VlaHJ0ZXIgSGVyciBEZXVzY2hsZSwKPlNpZSBzaW5kIG1pciB2b24gSGVycm4gTWV5ZW5kcmllc2NoIGVtcGZvaGxlbiB3b3JkZW4uCj5JY2ggdmVyc3VjaGUgSW5mb3JtYXRpb25lbiB1ZWJlciBkYXMgVC1laWdlbmUgMzRNYml0L3MgTmV0eiB1bmQgZGVuPTIwCj5sb2thbGVuIFBvcC1CZXJsaW4gcmF1c3p1ZmluZGVuLCBiencuIHdhcyBlaW4gQW5zY2hsdXNzIGtvc3RldCB1bmQ9MjAKPndvIG1hbiBpaG4gaGVyYmVrb21tdC4gTGF1dCBIZXJybiBTY2huaWNrIGluIEJlcmxpbiBnaWJ0IGVzIGRlbj0yMAo+VC1Qb3AgbmljaHQsIGxhdXQgVHJhY2Vyb3V0ZSB2b24gSGVycm4gTWV5ZW5kcmllc2NoIHNlaHJ3b2hsLiBBdWNoPTIwCj5pc3QgZGllcyBOZXR6IGluIGRlciBJWCB2b20gTWFpIDk2IGVyd2FlaG50Lgo+S29lbm5lbiBTaWUgbWlyIGhlbGZlbj8KPgo+TWZHCj4tLT0yMAo+SnVlcmdlbiBTcGVjaHQgLSBLVUxUVVJCT1gKPgo+Cgo9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9M0Q9Cj0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0KPTNECkRpcGwuLUluZy4gIEd1ZW50ZXIgICAgIEQgRSBVIFMgQyBIIEwgRQpEZXV0c2NoZSBUZWxla29tIEFHICAgICBOaWVkZXJsYXNzdW5nIDMgSGFubm92ZXIKR3Jvc3NLdW5kZW5NYW5hZ2VtZW50IC0gVGVjaG4uIFZlcnRyaWVicy1TdXBwb3J0OgpUZWFtLUxlaXRlciAgICAgICAgICAgICBJbnRlcm5ldCBPbmxpbmUtRGllbnN0ZQotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KR3JLTS1UVlMtSU9EICAgICAgICAgICBUZWw6ICs0OS01MTEtMzMzLTI3NzIKVmFocmVud2FsZGVyLVN0ci4gMjQ1ICBGQVg6ICs0OS01MTEtMzMzLTI3NTEKMzAxNzkgSGFubm92ZXIgICAgICAgZU1haWw6IGRldXNjaGxlQG1uaC50ZWxla29tLmRlPTIwCj0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0zRD0KPTNEPTNEPTNEPTNEPTNEPTNEPTNEPTNEPTNEPTNEPTNEPTNEPTNEPTNEPTNEPTNEPTNEPTNEPTNEPTNEPTNEPTNEPTNEPTNEPTNEPQo9M0QKCgoK", + "mimeType": "message/rfc822", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "From specht@kulturbox.de Thu Jun 20 08:35:23 1996\nDate: Thu, 20 Jun 1996 08:35:17 +0200\nFrom: Juergen Specht \nOrganization: KULTURBOX\nX-Mailer: Mozilla 2.02 (WinNT; I)\nMIME-Version: 1.0\nTo: andreas.koenig@mind.de, kun@pop.combox.de, 101762.2307@compuserve.com\nSubject: [Fwd: Re: 34Mbit/s Netz]\nContent-Type: multipart/mixed; boundary=\"------------70522FC73543\"\nX-Filter: mailagent [version 3.0 PL44] for k@.in-berlin.de", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/mp-msg-rfc822.msg b/packages/node-mimimi/test/mimetools-testmsgs/mp-msg-rfc822.msg new file mode 100644 index 00000000000..fc26fcd4bdf --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/mp-msg-rfc822.msg @@ -0,0 +1,112 @@ +From specht@kulturbox.de Thu Jun 20 08:35:23 1996 +Date: Thu, 20 Jun 1996 08:35:17 +0200 +From: Juergen Specht +Organization: KULTURBOX +X-Mailer: Mozilla 2.02 (WinNT; I) +MIME-Version: 1.0 +To: andreas.koenig@mind.de, kun@pop.combox.de, 101762.2307@compuserve.com +Subject: [Fwd: Re: 34Mbit/s Netz] +Content-Type: multipart/mixed; boundary="------------70522FC73543" +X-Filter: mailagent [version 3.0 PL44] for k@.in-berlin.de + +This is a multi-part message in MIME format. + +--------------70522FC73543 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +-- +Juergen Specht - KULTURBOX + +--------------70522FC73543 +Content-Type: message/rfc822 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +X-POP3-Rcpt: specht@trachea +Return-Path: hermes +Received: (from hermes@localhost) by kulturbox.netmbx.de (8.7.1/8.7.1) id SAA04513 for specht; Wed, 19 Jun 1996 18:30:12 +0200 +Received: by netmbx.netmbx.de (/\==/\ Smail3.1.28.1) + from mail.cs.tu-berlin.de with smtp + id ; Wed, 19 Jun 96 18:12 MES +Received: (from nobody@localhost) by mail.cs.tu-berlin.de (8.6.12/8.6.12) id SAA12413; Wed, 19 Jun 1996 18:26:28 +0200 +Resent-Date: Wed, 19 Jun 1996 18:26:28 +0200 +Resent-Message-Id: <199606191626.SAA12413@mail.cs.tu-berlin.de> +Resent-From: nobody@cs.tu-berlin.de +Resent-To: kultur@kulturbox.netmbx.de +Received: from gatekeeper.telekom.de ([194.25.15.11]) by mail.cs.tu-berlin.de (8.6.12/8.6.12) with SMTP id SAA11678 for ; Wed, 19 Jun 1996 18:11:29 +0200 +Received: from ULM02.mnh.telekom.de by gatekeeper.telekom.de; (5.65v3.0/1.1.8.2/02Aug95-0132PM) + id AA01376; Wed, 19 Jun 1996 18:11:27 +0200 +Received: from ulm02.mnh.telekom.de (deuschle@mnh.telekom.de) by ULM02.mnh.telekom.de (8.6.10/3) with SMTP id SAA30680 for ; Wed, 19 Jun 1996 18:14:40 GMT +Message-Id: <199606191814.SAA30680@ULM02.mnh.telekom.de> +X-Sender: deuschle@ulm02.mnh.telekom.de +X-Mailer: Windows Eudora Version 1.4.4 +Mime-Version: 1.0 +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable +Date: Wed, 19 Jun 1996 18:12:02 +0200 +To: Juergen Specht +From: deuschle@mnh.telekom.de (Guenter Deuschle) +Subject: Re: 34Mbit/s Netz +X-Mozilla-Status: 0011 + +Hallo Herr Specht, +entschuldigen Sie vorab, dass ich Ihnen nicht telefonisch zur Verfuegung +stehe, ich Praesentationen gehalten/ noch zu halten und viele +Kundennachfragen zu projektieren. Nach Informationen des Produkt-Managers +Temme steht der POP schon zur Verf=FCgung! Standort: voraussichtlich: +Winterfeldstr. 21, 10781 Berlin. +Der POP hat zur Zeit direkte 34M-Anbindungen zu folgenden Orten: Rostock, +Hamburg, Hannover & Leipzig. 4 weitere werden in kuerze in Betrieb gehen. +Damit haben Sie einen Besonderen Sicherheitsstandard verfuegbar! +Kontakt muessen Sie ueber Ihre oerltliche Vertriebseinheit aufnehmen: +entweder den Geschaefts-Kunden-Vertrieb oder das GrossKundenManagement. +Diese Vertriebseinheiten greifen auf den oertlichen Technischen +Vertriebs-Support zu. Die Informationen werden ueber TVS zur Vertriebseiheit +gegeben und dann zu Ihnen. + Sie benoetigen eine Standleitung von Ihrer Lokation zum Internet-POP +Uebergabepunkt zu Ihrem Info-Server ist ein CISCO 1000-Router. Dann zahlen +Sie neben den monatlichen Kosten fuer die Standleitung die Kosten fuer den +Internet-Zugang: zB bei 64k: 1500DM bei 2GByte Freivolumen. 128K: 3000 DM +bei 5 GB Freivolumen & 2M: 30.000 DM bei 50GB Freivolumen. +Freundliche Gruesse=20 +Guenter Deuschle + + +>Sehr geehrter Herr Deuschle, +>Sie sind mir von Herrn Meyendriesch empfohlen worden. +>Ich versuche Informationen ueber das T-eigene 34Mbit/s Netz und den=20 +>lokalen Pop-Berlin rauszufinden, bzw. was ein Anschluss kostet und=20 +>wo man ihn herbekommt. Laut Herrn Schnick in Berlin gibt es den=20 +>T-Pop nicht, laut Traceroute von Herrn Meyendriesch sehrwohl. Auch=20 +>ist dies Netz in der IX vom Mai 96 erwaehnt. +>Koennen Sie mir helfen? +> +>MfG +>--=20 +>Juergen Specht - KULTURBOX +> +> + +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= +=3D +Dipl.-Ing. Guenter D E U S C H L E +Deutsche Telekom AG Niederlassung 3 Hannover +GrossKundenManagement - Techn. Vertriebs-Support: +Team-Leiter Internet Online-Dienste +--------------------------------------------------- +GrKM-TVS-IOD Tel: +49-511-333-2772 +Vahrenwalder-Str. 245 FAX: +49-511-333-2751 +30179 Hannover eMail: deuschle@mnh.telekom.de=20 +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= +=3D + + + + +--------------70522FC73543-- + + + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-2evil-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/multi-2evil-expected.json new file mode 100644 index 00000000000..3b0285104e5 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-2evil-expected.json @@ -0,0 +1,53 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "------------299A70B339B65A93542D2AE", + "alternativeBoundary": null, + "sender": { + "name": "Eryq", + "mailAddress": "eryq@rhine.gsfc.nasa.gov", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "john-bigboote@eryq.pr.mcs.net", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 829203030000, + "subject": "Two images for you...", + "plainBodyText": "When unpacked, this message should produce two GIF files:\r\n\r\n\t* The 1st should be called \"3d-compress.gif\"\r\n\t* The 2nd should be called \"3d-eye.gif\"\r\n\r\nDifferent ways of specifying the filenames have been used.\r\n\r\n-- \r\n ____ __\r\n / __/__________/_/ Eryq (eryq@rhine.gsfc.nasa.gov)\r\n / __/ _/ / / , / Hughes STX Corporation, NASA/Goddard\r\n/___/_/ \\ /\\ /___ \r\n /_/ /_____/ http://selsvr.stx.com/~eryq/\r\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "_evil_because_ofpath3d-=_ISO-8859-1_Q_=63_=om=_US-ASCII_EN_Q_pr_=ess.gif", + "data": "R0lGODdhKAAoAOMAAAAAAAAAgB6Q/y9PT25ubnCAkKBSLb6+vufn5/Xes/+lAP/6zQAAAAAAAAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJLMOyYbcoxkaZ5oCkoH6L5wLMfiWqd4btZhmxbAoFCY47EIqMJgyWw2ATjj7aRkAq5YwDMl9VGtKO0SiuoiTVlscsxt9c4HgXxUIA0EAVOVfDKT8Hl1B3kDAYYle202XnGGgoMHhYckiWVuR3+OTgCGeZRslotwgJ2lnYigfZdTjQULr7ALBZN0qTurjHgLKAu0B5Wqopm7J72etQN8t8Ijury+wMtvw8/Hv7Ylfs0BxCbGqMmK0yOOQ0GTCgrR2bhwJGlXJQYG6mMKoeNoWSbzCWIACe5JwxQm3AkDAbUAQCiQhDZEBeBl6afgCsOBrD45edIvQceGWSMevpOYhl6CkydBHhBZQmGKjihVshypjB9ClAHZMTugzOU7mzhBPiSZ5uDNnA7b/aTZ0mhMnfl0pDBFa6bUElSPWb0qtYuHrxlwcR17YsWMs2jTql3LFkQEADs=", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + }, + { + "name": "3d-eye-is-an-evil-filename because of excessive length and verbosity. Unfortunately what can we do given an idiotic situation such as this_", + "data": "R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7+3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatLrU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJvs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjwE0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "Return-Path: eryq@rhine.gsfc.nasa.gov\nSender: john-bigboote\nDate: Thu, 11 Apr 1996 01:10:30 -0500\nFrom: Eryq \nOrganization: Yoyodyne Propulsion Systems\nX-Mailer: Mozilla 2.0 (X11; I; Linux 1.1.18 i486)\nMIME-Version: 1.0\nTo: john-bigboote@eryq.pr.mcs.net\nSubject: Two images for you...\nContent-Type: multipart/mixed; boundary=\"------------299A70B339B65A93542D2AE\"", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-2evil.msg b/packages/node-mimimi/test/mimetools-testmsgs/multi-2evil.msg new file mode 100644 index 00000000000..737e9045c7a --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-2evil.msg @@ -0,0 +1,58 @@ +Return-Path: eryq@rhine.gsfc.nasa.gov +Sender: john-bigboote +Date: Thu, 11 Apr 1996 01:10:30 -0500 +From: Eryq +Organization: Yoyodyne Propulsion Systems +X-Mailer: Mozilla 2.0 (X11; I; Linux 1.1.18 i486) +MIME-Version: 1.0 +To: john-bigboote@eryq.pr.mcs.net +Subject: Two images for you... +Content-Type: multipart/mixed; boundary="------------299A70B339B65A93542D2AE" + +This is a multi-part message in MIME format. + +--------------299A70B339B65A93542D2AE +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +When unpacked, this message should produce two GIF files: + + * The 1st should be called "3d-compress.gif" + * The 2nd should be called "3d-eye.gif" + +Different ways of specifying the filenames have been used. + +-- + ____ __ + / __/__________/_/ Eryq (eryq@rhine.gsfc.nasa.gov) + / __/ _/ / / , / Hughes STX Corporation, NASA/Goddard +/___/_/ \ /\ /___ + /_/ /_____/ http://selsvr.stx.com/~eryq/ + +--------------299A70B339B65A93542D2AE +Content-Type: image/gif +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="/evil/because:of\path\3d-=?ISO-8859-1?Q?=63?=om=?US-ASCII*EN?Q?pr?=ess.gif" + +R0lGODdhKAAoAOMAAAAAAAAAgB6Q/y9PT25ubnCAkKBSLb6+vufn5/Xes/+lAP/6zQAAAAAA +AAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJLMOyYbcoxkaZ5oCkoH6L5wLMfiWqd4btZhmxbA +oFCY47EIqMJgyWw2ATjj7aRkAq5YwDMl9VGtKO0SiuoiTVlscsxt9c4HgXxUIA0EAVOVfDKT +8Hl1B3kDAYYle202XnGGgoMHhYckiWVuR3+OTgCGeZRslotwgJ2lnYigfZdTjQULr7ALBZN0 +qTurjHgLKAu0B5Wqopm7J72etQN8t8Ijury+wMtvw8/Hv7Ylfs0BxCbGqMmK0yOOQ0GTCgrR +2bhwJGlXJQYG6mMKoeNoWSbzCWIACe5JwxQm3AkDAbUAQCiQhDZEBeBl6afgCsOBrD45edIv +QceGWSMevpOYhl6CkydBHhBZQmGKjihVshypjB9ClAHZMTugzOU7mzhBPiSZ5uDNnA7b/aTZ +0mhMnfl0pDBFa6bUElSPWb0qtYuHrxlwcR17YsWMs2jTql3LFkQEADs= +--------------299A70B339B65A93542D2AE +Content-Type: image/gif; name="3d-eye-is-an-evil-filename because of excessive length and verbosity. Unfortunately what can we do given an idiotic situation such as this?" +Content-Transfer-Encoding: base64 + +R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAA +AAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7 +VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7 ++3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatL +rU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJ +vs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjw +E0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7 +--------------299A70B339B65A93542D2AE-- +That was a multi-part message in MIME format. + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-2gifs-base64-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/multi-2gifs-base64-expected.json new file mode 100644 index 00000000000..19ae879a12d --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-2gifs-base64-expected.json @@ -0,0 +1,39 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": null, + "alternativeBoundary": null, + "sender": { + "name": "", + "mailAddress": "", + "valid": false + }, + "toRecipients": [], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "a multipart message/rfc822 which has been base64-encoded", + "plainBodyText": null, + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "unknown.eml", + "data": "VW1WMGRYSnVMVkJoZEdnNklHVnllWEZBY21ocGJtVXVaM05tWXk1dVlYTmhMbWR2ZGdwVFpXNWtaWEk2SUdwdmFHNHRZbWxuDQpZbTl2ZEdVS1JHRjBaVG9nVkdoMUxDQXhNU0JCY0hJZ01UazVOaUF3TVRveE1Eb3pNQ0F0TURVd01BcEdjbTl0T2lCRmNubHgNCklEeGxjbmx4UUhKb2FXNWxMbWR6Wm1NdWJtRnpZUzVuYjNZK0NrOXlaMkZ1YVhwaGRHbHZiam9nV1c5NWIyUjVibVVnVUhKdg0KY0hWc2MybHZiaUJUZVhOMFpXMXpDbGd0VFdGcGJHVnlPaUJOYjNwcGJHeGhJREl1TUNBb1dERXhPeUJKT3lCTWFXNTFlQ0F4DQpMakV1TVRnZ2FUUTROaWtLVFVsTlJTMVdaWEp6YVc5dU9pQXhMakFLVkc4NklHcHZhRzR0WW1sblltOXZkR1ZBWlhKNWNTNXcNCmNpNXRZM011Ym1WMENsTjFZbXBsWTNRNklGUjNieUJwYldGblpYTWdabTl5SUhsdmRTNHVMZ3BEYjI1MFpXNTBMVlI1Y0dVNg0KSUcxMWJIUnBjR0Z5ZEM5dGFYaGxaRHNnWW05MWJtUmhjbms5SWkwdExTMHRMUzB0TFMwdExUSTVPVUUzTUVJek16bENOalZCDQpPVE0xTkRKRU1rRkZJZ29LVkdocGN5QnBjeUJoSUcxMWJIUnBMWEJoY25RZ2JXVnpjMkZuWlNCcGJpQk5TVTFGSUdadmNtMWgNCmRDNEtDaTB0TFMwdExTMHRMUzB0TFMwdE1qazVRVGN3UWpNek9VSTJOVUU1TXpVME1rUXlRVVVLUTI5dWRHVnVkQzFVZVhCbA0KT2lCMFpYaDBMM0JzWVdsdU95QmphR0Z5YzJWMFBYVnpMV0Z6WTJscENrTnZiblJsYm5RdFZISmhibk5tWlhJdFJXNWpiMlJwDQpibWM2SURkaWFYUUtDbGRvWlc0Z2RXNXdZV05yWldRc0lIUm9hWE1nYldWemMyRm5aU0J6YUc5MWJHUWdjSEp2WkhWalpTQjANCmQyOGdSMGxHSUdacGJHVnpPZ29LQ1NvZ1ZHaGxJREZ6ZENCemFHOTFiR1FnWW1VZ1kyRnNiR1ZrSUNJelpDMWpiMjF3Y21Weg0KY3k1bmFXWWlDZ2txSUZSb1pTQXlibVFnYzJodmRXeGtJR0psSUdOaGJHeGxaQ0FpTTJRdFpYbGxMbWRwWmlJS0NrUnBabVpsDQpjbVZ1ZENCM1lYbHpJRzltSUhOd1pXTnBabmxwYm1jZ2RHaGxJR1pwYkdWdVlXMWxjeUJvWVhabElHSmxaVzRnZFhObFpDNEsNCkNpMHRJQW9nSUNCZlgxOWZJQ0FnSUNBZ0lDQWdJQ0JmWHdvZ0lDOGdYMTh2WDE5ZlgxOWZYMTlmWHk5Zkx5QWdSWEo1Y1NBbw0KWlhKNWNVQnlhR2x1WlM1bmMyWmpMbTVoYzJFdVoyOTJLUW9nTHlCZlh5OGdYeThnTHlBdklDd2dMeUFnSUNBZ1NIVm5hR1Z6DQpJRk5VV0NCRGIzSndiM0poZEdsdmJpd2dUa0ZUUVM5SGIyUmtZWEprQ2k5ZlgxOHZYeThnWENBZ0wxd2dJQzlmWDE4Z0NpQWcNCklDQWdJQ0FnTDE4dklDOWZYMTlmWHk4Z0lDQm9kSFJ3T2k4dmMyVnNjM1p5TG5OMGVDNWpiMjB2Zm1WeWVYRXZDZ290TFMwdA0KTFMwdExTMHRMUzB0TFRJNU9VRTNNRUl6TXpsQ05qVkJPVE0xTkRKRU1rRkZDa052Ym5SbGJuUXRWSGx3WlRvZ2FXMWhaMlV2DQpaMmxtQ2tOdmJuUmxiblF0VkhKaGJuTm1aWEl0Ulc1amIyUnBibWM2SUdKaGMyVTJOQXBEYjI1MFpXNTBMVVJwYzNCdmMybDANCmFXOXVPaUJwYm14cGJtVTdJR1pwYkdWdVlXMWxQU0l6WkMxamIyMXdjbVZ6Y3k1bmFXWWlDZ3BTTUd4SFQwUmthRXRCUVc5Qg0KVDAxQlFVRkJRVUZCUVVGblFqWlJMM2s1VUZReU5YVmlia05CYTB0Q1UweGlOaXQyZFdadU5TOVlaWE12SzJ4QlVDODJlbEZCDQpRVUZCUVVFS1FVRkJRVUZCUVVGQlEzZEJRVUZCUVV0QlFXOUJRVUZGTDJoRVNsTmhkVGxsU2t4TlQzbFpZbU52ZUd0aFdqVnYNClEydHZTRFpNTlhkTVRXWnBWM0ZrTkdKMFdtaHRlR0pCQ205R1ExazBOMFZKY1UxS1ozbFhkekpCVkdwcU4yRlNhMEZ4TlZsMw0KUkUxc09WWkhkRXRQTUZOcGRXOXBWRlpzYzJOemVIUTVZelJJWjFoNFZVbEJNRVZCVms5V1prUkxWQW80U0d3eFFqTnJSRUZaDQpXV3hsTWpBeVdHNUhSMmR2VFVob1dXTnJhVmRXZFZJekswOVVaME5IWlZwU2MyeHZkSGRuU2pKc2JsbHBaMlphWkZScVVWVk0NCmNqZEJURUphVGpBS2NWUjFjbXBJWjB4TFFYVXdRalZYY1c5d2JUZEtOekpsZEZGT09IUTRTV3AxY25rcmQwMTBkbmM0TDBoMg0KTjFsc1puTXdRbmhEWWtkeFRXMUxNSGxQVDFFd1IxUkRaM0pTQ2pKaWFIZEtSMnhZU2xGWlJ6WnRUVXR2WlU1dlYxTmlla05YDQpTVUZEWlRWS2QzaFJiVE5CYTBSQllsVkJVVU5wVVdoRVdrVkNaVUpzTm1GbVowTnpUMEp5UkRRMVpXUkpkZ3BSWTJWSFYxTk4NClpYWndUMWxvYkRaRGEzbGtRa2hvUWxwUmJVZExhbWxvVm5Ob2VYQnFRamxEYkVGSVdrMVVkV2Q2VDFVM2JYcG9RbEJwVTFvMQ0KZFVST2JrRTNZaTloVkZvS01HMW9UVzVtYkRCd1JFSkdZVFppVlVWc1UxQlhZakJ4ZEZsMVNISjRiSGRqVWpFM1dYTlhUWE15DQphbFJ4YkROTVJtdFJSVUZFY3owS0xTMHRMUzB0TFMwdExTMHRMUzB5T1RsQk56QkNNek01UWpZMVFUa3pOVFF5UkRKQlJRcEQNCmIyNTBaVzUwTFZSNWNHVTZJR2x0WVdkbEwyZHBaanNnYm1GdFpUMGlNMlF0WlhsbExtZHBaaUlLUTI5dWRHVnVkQzFVY21GdQ0KYzJabGNpMUZibU52WkdsdVp6b2dZbUZ6WlRZMENncFNNR3hIVDBSa2FFdEJRVzlCVUUxQlFVRkJRVUZCUVVGNlRqTjFMemMyDQpLM1p2YVVscFJ6VjFZbk42WkRkMkx5OHZLMlp1TlhkQlFVRkJRVUZCUVVGQlFVRkJRVUZCUVVFS1FVRkJRVUZCUVVGQlEzZEINClFVRkJRVXRCUVc5QlFVRkZMMmhFU2xOaGRUbGxTbUpOVDNrMFlrMXZlR3RoV2pWdlEydHZSRFpNTlhkTVRXWnBWMjV6TkRGdg0KV25RM2JFMDNDbFoxYW01RE9UWkpVbFp6VUZkUlJUUnVlRkJxYTNadGMxRnRkVGh2WXk5TFFsVlRWbGRyTjFobGNFZEhUR1ZPDQpjbmh2ZUVwUE1VMXFTVXhxZEdobkwydFhXRkUyZDA4dk53b3JNMlJEWlZKU2FtWkJTMGhwU1cxS1FWWXJSRU5HTUVKcFZ6VlcNClFXOHhRMFZzWVZKb05VNXFiR3RsV1cxd2VWUm5jR05VUVV0SGFXRmhVMlp3ZDB0d1ZsRmhlRlpoZEV3S2NsVTRSMkZSWkU5Qw0KUVZGQlFqY3JlVmhzYVZoVWNtZEJlSE5YTkhaR1lXSjJPRUpQZEVKelFuUTNZMGQyZDBOSlZEbHVUM2xPUlVsNGRVTTBlbkp4DQpTM3BqT1ZoaVQwUktDblp6TjFrMVpYZElNMlEzUm5obE0ycENOSEpxT0hRMlVIVk9ZVFp5TW1Kb1MxRllUakUzUmxsRFFrMXgNClZFZHBRbnBUVG1oNE5XY3dia1ZOYUd4elUwcHFhVkpaZGtScWR3cEZNR05rUjNoUkwyZHpkMjl6YjB0VmEyMTFWVEpHYmtwag0KYzFOTFIxUkNhbmx3ZUVwemVXRkpRMEZCTndvdExTMHRMUzB0TFMwdExTMHRMVEk1T1VFM01FSXpNemxDTmpWQk9UTTFOREpFDQpNa0ZGTFMwS1ZHaGhkQ0IzWVhNZ1lTQnRkV3gwYVMxd1lYSjBJRzFsYzNOaFoyVWdhVzRnVFVsTlJTQm1iM0p0WVhRdUNnPT0NCg==", + "mimeType": "message/rfc822", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "Content-type: message/rfc822\nContent-transfer-encoding: base64\nSubject: a multipart message/rfc822 which has been base64-encoded", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-2gifs-base64.msg b/packages/node-mimimi/test/mimetools-testmsgs/multi-2gifs-base64.msg new file mode 100644 index 00000000000..7cdaac951f4 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-2gifs-base64.msg @@ -0,0 +1,48 @@ +Content-type: message/rfc822 +Content-transfer-encoding: base64 +Subject: a multipart message/rfc822 which has been base64-encoded + +UmV0dXJuLVBhdGg6IGVyeXFAcmhpbmUuZ3NmYy5uYXNhLmdvdgpTZW5kZXI6IGpvaG4tYmln +Ym9vdGUKRGF0ZTogVGh1LCAxMSBBcHIgMTk5NiAwMToxMDozMCAtMDUwMApGcm9tOiBFcnlx +IDxlcnlxQHJoaW5lLmdzZmMubmFzYS5nb3Y+Ck9yZ2FuaXphdGlvbjogWW95b2R5bmUgUHJv +cHVsc2lvbiBTeXN0ZW1zClgtTWFpbGVyOiBNb3ppbGxhIDIuMCAoWDExOyBJOyBMaW51eCAx +LjEuMTggaTQ4NikKTUlNRS1WZXJzaW9uOiAxLjAKVG86IGpvaG4tYmlnYm9vdGVAZXJ5cS5w +ci5tY3MubmV0ClN1YmplY3Q6IFR3byBpbWFnZXMgZm9yIHlvdS4uLgpDb250ZW50LVR5cGU6 +IG11bHRpcGFydC9taXhlZDsgYm91bmRhcnk9Ii0tLS0tLS0tLS0tLTI5OUE3MEIzMzlCNjVB +OTM1NDJEMkFFIgoKVGhpcyBpcyBhIG11bHRpLXBhcnQgbWVzc2FnZSBpbiBNSU1FIGZvcm1h +dC4KCi0tLS0tLS0tLS0tLS0tMjk5QTcwQjMzOUI2NUE5MzU0MkQyQUUKQ29udGVudC1UeXBl +OiB0ZXh0L3BsYWluOyBjaGFyc2V0PXVzLWFzY2lpCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rp +bmc6IDdiaXQKCldoZW4gdW5wYWNrZWQsIHRoaXMgbWVzc2FnZSBzaG91bGQgcHJvZHVjZSB0 +d28gR0lGIGZpbGVzOgoKCSogVGhlIDFzdCBzaG91bGQgYmUgY2FsbGVkICIzZC1jb21wcmVz +cy5naWYiCgkqIFRoZSAybmQgc2hvdWxkIGJlIGNhbGxlZCAiM2QtZXllLmdpZiIKCkRpZmZl +cmVudCB3YXlzIG9mIHNwZWNpZnlpbmcgdGhlIGZpbGVuYW1lcyBoYXZlIGJlZW4gdXNlZC4K +Ci0tIAogICBfX19fICAgICAgICAgICBfXwogIC8gX18vX19fX19fX19fXy9fLyAgRXJ5cSAo +ZXJ5cUByaGluZS5nc2ZjLm5hc2EuZ292KQogLyBfXy8gXy8gLyAvICwgLyAgICAgSHVnaGVz +IFNUWCBDb3Jwb3JhdGlvbiwgTkFTQS9Hb2RkYXJkCi9fX18vXy8gXCAgL1wgIC9fX18gCiAg +ICAgICAgL18vIC9fX19fXy8gICBodHRwOi8vc2Vsc3ZyLnN0eC5jb20vfmVyeXEvCgotLS0t +LS0tLS0tLS0tLTI5OUE3MEIzMzlCNjVBOTM1NDJEMkFFCkNvbnRlbnQtVHlwZTogaW1hZ2Uv +Z2lmCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NApDb250ZW50LURpc3Bvc2l0 +aW9uOiBpbmxpbmU7IGZpbGVuYW1lPSIzZC1jb21wcmVzcy5naWYiCgpSMGxHT0RkaEtBQW9B +T01BQUFBQUFBQUFnQjZRL3k5UFQyNXVibkNBa0tCU0xiNit2dWZuNS9YZXMvK2xBUC82elFB +QUFBQUEKQUFBQUFBQUFBQ3dBQUFBQUtBQW9BQUFFL2hESlNhdTllSkxNT3lZYmNveGthWjVv +Q2tvSDZMNXdMTWZpV3FkNGJ0WmhteGJBCm9GQ1k0N0VJcU1KZ3lXdzJBVGpqN2FSa0FxNVl3 +RE1sOVZHdEtPMFNpdW9pVFZsc2NzeHQ5YzRIZ1h4VUlBMEVBVk9WZkRLVAo4SGwxQjNrREFZ +WWxlMjAyWG5HR2dvTUhoWWNraVdWdVIzK09UZ0NHZVpSc2xvdHdnSjJsbllpZ2ZaZFRqUVVM +cjdBTEJaTjAKcVR1cmpIZ0xLQXUwQjVXcW9wbTdKNzJldFFOOHQ4SWp1cnkrd010dnc4L0h2 +N1lsZnMwQnhDYkdxTW1LMHlPT1EwR1RDZ3JSCjJiaHdKR2xYSlFZRzZtTUtvZU5vV1NiekNX +SUFDZTVKd3hRbTNBa0RBYlVBUUNpUWhEWkVCZUJsNmFmZ0NzT0JyRDQ1ZWRJdgpRY2VHV1NN +ZXZwT1lobDZDa3lkQkhoQlpRbUdLamloVnNoeXBqQjlDbEFIWk1UdWd6T1U3bXpoQlBpU1o1 +dURObkE3Yi9hVFoKMG1oTW5mbDBwREJGYTZiVUVsU1BXYjBxdFl1SHJ4bHdjUjE3WXNXTXMy +alRxbDNMRmtRRUFEcz0KLS0tLS0tLS0tLS0tLS0yOTlBNzBCMzM5QjY1QTkzNTQyRDJBRQpD +b250ZW50LVR5cGU6IGltYWdlL2dpZjsgbmFtZT0iM2QtZXllLmdpZiIKQ29udGVudC1UcmFu +c2Zlci1FbmNvZGluZzogYmFzZTY0CgpSMGxHT0RkaEtBQW9BUE1BQUFBQUFBQUF6TjN1Lzc2 +K3ZvaUlpRzV1YnN6ZDd2Ly8vK2ZuNXdBQUFBQUFBQUFBQUFBQUFBQUEKQUFBQUFBQUFBQ3dB +QUFBQUtBQW9BQUFFL2hESlNhdTllSmJNT3k0Yk1veGthWjVvQ2tvRDZMNXdMTWZpV25zNDFv +WnQ3bE03ClZ1am5DOTZJUlZzUFdRRTRueFBqa3Ztc1FtdThvYy9LQlVTVldrN1hlcEdHTGVO +cnhveEpPMU1qSUxqdGhnL2tXWFE2d08vNworM2RDZVJSamZBS0hpSW1KQVYrRENGMEJpVzVW +QW8xQ0VsYVJoNU5qbGtlWW1weVRncGNUQUtHaWFhU2Zwd0twVlFheFZhdEwKclU4R2FRZE9C +QVFBQjcreVhsaVhUcmdBeHNXNHZGYWJ2OEJPdEJzQnQ3Y0d2d0NJVDluT3lORUl4dUM0enJx +S3pjOVhiT0RKCnZzN1k1ZXdIM2Q3RnhlM2pCNHJqOHQ2UHVOYTZyMmJoS1FYTjE3RllDQk1x +VEdpQnpTTmh4NWcwbkVNaGxzU0pqaVJZdkRqdwpFMGNkR3hRL2dzd29zb0tVa211VTJGbkpj +c1NLR1RCanlweEpzeWFJQ0FBNwotLS0tLS0tLS0tLS0tLTI5OUE3MEIzMzlCNjVBOTM1NDJE +MkFFLS0KVGhhdCB3YXMgYSBtdWx0aS1wYXJ0IG1lc3NhZ2UgaW4gTUlNRSBmb3JtYXQuCg== diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-2gifs-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/multi-2gifs-expected.json new file mode 100644 index 00000000000..954b27f0c42 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-2gifs-expected.json @@ -0,0 +1,53 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "------------299A70B339B65A93542D2AE", + "alternativeBoundary": null, + "sender": { + "name": "Eryq", + "mailAddress": "eryq@rhine.gsfc.nasa.gov", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "john-bigboote@eryq.pr.mcs.net", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 829203030000, + "subject": "Two images for you...", + "plainBodyText": "When unpacked, this message should produce two GIF files:\r\n\r\n\t* The 1st should be called \"3d-compress.gif\"\r\n\t* The 2nd should be called \"3d-eye.gif\"\r\n\r\nDifferent ways of specifying the filenames have been used.\r\n\r\n-- \r\n ____ __\r\n / __/__________/_/ Eryq (eryq@rhine.gsfc.nasa.gov)\r\n / __/ _/ / / , / Hughes STX Corporation, NASA/Goddard\r\n/___/_/ \\ /\\ /___ \r\n /_/ /_____/ http://selsvr.stx.com/~eryq/\r\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "3d-compress.gif", + "data": "R0lGODdhKAAoAOMAAAAAAAAAgB6Q/y9PT25ubnCAkKBSLb6+vufn5/Xes/+lAP/6zQAAAAAAAAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJLMOyYbcoxkaZ5oCkoH6L5wLMfiWqd4btZhmxbAoFCY47EIqMJgyWw2ATjj7aRkAq5YwDMl9VGtKO0SiuoiTVlscsxt9c4HgXxUIA0EAVOVfDKT8Hl1B3kDAYYle202XnGGgoMHhYckiWVuR3+OTgCGeZRslotwgJ2lnYigfZdTjQULr7ALBZN0qTurjHgLKAu0B5Wqopm7J72etQN8t8Ijury+wMtvw8/Hv7Ylfs0BxCbGqMmK0yOOQ0GTCgrR2bhwJGlXJQYG6mMKoeNoWSbzCWIACe5JwxQm3AkDAbUAQCiQhDZEBeBl6afgCsOBrD45edIvQceGWSMevpOYhl6CkydBHhBZQmGKjihVshypjB9ClAHZMTugzOU7mzhBPiSZ5uDNnA7b/aTZ0mhMnfl0pDBFa6bUElSPWb0qtYuHrxlwcR17YsWMs2jTql3LFkQEADs=", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + }, + { + "name": "3d-eye.gif", + "data": "R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7+3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatLrU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJvs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjwE0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "Return-Path: eryq@rhine.gsfc.nasa.gov\nSender: john-bigboote\nDate: Thu, 11 Apr 1996 01:10:30 -0500\nFrom: Eryq \nOrganization: Yoyodyne Propulsion Systems\nX-Mailer: Mozilla 2.0 (X11; I; Linux 1.1.18 i486)\nMIME-Version: 1.0\nTo: john-bigboote@eryq.pr.mcs.net\nSubject: Two images for you...\nContent-Type: multipart/mixed; boundary=\"------------299A70B339B65A93542D2AE\"", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-2gifs.msg b/packages/node-mimimi/test/mimetools-testmsgs/multi-2gifs.msg new file mode 100644 index 00000000000..49a87417db2 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-2gifs.msg @@ -0,0 +1,57 @@ +Return-Path: eryq@rhine.gsfc.nasa.gov +Sender: john-bigboote +Date: Thu, 11 Apr 1996 01:10:30 -0500 +From: Eryq +Organization: Yoyodyne Propulsion Systems +X-Mailer: Mozilla 2.0 (X11; I; Linux 1.1.18 i486) +MIME-Version: 1.0 +To: john-bigboote@eryq.pr.mcs.net +Subject: Two images for you... +Content-Type: multipart/mixed; boundary="------------299A70B339B65A93542D2AE" + +This is a multi-part message in MIME format. + +--------------299A70B339B65A93542D2AE +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +When unpacked, this message should produce two GIF files: + + * The 1st should be called "3d-compress.gif" + * The 2nd should be called "3d-eye.gif" + +Different ways of specifying the filenames have been used. + +-- + ____ __ + / __/__________/_/ Eryq (eryq@rhine.gsfc.nasa.gov) + / __/ _/ / / , / Hughes STX Corporation, NASA/Goddard +/___/_/ \ /\ /___ + /_/ /_____/ http://selsvr.stx.com/~eryq/ + +--------------299A70B339B65A93542D2AE +Content-Type: image/gif +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="3d-compress.gif" + +R0lGODdhKAAoAOMAAAAAAAAAgB6Q/y9PT25ubnCAkKBSLb6+vufn5/Xes/+lAP/6zQAAAAAA +AAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJLMOyYbcoxkaZ5oCkoH6L5wLMfiWqd4btZhmxbA +oFCY47EIqMJgyWw2ATjj7aRkAq5YwDMl9VGtKO0SiuoiTVlscsxt9c4HgXxUIA0EAVOVfDKT +8Hl1B3kDAYYle202XnGGgoMHhYckiWVuR3+OTgCGeZRslotwgJ2lnYigfZdTjQULr7ALBZN0 +qTurjHgLKAu0B5Wqopm7J72etQN8t8Ijury+wMtvw8/Hv7Ylfs0BxCbGqMmK0yOOQ0GTCgrR +2bhwJGlXJQYG6mMKoeNoWSbzCWIACe5JwxQm3AkDAbUAQCiQhDZEBeBl6afgCsOBrD45edIv +QceGWSMevpOYhl6CkydBHhBZQmGKjihVshypjB9ClAHZMTugzOU7mzhBPiSZ5uDNnA7b/aTZ +0mhMnfl0pDBFa6bUElSPWb0qtYuHrxlwcR17YsWMs2jTql3LFkQEADs= +--------------299A70B339B65A93542D2AE +Content-Type: image/gif; name="3d-eye.gif" +Content-Transfer-Encoding: base64 + +R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAA +AAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7 +VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7 ++3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatL +rU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJ +vs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjw +E0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7 +--------------299A70B339B65A93542D2AE-- +That was a multi-part message in MIME format. diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-bad-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/multi-bad-expected.json new file mode 100644 index 00000000000..c3ae75993bc --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-bad-expected.json @@ -0,0 +1,36 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "-------------------------------147881770724098", + "alternativeBoundary": null, + "sender": { + "name": "Michelle Holm", + "mailAddress": "holm@sitka.colorado.edu", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "imswww@rhine.gsfc.nasa.gov", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 809011857000, + "subject": "http://rhine.gsfc.nasa.gov:8080/ims-bin/v.b/imssearch", + "plainBodyText": "[[ERROR]] Error 600: Internal logic.\n\nDying gasp:\nBad mode: SEARCH/ (600).\n\nRecommended action to correct the situation:\nYIKES! IMS/www failed one of its internal consistency checks! Please SAVE THIS\nFILE, and contact IMS/www's developers immediately so they can fix the problem!\nIf the parentheses at the end of this sentence are not blank, you can contact\nthem here (imswww@rhine.gsfc.nasa.gov).\n\n ------------------------------------------------------------------------\n\n\nLocation of error\n\nDying gasp:\nPackage \"main\", file \"/usr/app/people/imswww/v.b/lib/perl/imssearch.pl\", line\n753.\n\nTraceback:\n\n 1. Iw::Die: from \"main\", \"/usr/app/people/imswww/v.b/lib/perl/imssearch.pl\n line 753\n 2. main::Main: from \"main\", \"/usr/app/people/imswww/public_cgi/v.b/imssearch\n line 85\n\n ------------------------------------------------------------------------\n\n\nBasic state information\n\nInclude path\n\n/usr/app/people/imswww/v.b/lib/perl\n/usr/app/people/imswww/v.b/lib/perl/Eg\n/usr/local/lib/perl5/sun4-sunos\n/usr/local/lib/perl5\n\n\n\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [], + "mailHeaders": "From: Michelle Holm \nDate: Mon, 21 Aug 95 13:30:57 -600\nSender: holm@sitka.colorado.edu\nTo: imswww@rhine.gsfc.nasa.gov\nMime-Version: 1.0\nX-Mailer: Mozilla/1.0N (X11; IRIX 5.2 IP20)\nContent-Type: multipart/mixed;\r\n\tboundary=\"-------------------------------147881770724098\"\nSubject: http://rhine.gsfc.nasa.gov:8080/ims-bin/v.b/imssearch\nX-Url: http://rhine.gsfc.nasa.gov:8080/ims-bin/v.b/imssearch\nContent-Type: text/plain; charset=iso-8859-1\nContent-Transfer-Encoding: 8bit", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-bad.msg b/packages/node-mimimi/test/mimetools-testmsgs/multi-bad.msg new file mode 100644 index 00000000000..9437b637242 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-bad.msg @@ -0,0 +1,181 @@ +From: Michelle Holm +Date: Mon, 21 Aug 95 13:30:57 -600 +Sender: holm@sitka.colorado.edu +To: imswww@rhine.gsfc.nasa.gov +Mime-Version: 1.0 +X-Mailer: Mozilla/1.0N (X11; IRIX 5.2 IP20) +Content-Type: multipart/mixed; + boundary="-------------------------------147881770724098" +Subject: http://rhine.gsfc.nasa.gov:8080/ims-bin/v.b/imssearch +X-Url: http://rhine.gsfc.nasa.gov:8080/ims-bin/v.b/imssearch +Content-Type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: 8bit + +> [[ERROR]] Error 600: Internal logic. +> +> Dying gasp: +> Bad mode: SEARCH/ (600). +> +> Recommended action to correct the situation: +> YIKES! IMS/www failed one of its internal consistency checks! Please SAVE +> THIS FILE, and contact IMS/www's developers immediately so they can fix the +> problem! If the parentheses at the end of this sentence are not blank, you +> can contact them here (imswww@rhine.gsfc.nasa.gov). +> +> ------------------------------------------------------------------------ +> +> +> Location of error +> +> Dying gasp: +> Package "main", file "/usr/app/people/imswww/v.b/lib/perl/imssearch.pl", line +> 753. +> +> Traceback: +> +> 1. Iw::Die: from "main", "/usr/app/people/imswww/v.b/lib/perl/imssearch.pl +> line 753 +> 2. main::Main: from "main", +> "/usr/app/people/imswww/public_cgi/v.b/imssearch line 85 +> +> ------------------------------------------------------------------------ +> +> +> Basic state information +> +> Include path +> +> /usr/app/people/imswww/v.b/lib/perl +> /usr/app/people/imswww/v.b/lib/perl/Eg +> /usr/local/lib/perl5/sun4-sunos +> /usr/local/lib/perl5 +> . +> +> Environment variables +> +> CONTENT_LENGTH = "281" +> CONTENT_TYPE = "application/x-www-form-urlencoded" +> DOCUMENT_ROOT = "/usr/local/etc/httpd/htdocs" +> GAEADATA_DIR = "/home/rhine/ims/lib/gaea_data" +> GAEATMP_DIR = "/usr/app/people/imswww/v.b/tmp/imswww-usr/809033436-10153" +> GATEWAY_INTERFACE = "CGI/1.1" +> HTTP_ACCEPT = "*/*, image/gif, image/x-xbitmap, image/jpeg" +> HTTP_REFERER = "http://rhine.gsfc.nasa.gov:8080/ims-bin/v.b/imssearch" +> HTTP_USER_AGENT = "Mozilla/1.0N (X11; IRIX 5.2 IP20)" +> IMS_STAFF = "1" +> IW_CGI_DIR = "/usr/app/people/imswww/v.b/cgi-bin" +> IW_DOCS_DIR = "/usr/app/people/imswww/v.b/docs" +> IW_LIB_DIR = "/usr/app/people/imswww/v.b/lib" +> IW_SESSION_DIR = "/usr/app/people/imswww/v.b/tmp/imswww-usr/809033436-10153" +> IW_SESSION_ID = "809033436-10153" +> IW_TMP_DIR = "/usr/app/people/imswww/v.b/tmp" +> PATH = "/bin:/usr/bin:/usr/etc:/usr/ucb:/usr/local/bin:/usr/ucb" +> QUERY_STRING = "" +> REMOTE_ADDR = "128.138.135.33" +> REMOTE_HOST = "sitka.colorado.edu" +> REQUEST_METHOD = "POST" +> SCRIPT_NAME = "/ims-bin/v.b/imssearch" +> SERVER_NAME = "rhine.gsfc.nasa.gov" +> SERVER_PORT = "8080" +> SERVER_PROTOCOL = "HTTP/1.0" +> SERVER_SOFTWARE = "NCSA/1.4.2" +> SYSLOG_LEVEL = "7" +> USRDATA_DIR = "/usr/app/people/imswww/v.b/tmp/imswww-usr/809033436-10153" +> +> Tags +> +> pmap-geo-opt = "map" +> pparam-param = "Sea Ice Concentration" +> pparam-param = "Snow Cover" +> pparam-param = "Total Sea Ice Concentration" +> s-east-long = "-40.0" +> s-north-lat = "75.3" +> s-south-lat = "66.0" +> s-start-date = "01-01-1990" +> s-start-time = "" +> s-stop-date = "31-12-1994" +> s-stop-time = "" +> s-west-long = "-176.0" +> sid = "809033436-10153" +> +> Permissions +> +> Real user id = 65534 +> Real group ids = 65534 65534 +> Effective user id = 65534 +> Effective group ids = 65534 65534 +> +> ------------------------------------------------------------------------ +> +> +> Log file /usr/app/people/imswww/v.b/tmp/imswww-usr/10184.clog +> +> II 1995/08/21 15:33:50 IwLog 94 +> Logging begun +> WWW 1995/08/21 15:33:50 Iw 263 +> Perl: Use of uninitialized value at /usr/app/people/imswww/v.b/lib/perl/ims ; +> search.pl line 82. +> | +> EEEE 1995/08/21 15:33:51 Iw 95 +> Bad mode: SEARCH/ (600). +> +> ------------------------------------------------------------------------ +> +> +> Session information +> +> [Session Directory] +> +> ------------------------------------------------------------------------ +> +> Generated by EOSDIS IMS/www version 0.3b / imswww@rhine.gsfc.nasa.gov +> NASA/GSFC Task Representative: Yonsook Enloe, yonsook@killians.gsfc.nasa.gov +> +> A joint project of NASA/GSFC, A/WWW Enterprises, and Hughes STX Corporation. +> Full contact information is available. + +---------------------------------147881770724098 +Content-Type: text/plain +Content-Transfer-Encoding: 8bit + +[[ERROR]] Error 600: Internal logic. + +Dying gasp: +Bad mode: SEARCH/ (600). + +Recommended action to correct the situation: +YIKES! IMS/www failed one of its internal consistency checks! Please SAVE THIS +FILE, and contact IMS/www's developers immediately so they can fix the problem! +If the parentheses at the end of this sentence are not blank, you can contact +them here (imswww@rhine.gsfc.nasa.gov). + + ------------------------------------------------------------------------ + + +Location of error + +Dying gasp: +Package "main", file "/usr/app/people/imswww/v.b/lib/perl/imssearch.pl", line +753. + +Traceback: + + 1. Iw::Die: from "main", "/usr/app/people/imswww/v.b/lib/perl/imssearch.pl + line 753 + 2. main::Main: from "main", "/usr/app/people/imswww/public_cgi/v.b/imssearch + line 85 + + ------------------------------------------------------------------------ + + +Basic state information + +Include path + +/usr/app/people/imswww/v.b/lib/perl +/usr/app/people/imswww/v.b/lib/perl/Eg +/usr/local/lib/perl5/sun4-sunos +/usr/local/lib/perl5 + + + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-badnames-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/multi-badnames-expected.json new file mode 100644 index 00000000000..897251d2cd9 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-badnames-expected.json @@ -0,0 +1,36 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "simple boundary", + "alternativeBoundary": null, + "sender": { + "name": "Nathaniel Borenstein", + "mailAddress": "nsb@bellcore.com", + "valid": true + }, + "toRecipients": [ + { + "name": "Ned Freed", + "mailAddress": "ned@innosoft.com", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "Sample message", + "plainBodyText": "This is explicitly typed plain ASCII text.\nIt DOES end with a linebreak.\n\nThis is explicitly typed plain ASCII text.\nIt DOES end with a linebreak.\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [], + "mailHeaders": "From: Nathaniel Borenstein \nTo: Ned Freed \nSubject: Sample message\nMIME-Version: 1.0\nContent-type: multipart/mixed; boundary=\"simple\r\n boundary\"", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-badnames.msg b/packages/node-mimimi/test/mimetools-testmsgs/multi-badnames.msg new file mode 100644 index 00000000000..5533437fe66 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-badnames.msg @@ -0,0 +1,30 @@ +From: Nathaniel Borenstein +To: Ned Freed +Subject: Sample message +MIME-Version: 1.0 +Content-type: multipart/mixed; boundary="simple + boundary" + +This is the preamble. It is to be ignored, though it +is a handy place for mail composers to include an +explanatory note to non-MIME conformant readers. +--simple boundary +Content-type: text/plain; charset=us-ascii; name="/foo/bar" + + +--simple boundary +Content-type: text/plain; charset=us-ascii; name="foo bar" + +This is explicitly typed plain ASCII text. +It DOES end with a linebreak. + + +--simple boundary +Content-type: text/plain; charset=us-ascii; name="foobar" + +This is explicitly typed plain ASCII text. +It DOES end with a linebreak. + +--simple boundary-- +This is the epilogue. It is also to be ignored. + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-clen-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/multi-clen-expected.json new file mode 100644 index 00000000000..d1ea00cde42 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-clen-expected.json @@ -0,0 +1,53 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "simple boundary", + "alternativeBoundary": null, + "sender": { + "name": "Nathaniel Borenstein", + "mailAddress": "nsb@bellcore.com", + "valid": true + }, + "toRecipients": [ + { + "name": "Ned Freed", + "mailAddress": "ned@innosoft.com", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "Sample message", + "plainBodyText": "This is implicitly typed plain ASCII text.\r\nIt does NOT end with a linebreak.", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "unknown.txt", + "data": "MTIzNDU2Nzg5DQoxMjM0NTY3ODkNCjEyMzQ1Njc4OQ0K", + "mimeType": "text/x-numbers", + "charset": "us-ascii", + "contentId": "", + "calendarMethod": null + }, + { + "name": "unknown.txt", + "data": "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIgQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTINCkFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyIEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyDQpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMiBBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMg0KQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIgQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTINCkFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyIEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyDQpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMiBBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMg0KQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIgQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTINCkFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyIEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyDQpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMiBBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMg0KQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIgQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTINCg==", + "mimeType": "text/x-alphabet", + "charset": "us-ascii", + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "From: Nathaniel Borenstein \nTo: Ned Freed \nSubject: Sample message\nMIME-Version: 1.0\nContent-type: multipart/mixed; boundary=\"simple\r\n boundary\"", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-clen.msg b/packages/node-mimimi/test/mimetools-testmsgs/multi-clen.msg new file mode 100644 index 00000000000..bb1517686dd --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-clen.msg @@ -0,0 +1,40 @@ +From: Nathaniel Borenstein +To: Ned Freed +Subject: Sample message +MIME-Version: 1.0 +Content-type: multipart/mixed; boundary="simple + boundary" + +This is the preamble. It is to be ignored, though it +is a handy place for mail composers to include an +explanatory note to non-MIME conformant readers. +--simple boundary + +This is implicitly typed plain ASCII text. +It does NOT end with a linebreak. +--simple boundary +Content-type: text/x-numbers; charset=us-ascii +Content-length: 30 + +123456789 +123456789 +123456789 + +--simple boundary +Content-type: text/x-alphabet; charset=us-ascii +Content-length: 600 + +ABCDEFGHIJKLMNOPQRSTUVWXYZ012 ABCDEFGHIJKLMNOPQRSTUVWXYZ012 +ABCDEFGHIJKLMNOPQRSTUVWXYZ012 ABCDEFGHIJKLMNOPQRSTUVWXYZ012 +ABCDEFGHIJKLMNOPQRSTUVWXYZ012 ABCDEFGHIJKLMNOPQRSTUVWXYZ012 +ABCDEFGHIJKLMNOPQRSTUVWXYZ012 ABCDEFGHIJKLMNOPQRSTUVWXYZ012 +ABCDEFGHIJKLMNOPQRSTUVWXYZ012 ABCDEFGHIJKLMNOPQRSTUVWXYZ012 +ABCDEFGHIJKLMNOPQRSTUVWXYZ012 ABCDEFGHIJKLMNOPQRSTUVWXYZ012 +ABCDEFGHIJKLMNOPQRSTUVWXYZ012 ABCDEFGHIJKLMNOPQRSTUVWXYZ012 +ABCDEFGHIJKLMNOPQRSTUVWXYZ012 ABCDEFGHIJKLMNOPQRSTUVWXYZ012 +ABCDEFGHIJKLMNOPQRSTUVWXYZ012 ABCDEFGHIJKLMNOPQRSTUVWXYZ012 +ABCDEFGHIJKLMNOPQRSTUVWXYZ012 ABCDEFGHIJKLMNOPQRSTUVWXYZ012 + +--simple boundary-- +This is the epilogue. It is also to be ignored. + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-digest-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/multi-digest-expected.json new file mode 100644 index 00000000000..69bde02748d --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-digest-expected.json @@ -0,0 +1,45 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "simple boundary", + "alternativeBoundary": null, + "sender": { + "name": "Nathaniel Borenstein", + "mailAddress": "nsb@bellcore.com", + "valid": true + }, + "toRecipients": [ + { + "name": "Ned Freed", + "mailAddress": "ned@innosoft.com", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "Sample digest message", + "plainBodyText": "From: noone@nowhere.org\r\nSubject: embedded message 1\r\n\r\nThis is implicitly-typed ASCII text.\r\nIt does NOT end with a linebreak.", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "embedded message 2.eml", + "data": "RnJvbTogbm9vbmVAbm93aGVyZS5vcmcNClN1YmplY3Q6IGVtYmVkZGVkIG1lc3NhZ2UgMg0KQ29udGVudC10eXBlOiB0ZXh0DQoNClRoaXMgaXMgZXhwbGljaXRseSB0eXBlZCBwbGFpbiBBU0NJSSB0ZXh0Lg0KSXQgRE9FUyBlbmQgd2l0aCBhIGxpbmVicmVhay4NCg==", + "mimeType": "message/rfc822", + "charset": "us-ascii", + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "From: Nathaniel Borenstein \nTo: Ned Freed \nSubject: Sample digest message\nMIME-Version: 1.0\nContent-type: multipart/digest; boundary=\"simple\r\n boundary\"", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-digest.msg b/packages/node-mimimi/test/mimetools-testmsgs/multi-digest.msg new file mode 100644 index 00000000000..6254bbd8b26 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-digest.msg @@ -0,0 +1,30 @@ +From: Nathaniel Borenstein +To: Ned Freed +Subject: Sample digest message +MIME-Version: 1.0 +Content-type: multipart/digest; boundary="simple + boundary" + +This is the preamble. It is to be ignored, though it +is a handy place for mail composers to include an +explanatory note to non-MIME conformant readers. +--simple boundary + +From: noone@nowhere.org +Subject: embedded message 1 + +This is implicitly-typed ASCII text. +It does NOT end with a linebreak. +--simple boundary +Content-type: message/rfc822; charset=us-ascii + +From: noone@nowhere.org +Subject: embedded message 2 +Content-type: text + +This is explicitly typed plain ASCII text. +It DOES end with a linebreak. + +--simple boundary-- +This is the epilogue. It is also to be ignored. + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-frag-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/multi-frag-expected.json new file mode 100644 index 00000000000..72874001d91 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-frag-expected.json @@ -0,0 +1,7 @@ +{ + "exception": { + "clazz": "com.sun.mail.util.DecodingException", + "message": "BASE64Decoder: Error in encoded stream: needed 4 valid base64 characters but only got 3 before EOF, the 10 most recent characters were: \"message.\\r\\n\"" + }, + "result": null +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-frag.msg b/packages/node-mimimi/test/mimetools-testmsgs/multi-frag.msg new file mode 100644 index 00000000000..e166243cf1a --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-frag.msg @@ -0,0 +1,90 @@ +MIME-Version: 1.0 +From: Lord John Whorfin +To: +Subject: A complex nested multipart example +Content-Type: multipart/mixed; + boundary=unique-boundary-1 + +The preamble of the outer multipart message. +Mail readers that understand multipart format +should ignore this preamble. +If you are reading this text, you might want to +consider changing to a mail reader that understands +how to properly display multipart messages. +--unique-boundary-1 + +Part 1 of the outer message. +[Note that the preceding blank line means +no header fields were given and this is text, +with charset US ASCII. It could have been +done with explicit typing as in the next part.] + +--unique-boundary-1 +Content-type: text/plain; charset=US-ASCII + +Part 2 of the outer message. +This could have been part of the previous part, +but illustrates explicit versus implicit +typing of body parts. + +--unique-boundary-1 +Subject: Part 3 of the outer message is multipart! +Content-Type: multipart/parallel; + boundary=unique-boundary-2 + +A one-line preamble for the inner multipart message. +--unique-boundary-2 +Content-Type: image/gif +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="3d-vise.gif" +Subject: Part 1 of the inner message is a GIF, "3d-vise.gif" + +R0lGODdhKAAoAOMAAAAAAAAAgB6Q/y9PT25ubnCAkKBSLb6+vufn5/Xes/+lAP/6zQAAAAAA +AAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJLMOyYbcoxkaZ5oCkoH6L5wLMfiWqd4btZhmxbA +oFCY47EIqMJgyWw2ATjj7aRkAq5YwDMl9VGtKO0SiuoiTVlscsxt9c4HgXxUIA0EAVOVfDKT +8Hl1B3kDAYYle202XnGGgoMHhYckiWVuR3+OTgCGeZRslotwgJ2lnYigfZdTjQULr7ALBZN0 +qTurjHgLKAu0B5Wqopm7J72etQN8t8Ijury+wMtvw8/Hv7Ylfs0BxCbGqMmK0yOOQ0GTCgrR +2bhwJGlXJQYG6mMKoeNoWSbzCWIACe5JwxQm3AkDAbUAQCiQhDZEBeBl6afgCsOBrD45edIv +QceGWSMevpOYhl6CkydBHhBZQmGKjihVshypjB9ClAHZMTugzOU7mzhBPiSZ5uDNnA7b/aTZ +0mhMnfl0pDBFa6bUElSPWb0qtYuHrxlwcR17YsWMs2jTql3LFkQEADs= +--unique-boundary-2 +Content-Type: image/gif +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="3d-eye.gif" +Subject: Part 2 of the inner message is another GIF, "3d-eye.gif", but + the terminating boundary is bad! + +R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAA +AAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7 +VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7 ++3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatL +rU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJ +vs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjw +E0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7 +XXXXXX--unique-boundary-2-- + +The epilogue for the inner multipart message. + +--unique-boundary-1 +Content-type: text/richtext + +This is part 4 of the outer message +as defined in RFC1341 + +Isn't it cool? + +--unique-boundary-1 +Content-Type: message/rfc822; name="nice.name"; + +From: (mailbox in US-ASCII) +To: (address in US-ASCII) +Subject: Part 5 of the outer message is itself an RFC822 message! +Content-Type: Text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: Quoted-printable + +Part 5 of the outer message is itself an RFC822 message! + +--unique-boundary-1-- + +The epilogue for the outer message. + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-igor-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/multi-igor-expected.json new file mode 100644 index 00000000000..9c4c6c7a947 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-igor-expected.json @@ -0,0 +1,61 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "-490585488-806670346-834061839=:2195", + "alternativeBoundary": null, + "sender": { + "name": "Starovoitov Igor", + "mailAddress": "igor@fripp.aic.synapse.ru", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "eryq@rhine.gsfc.nasa.gov", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 834061839000, + "subject": "Need help", + "plainBodyText": "Dear Sir,\r\n\r\nI have a problem with Your MIME-Parser-1.9\r\nand multipart-nested messages. \r\nNot all parts are parsed.\r\n\r\nHere my Makefile, Your own multipart-nested.msg\r\nand its out after \"make test\".\r\nSome my messages not completely parsed too. \r\n\r\nIs this a bug?\r\n\r\nThank You for help.\r\n\r\n\r\nIgor Starovoytov.", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "Makefile", + "data": "Iy0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQ0KIyBNYWtlZmlsZSBmb3IgTUlNRTo6DQojLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQoNCiMgV2hlcmUgdG8gaW5zdGFsbCB0aGUgbGlicmFyaWVzOg0KU0lURV9QRVJMID0gL3Vzci9saWIvcGVybDUNCg0KIyBXaGF0IFBlcmw1IGlzIGNhbGxlZCBvbiB5b3VyIHN5c3RlbSAobm8gbmVlZCB0byBnaXZlIGVudGlyZSBwYXRoKToNClBFUkw1ICAgICA9IHBlcmwNCg0KIyBZb3UgcHJvYmFibHkgd29uJ3QgbmVlZCB0byBjaGFuZ2UgdGhlc2UuLi4NCk1PRFMgICAgICA9IERlY29kZXIucG0gRW50aXR5LnBtIEhlYWQucG0gUGFyc2VyLnBtIEJhc2U2NC5wbSBRdW90ZWRQcmludC5wbQ0KU0hFTEwgICAgID0gL2Jpbi9zaA0KDQojLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQojIEZvciBpbnN0YWxsZXJzLi4uDQojLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQoNCmhlbHA6CQ0KCUBlY2hvICJWYWxpZCB0YXJnZXRzOiB0ZXN0IGNsZWFuIGluc3RhbGwiDQoNCmNsZWFuOg0KCXJtIC1mIHRlc3RvdXQvKg0KDQp0ZXN0Og0KIwlAZWNobyAiVEVTVElORyBIZWFkLnBtLi4uIg0KIwkke1BFUkw1fSBNSU1FL0hlYWQucG0gICA8IHRlc3Rpbi9maXJzdC5oZHIgICAgICAgPiB0ZXN0b3V0L0hlYWQub3V0DQojCUBlY2hvICJURVNUSU5HIERlY29kZXIucG0uLi4iDQojCSR7UEVSTDV9IE1JTUUvRGVjb2Rlci5wbSA8IHRlc3Rpbi9xdW90LXByaW50LmJvZHkgPiB0ZXN0b3V0L0RlY29kZXIub3V0DQojCUBlY2hvICJURVNUSU5HIFBhcnNlci5wbSAoc2ltcGxlKS4uLiINCiMJJHtQRVJMNX0gTUlNRS9QYXJzZXIucG0gPCB0ZXN0aW4vc2ltcGxlLm1zZyAgICAgID4gdGVzdG91dC9QYXJzZXIucy5vdXQNCiMJQGVjaG8gIlRFU1RJTkcgUGFyc2VyLnBtIChtdWx0aXBhcnQpLi4uIg0KIwkke1BFUkw1fSBNSU1FL1BhcnNlci5wbSA8IHRlc3Rpbi9tdWx0aS0yZ2lmcy5tc2cgPiB0ZXN0b3V0L1BhcnNlci5tLm91dA0KCUBlY2hvICJURVNUSU5HIFBhcnNlci5wbSAobXVsdGlfbmVzdGVkLm1zZykuLi4iDQoJJHtQRVJMNX0gTUlNRS9QYXJzZXIucG0gPCB0ZXN0aW4vbXVsdGktbmVzdGVkLm1zZyA+IHRlc3RvdXQvUGFyc2VyLm4ub3V0DQoJQGVjaG8gIkFsbCB0ZXN0cyBwYXNzZWQuLi4gc2VlIC4vdGVzdG91dC9NT0RVTEUqLm91dCBmb3Igb3V0cHV0Ig0KDQppbnN0YWxsOg0KCUBpZiBbICEgLWQgJHtTSVRFX1BFUkx9IF07IHRoZW4gXA0KCSAgICBlY2hvICJQbGVhc2UgZWRpdCB0aGUgU0lURV9QRVJMIGluIHlvdXIgTWFrZWZpbGUiOyBleGl0IC0xOyBcDQogICAgICAgIGZpICAgICAgICAgIA0KCUBpZiBbICEgLXcgJHtTSVRFX1BFUkx9IF07IHRoZW4gXA0KCSAgICBlY2hvICJObyBwZXJtaXNzaW9uLi4uIHNob3VsZCB5b3UgYmUgcm9vdD8iOyBleGl0IC0xOyBcDQogICAgICAgIGZpICAgICAgICAgIA0KCUBpZiBbICEgLWQgJHtTSVRFX1BFUkx9L01JTUUgXTsgdGhlbiBcDQoJICAgIG1rZGlyICR7U0lURV9QRVJMfS9NSU1FOyBcDQogICAgICAgIGZpDQoJaW5zdGFsbCAtbSAwNjQ0IE1JTUUvKi5wbSAke1NJVEVfUEVSTH0vTUlNRQ0KDQoNCiMtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NCiMgRm9yIGRldmVsb3BlciBvbmx5Li4uDQojLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQoNClBPRDJIVE1MX0ZMQUdTID0gLS1wb2RwYXRoPS4gLS1mbHVzaCAtLWh0bWxyb290PS4uDQpIVE1MUyAgICAgICAgICA9ICR7TU9EUzoucG09Lmh0bWx9DQpWUEFUSCAgICAgICAgICA9IE1JTUUNCg0KLlNVRkZJWEVTOiAucG0gLnBvZCAuaHRtbA0KDQojIHYuMS44IGdlbmVyYXRlZCAzMCBBcHIgOTYNCiMgdi4xLjkgaXMgb25seSBiZWNhdXNlIDEuOCBmYWlsZWQgQ1BBTiBpbmdlc3Rpb24NCmRpc3Q6IGRvY3VtZW50ZWQJDQoJVkVSU0lPTj0xLjkgOyBcDQoJbWtkaXN0IC10Z3ogTUlNRS1wYXJzZXItJCRWRVJTSU9OIDsgXA0KCWNwIE1LRElTVC9NSU1FLXBhcnNlci0kJFZFUlNJT04udGd6ICR7SE9NRX0vcHVibGljX2h0bWwvY3Bhbg0KCQ0KZG9jdW1lbnRlZDogJHtIVE1MU30gJHtNT0RTfQ0KDQoucG0uaHRtbDoNCglwb2QyaHRtbCAke1BPRDJIVE1MX0ZMQUdTfSBcDQoJCS0tdGl0bGU9TUlNRTo6JCogXA0KCQktLWluZmlsZT0kPCBcDQoJCS0tb3V0ZmlsZT1kb2NzLyQqLmh0bWwNCg0KIy0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQ0K", + "mimeType": "text/plain", + "charset": "us-ascii", + "contentId": "Pine.LNX.3.91.960606155039.2195B@fripp.aic.synapse.ru", + "calendarMethod": null + }, + { + "name": "multi-nested.msg", + "data": "TUlNRS1WZXJzaW9uOiAxLjANCkZyb206IExvcmQgSm9obiBXaG9yZmluIDx3aG9yZmluQHlveW9keW5lLmNvbT4NClRvOiA8am9obi15YXlhQHlveW9keW5lLmNvbT4NClN1YmplY3Q6IEEgY29tcGxleCBuZXN0ZWQgbXVsdGlwYXJ0IGV4YW1wbGUNCkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L21peGVkOw0KICAgICBib3VuZGFyeT11bmlxdWUtYm91bmRhcnktMQ0KDQpUaGUgcHJlYW1ibGUgb2YgdGhlIG91dGVyIG11bHRpcGFydCBtZXNzYWdlLg0KTWFpbCByZWFkZXJzIHRoYXQgdW5kZXJzdGFuZCBtdWx0aXBhcnQgZm9ybWF0DQpzaG91bGQgaWdub3JlIHRoaXMgcHJlYW1ibGUuDQpJZiB5b3UgYXJlIHJlYWRpbmcgdGhpcyB0ZXh0LCB5b3UgbWlnaHQgd2FudCB0bw0KY29uc2lkZXIgY2hhbmdpbmcgdG8gYSBtYWlsIHJlYWRlciB0aGF0IHVuZGVyc3RhbmRzDQpob3cgdG8gcHJvcGVybHkgZGlzcGxheSBtdWx0aXBhcnQgbWVzc2FnZXMuDQotLXVuaXF1ZS1ib3VuZGFyeS0xDQoNClBhcnQgMSBvZiB0aGUgb3V0ZXIgbWVzc2FnZS4NCltOb3RlIHRoYXQgdGhlIHByZWNlZGluZyBibGFuayBsaW5lIG1lYW5zDQpubyBoZWFkZXIgZmllbGRzIHdlcmUgZ2l2ZW4gYW5kIHRoaXMgaXMgdGV4dCwNCndpdGggY2hhcnNldCBVUyBBU0NJSS4gIEl0IGNvdWxkIGhhdmUgYmVlbg0KZG9uZSB3aXRoIGV4cGxpY2l0IHR5cGluZyBhcyBpbiB0aGUgbmV4dCBwYXJ0Ll0NCg0KLS11bmlxdWUtYm91bmRhcnktMQ0KQ29udGVudC10eXBlOiB0ZXh0L3BsYWluOyBjaGFyc2V0PVVTLUFTQ0lJDQoNClBhcnQgMiBvZiB0aGUgb3V0ZXIgbWVzc2FnZS4NClRoaXMgY291bGQgaGF2ZSBiZWVuIHBhcnQgb2YgdGhlIHByZXZpb3VzIHBhcnQsDQpidXQgaWxsdXN0cmF0ZXMgZXhwbGljaXQgdmVyc3VzIGltcGxpY2l0DQp0eXBpbmcgb2YgYm9keSBwYXJ0cy4NCg0KLS11bmlxdWUtYm91bmRhcnktMQ0KU3ViamVjdDogUGFydCAzIG9mIHRoZSBvdXRlciBtZXNzYWdlIGlzIG11bHRpcGFydCENCkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L3BhcmFsbGVsOw0KICAgICBib3VuZGFyeT11bmlxdWUtYm91bmRhcnktMg0KDQpBIG9uZS1saW5lIHByZWFtYmxlIGZvciB0aGUgaW5uZXIgbXVsdGlwYXJ0IG1lc3NhZ2UuDQotLXVuaXF1ZS1ib3VuZGFyeS0yDQpDb250ZW50LVR5cGU6IGltYWdlL2dpZg0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogYmFzZTY0DQpDb250ZW50LURpc3Bvc2l0aW9uOiBpbmxpbmU7IGZpbGVuYW1lPSIzZC1jb21wcmVzcy5naWYiDQpTdWJqZWN0OiBQYXJ0IDEgb2YgdGhlIGlubmVyIG1lc3NhZ2UgaXMgYSBHSUYsICIzZC1jb21wcmVzcy5naWYiDQoNClIwbEdPRGRoS0FBb0FPTUFBQUFBQUFBQWdCNlEveTlQVDI1dWJuQ0FrS0JTTGI2K3Z1Zm41L1hlcy8rbEFQLzZ6UUFBQUFBQQ0KQUFBQUFBQUFBQ3dBQUFBQUtBQW9BQUFFL2hESlNhdTllSkxNT3lZYmNveGthWjVvQ2tvSDZMNXdMTWZpV3FkNGJ0WmhteGJBDQpvRkNZNDdFSXFNSmd5V3cyQVRqajdhUmtBcTVZd0RNbDlWR3RLTzBTaXVvaVRWbHNjc3h0OWM0SGdYeFVJQTBFQVZPVmZES1QNCjhIbDFCM2tEQVlZbGUyMDJYbkdHZ29NSGhZY2tpV1Z1UjMrT1RnQ0dlWlJzbG90d2dKMmxuWWlnZlpkVGpRVUxyN0FMQlpOMA0KcVR1cmpIZ0xLQXUwQjVXcW9wbTdKNzJldFFOOHQ4SWp1cnkrd010dnc4L0h2N1lsZnMwQnhDYkdxTW1LMHlPT1EwR1RDZ3JSDQoyYmh3SkdsWEpRWUc2bU1Lb2VOb1dTYnpDV0lBQ2U1Snd4UW0zQWtEQWJVQVFDaVFoRFpFQmVCbDZhZmdDc09CckQ0NWVkSXYNClFjZUdXU01ldnBPWWhsNkNreWRCSGhCWlFtR0tqaWhWc2h5cGpCOUNsQUhaTVR1Z3pPVTdtemhCUGlTWjV1RE5uQTdiL2FUWg0KMG1oTW5mbDBwREJGYTZiVUVsU1BXYjBxdFl1SHJ4bHdjUjE3WXNXTXMyalRxbDNMRmtRRUFEcz0NCi0tdW5pcXVlLWJvdW5kYXJ5LTINCkNvbnRlbnQtVHlwZTogaW1hZ2UvZ2lmDQpDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiBiYXNlNjQNCkNvbnRlbnQtRGlzcG9zaXRpb246IGlubGluZTsgZmlsZW5hbWU9IjNkLWV5ZS5naWYiDQpTdWJqZWN0OiBQYXJ0IDIgb2YgdGhlIGlubmVyIG1lc3NhZ2UgaXMgYW5vdGhlciBHSUYsICIzZC1leWUuZ2lmIg0KDQpSMGxHT0RkaEtBQW9BUE1BQUFBQUFBQUF6TjN1Lzc2K3ZvaUlpRzV1YnN6ZDd2Ly8vK2ZuNXdBQUFBQUFBQUFBQUFBQUFBQUENCkFBQUFBQUFBQUN3QUFBQUFLQUFvQUFBRS9oREpTYXU5ZUpiTU95NGJNb3hrYVo1b0Nrb0Q2TDV3TE1maVduczQxb1p0N2xNNw0KVnVqbkM5NklSVnNQV1FFNG54UGprdm1zUW11OG9jL0tCVVNWV2s3WGVwR0dMZU5yeG94Sk8xTWpJTGp0aGcva1dYUTZ3Ty83DQorM2RDZVJSamZBS0hpSW1KQVYrRENGMEJpVzVWQW8xQ0VsYVJoNU5qbGtlWW1weVRncGNUQUtHaWFhU2Zwd0twVlFheFZhdEwNCnJVOEdhUWRPQkFRQUI3K3lYbGlYVHJnQXhzVzR2RmFidjhCT3RCc0J0N2NHdndDSVQ5bk95TkVJeHVDNHpycUt6YzlYYk9ESg0KdnM3WTVld0gzZDdGeGUzakI0cmo4dDZQdU5hNnIyYmhLUVhOMTdGWUNCTXFUR2lCelNOaHg1ZzBuRU1obHNTSmppUll2RGp3DQpFMGNkR3hRL2dzd29zb0tVa211VTJGbkpjc1NLR1RCanlweEpzeWFJQ0FBNw0KLS11bmlxdWUtYm91bmRhcnktMi0tDQoNClRoZSBlcGlsb2d1ZSBmb3IgdGhlIGlubmVyIG11bHRpcGFydCBtZXNzYWdlLg0KDQotLXVuaXF1ZS1ib3VuZGFyeS0xDQpDb250ZW50LXR5cGU6IHRleHQvcmljaHRleHQNCg0KVGhpcyBpcyA8Ym9sZD5wYXJ0IDQgb2YgdGhlIG91dGVyIG1lc3NhZ2U8L2JvbGQ+DQo8c21hbGxlcj5hcyBkZWZpbmVkIGluIFJGQzEzNDE8L3NtYWxsZXI+PG5sPg0KPG5sPg0KSXNuJ3QgaXQgPGJpZ2dlcj48YmlnZ2VyPmNvb2w/PC9iaWdnZXI+PC9iaWdnZXI+DQoNCi0tdW5pcXVlLWJvdW5kYXJ5LTENCkNvbnRlbnQtVHlwZTogbWVzc2FnZS9yZmM4MjINCg0KRnJvbTogKG1haWxib3ggaW4gVVMtQVNDSUkpDQpUbzogKGFkZHJlc3MgaW4gVVMtQVNDSUkpDQpTdWJqZWN0OiBQYXJ0IDUgb2YgdGhlIG91dGVyIG1lc3NhZ2UgaXMgaXRzZWxmIGFuIFJGQzgyMiBtZXNzYWdlIQ0KQ29udGVudC1UeXBlOiBUZXh0L3BsYWluOyBjaGFyc2V0PUlTTy04ODU5LTENCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IFF1b3RlZC1wcmludGFibGUNCg0KUGFydCA1IG9mIHRoZSBvdXRlciBtZXNzYWdlIGlzIGl0c2VsZiBhbiBSRkM4MjIgbWVzc2FnZSENCg0KLS11bmlxdWUtYm91bmRhcnktMS0tDQoNClRoZSBlcGlsb2d1ZSBmb3IgdGhlIG91dGVyIG1lc3NhZ2UuDQo=", + "mimeType": "text/plain", + "charset": "us-ascii", + "contentId": "Pine.LNX.3.91.960606155039.2195C@fripp.aic.synapse.ru", + "calendarMethod": null + }, + { + "name": "Parser.n.out", + "data": "KiBXYWl0aW5nIGZvciBhIE1JTUUgbWVzc2FnZSBmcm9tIFNURElOLi4uDQo9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCkNvbnRlbnQtdHlwZTogbXVsdGlwYXJ0L21peGVkDQpCb2R5LWZpbGU6IE5PTkUNClN1YmplY3Q6IEEgY29tcGxleCBuZXN0ZWQgbXVsdGlwYXJ0IGV4YW1wbGUNCk51bS1wYXJ0czogMw0KLS0NCiAgICBDb250ZW50LXR5cGU6IHRleHQvcGxhaW4NCiAgICBCb2R5LWZpbGU6IC4vdGVzdG91dC9tc2ctMzUzOC0xLmRvYw0KICAgIC0tDQogICAgQ29udGVudC10eXBlOiB0ZXh0L3BsYWluDQogICAgQm9keS1maWxlOiAuL3Rlc3RvdXQvbXNnLTM1MzgtMi5kb2MNCiAgICAtLQ0KICAgIENvbnRlbnQtdHlwZTogbXVsdGlwYXJ0L3BhcmFsbGVsDQogICAgQm9keS1maWxlOiBOT05FDQogICAgU3ViamVjdDogUGFydCAzIG9mIHRoZSBvdXRlciBtZXNzYWdlIGlzIG11bHRpcGFydCENCiAgICBOdW0tcGFydHM6IDINCiAgICAtLQ0KICAgICAgICBDb250ZW50LXR5cGU6IGltYWdlL2dpZg0KICAgICAgICBCb2R5LWZpbGU6IC4vdGVzdG91dC8zZC1jb21wcmVzcy5naWYNCiAgICAgICAgU3ViamVjdDogUGFydCAxIG9mIHRoZSBpbm5lciBtZXNzYWdlIGlzIGEgR0lGLCAiM2QtY29tcHJlc3MuZ2lmIg0KICAgICAgICAtLQ0KICAgICAgICBDb250ZW50LXR5cGU6IGltYWdlL2dpZg0KICAgICAgICBCb2R5LWZpbGU6IC4vdGVzdG91dC8zZC1leWUuZ2lmDQogICAgICAgIFN1YmplY3Q6IFBhcnQgMiBvZiB0aGUgaW5uZXIgbWVzc2FnZSBpcyBhbm90aGVyIEdJRiwgIjNkLWV5ZS5naWYiDQogICAgICAgIC0tDQo9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCg0K", + "mimeType": "text/plain", + "charset": "us-ascii", + "contentId": "Pine.LNX.3.91.960606155039.2195D@fripp.aic.synapse.ru", + "calendarMethod": null + } + ], + "mailHeaders": "Date: Thu, 6 Jun 1996 15:50:39 +0400 (MOW DST)\nFrom: Starovoitov Igor \nTo: eryq@rhine.gsfc.nasa.gov\nSubject: Need help\nMIME-Version: 1.0\nContent-Type: MULTIPART/MIXED; BOUNDARY=\"-490585488-806670346-834061839=:2195\"", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-igor.msg b/packages/node-mimimi/test/mimetools-testmsgs/multi-igor.msg new file mode 100644 index 00000000000..195cf489dbd --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-igor.msg @@ -0,0 +1,198 @@ +Date: Thu, 6 Jun 1996 15:50:39 +0400 (MOW DST) +From: Starovoitov Igor +To: eryq@rhine.gsfc.nasa.gov +Subject: Need help +MIME-Version: 1.0 +Content-Type: MULTIPART/MIXED; BOUNDARY="-490585488-806670346-834061839=:2195" + + This message is in MIME format. The first part should be readable text, + while the remaining parts are likely unreadable without MIME-aware tools. + Send mail to mime@docserver.cac.washington.edu for more info. + +---490585488-806670346-834061839=:2195 +Content-Type: TEXT/PLAIN; charset=US-ASCII + +Dear Sir, + +I have a problem with Your MIME-Parser-1.9 +and multipart-nested messages. +Not all parts are parsed. + +Here my Makefile, Your own multipart-nested.msg +and its out after "make test". +Some my messages not completely parsed too. + +Is this a bug? + +Thank You for help. + + +Igor Starovoytov. +---490585488-806670346-834061839=:2195 +Content-Type: TEXT/PLAIN; charset=US-ASCII; name=Makefile +Content-Transfer-Encoding: BASE64 +Content-ID: +Content-Description: Makefile + +Iy0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t +LS0tLS0tLS0tLS0tLS0tLQ0KIyBNYWtlZmlsZSBmb3IgTUlNRTo6DQojLS0t +LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t +LS0tLS0tLS0tLS0tDQoNCiMgV2hlcmUgdG8gaW5zdGFsbCB0aGUgbGlicmFy +aWVzOg0KU0lURV9QRVJMID0gL3Vzci9saWIvcGVybDUNCg0KIyBXaGF0IFBl +cmw1IGlzIGNhbGxlZCBvbiB5b3VyIHN5c3RlbSAobm8gbmVlZCB0byBnaXZl +IGVudGlyZSBwYXRoKToNClBFUkw1ICAgICA9IHBlcmwNCg0KIyBZb3UgcHJv +YmFibHkgd29uJ3QgbmVlZCB0byBjaGFuZ2UgdGhlc2UuLi4NCk1PRFMgICAg +ICA9IERlY29kZXIucG0gRW50aXR5LnBtIEhlYWQucG0gUGFyc2VyLnBtIEJh +c2U2NC5wbSBRdW90ZWRQcmludC5wbQ0KU0hFTEwgICAgID0gL2Jpbi9zaA0K +DQojLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t +LS0tLS0tLS0tLS0tLS0tLS0tDQojIEZvciBpbnN0YWxsZXJzLi4uDQojLS0t +LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t +LS0tLS0tLS0tLS0tDQoNCmhlbHA6CQ0KCUBlY2hvICJWYWxpZCB0YXJnZXRz +OiB0ZXN0IGNsZWFuIGluc3RhbGwiDQoNCmNsZWFuOg0KCXJtIC1mIHRlc3Rv +dXQvKg0KDQp0ZXN0Og0KIwlAZWNobyAiVEVTVElORyBIZWFkLnBtLi4uIg0K +Iwkke1BFUkw1fSBNSU1FL0hlYWQucG0gICA8IHRlc3Rpbi9maXJzdC5oZHIg +ICAgICAgPiB0ZXN0b3V0L0hlYWQub3V0DQojCUBlY2hvICJURVNUSU5HIERl +Y29kZXIucG0uLi4iDQojCSR7UEVSTDV9IE1JTUUvRGVjb2Rlci5wbSA8IHRl +c3Rpbi9xdW90LXByaW50LmJvZHkgPiB0ZXN0b3V0L0RlY29kZXIub3V0DQoj +CUBlY2hvICJURVNUSU5HIFBhcnNlci5wbSAoc2ltcGxlKS4uLiINCiMJJHtQ +RVJMNX0gTUlNRS9QYXJzZXIucG0gPCB0ZXN0aW4vc2ltcGxlLm1zZyAgICAg +ID4gdGVzdG91dC9QYXJzZXIucy5vdXQNCiMJQGVjaG8gIlRFU1RJTkcgUGFy +c2VyLnBtIChtdWx0aXBhcnQpLi4uIg0KIwkke1BFUkw1fSBNSU1FL1BhcnNl +ci5wbSA8IHRlc3Rpbi9tdWx0aS0yZ2lmcy5tc2cgPiB0ZXN0b3V0L1BhcnNl +ci5tLm91dA0KCUBlY2hvICJURVNUSU5HIFBhcnNlci5wbSAobXVsdGlfbmVz +dGVkLm1zZykuLi4iDQoJJHtQRVJMNX0gTUlNRS9QYXJzZXIucG0gPCB0ZXN0 +aW4vbXVsdGktbmVzdGVkLm1zZyA+IHRlc3RvdXQvUGFyc2VyLm4ub3V0DQoJ +QGVjaG8gIkFsbCB0ZXN0cyBwYXNzZWQuLi4gc2VlIC4vdGVzdG91dC9NT0RV +TEUqLm91dCBmb3Igb3V0cHV0Ig0KDQppbnN0YWxsOg0KCUBpZiBbICEgLWQg +JHtTSVRFX1BFUkx9IF07IHRoZW4gXA0KCSAgICBlY2hvICJQbGVhc2UgZWRp +dCB0aGUgU0lURV9QRVJMIGluIHlvdXIgTWFrZWZpbGUiOyBleGl0IC0xOyBc +DQogICAgICAgIGZpICAgICAgICAgIA0KCUBpZiBbICEgLXcgJHtTSVRFX1BF +Ukx9IF07IHRoZW4gXA0KCSAgICBlY2hvICJObyBwZXJtaXNzaW9uLi4uIHNo +b3VsZCB5b3UgYmUgcm9vdD8iOyBleGl0IC0xOyBcDQogICAgICAgIGZpICAg +ICAgICAgIA0KCUBpZiBbICEgLWQgJHtTSVRFX1BFUkx9L01JTUUgXTsgdGhl +biBcDQoJICAgIG1rZGlyICR7U0lURV9QRVJMfS9NSU1FOyBcDQogICAgICAg +IGZpDQoJaW5zdGFsbCAtbSAwNjQ0IE1JTUUvKi5wbSAke1NJVEVfUEVSTH0v +TUlNRQ0KDQoNCiMtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t +LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NCiMgRm9yIGRldmVsb3BlciBv +bmx5Li4uDQojLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t +LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQoNClBPRDJIVE1MX0ZMQUdTID0g +LS1wb2RwYXRoPS4gLS1mbHVzaCAtLWh0bWxyb290PS4uDQpIVE1MUyAgICAg +ICAgICA9ICR7TU9EUzoucG09Lmh0bWx9DQpWUEFUSCAgICAgICAgICA9IE1J +TUUNCg0KLlNVRkZJWEVTOiAucG0gLnBvZCAuaHRtbA0KDQojIHYuMS44IGdl +bmVyYXRlZCAzMCBBcHIgOTYNCiMgdi4xLjkgaXMgb25seSBiZWNhdXNlIDEu +OCBmYWlsZWQgQ1BBTiBpbmdlc3Rpb24NCmRpc3Q6IGRvY3VtZW50ZWQJDQoJ +VkVSU0lPTj0xLjkgOyBcDQoJbWtkaXN0IC10Z3ogTUlNRS1wYXJzZXItJCRW +RVJTSU9OIDsgXA0KCWNwIE1LRElTVC9NSU1FLXBhcnNlci0kJFZFUlNJT04u +dGd6ICR7SE9NRX0vcHVibGljX2h0bWwvY3Bhbg0KCQ0KZG9jdW1lbnRlZDog +JHtIVE1MU30gJHtNT0RTfQ0KDQoucG0uaHRtbDoNCglwb2QyaHRtbCAke1BP +RDJIVE1MX0ZMQUdTfSBcDQoJCS0tdGl0bGU9TUlNRTo6JCogXA0KCQktLWlu +ZmlsZT0kPCBcDQoJCS0tb3V0ZmlsZT1kb2NzLyQqLmh0bWwNCg0KIy0tLS0t +LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t +LS0tLS0tLS0tLQ0K +---490585488-806670346-834061839=:2195 +Content-Type: TEXT/PLAIN; charset=US-ASCII; name="multi-nested.msg" +Content-Transfer-Encoding: BASE64 +Content-ID: +Content-Description: test message + +TUlNRS1WZXJzaW9uOiAxLjANCkZyb206IExvcmQgSm9obiBXaG9yZmluIDx3 +aG9yZmluQHlveW9keW5lLmNvbT4NClRvOiA8am9obi15YXlhQHlveW9keW5l +LmNvbT4NClN1YmplY3Q6IEEgY29tcGxleCBuZXN0ZWQgbXVsdGlwYXJ0IGV4 +YW1wbGUNCkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L21peGVkOw0KICAgICBi +b3VuZGFyeT11bmlxdWUtYm91bmRhcnktMQ0KDQpUaGUgcHJlYW1ibGUgb2Yg +dGhlIG91dGVyIG11bHRpcGFydCBtZXNzYWdlLg0KTWFpbCByZWFkZXJzIHRo +YXQgdW5kZXJzdGFuZCBtdWx0aXBhcnQgZm9ybWF0DQpzaG91bGQgaWdub3Jl +IHRoaXMgcHJlYW1ibGUuDQpJZiB5b3UgYXJlIHJlYWRpbmcgdGhpcyB0ZXh0 +LCB5b3UgbWlnaHQgd2FudCB0bw0KY29uc2lkZXIgY2hhbmdpbmcgdG8gYSBt +YWlsIHJlYWRlciB0aGF0IHVuZGVyc3RhbmRzDQpob3cgdG8gcHJvcGVybHkg +ZGlzcGxheSBtdWx0aXBhcnQgbWVzc2FnZXMuDQotLXVuaXF1ZS1ib3VuZGFy +eS0xDQoNClBhcnQgMSBvZiB0aGUgb3V0ZXIgbWVzc2FnZS4NCltOb3RlIHRo +YXQgdGhlIHByZWNlZGluZyBibGFuayBsaW5lIG1lYW5zDQpubyBoZWFkZXIg +ZmllbGRzIHdlcmUgZ2l2ZW4gYW5kIHRoaXMgaXMgdGV4dCwNCndpdGggY2hh +cnNldCBVUyBBU0NJSS4gIEl0IGNvdWxkIGhhdmUgYmVlbg0KZG9uZSB3aXRo +IGV4cGxpY2l0IHR5cGluZyBhcyBpbiB0aGUgbmV4dCBwYXJ0Ll0NCg0KLS11 +bmlxdWUtYm91bmRhcnktMQ0KQ29udGVudC10eXBlOiB0ZXh0L3BsYWluOyBj +aGFyc2V0PVVTLUFTQ0lJDQoNClBhcnQgMiBvZiB0aGUgb3V0ZXIgbWVzc2Fn +ZS4NClRoaXMgY291bGQgaGF2ZSBiZWVuIHBhcnQgb2YgdGhlIHByZXZpb3Vz +IHBhcnQsDQpidXQgaWxsdXN0cmF0ZXMgZXhwbGljaXQgdmVyc3VzIGltcGxp +Y2l0DQp0eXBpbmcgb2YgYm9keSBwYXJ0cy4NCg0KLS11bmlxdWUtYm91bmRh +cnktMQ0KU3ViamVjdDogUGFydCAzIG9mIHRoZSBvdXRlciBtZXNzYWdlIGlz +IG11bHRpcGFydCENCkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L3BhcmFsbGVs +Ow0KICAgICBib3VuZGFyeT11bmlxdWUtYm91bmRhcnktMg0KDQpBIG9uZS1s +aW5lIHByZWFtYmxlIGZvciB0aGUgaW5uZXIgbXVsdGlwYXJ0IG1lc3NhZ2Uu +DQotLXVuaXF1ZS1ib3VuZGFyeS0yDQpDb250ZW50LVR5cGU6IGltYWdlL2dp +Zg0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogYmFzZTY0DQpDb250ZW50 +LURpc3Bvc2l0aW9uOiBpbmxpbmU7IGZpbGVuYW1lPSIzZC1jb21wcmVzcy5n +aWYiDQpTdWJqZWN0OiBQYXJ0IDEgb2YgdGhlIGlubmVyIG1lc3NhZ2UgaXMg +YSBHSUYsICIzZC1jb21wcmVzcy5naWYiDQoNClIwbEdPRGRoS0FBb0FPTUFB +QUFBQUFBQWdCNlEveTlQVDI1dWJuQ0FrS0JTTGI2K3Z1Zm41L1hlcy8rbEFQ +LzZ6UUFBQUFBQQ0KQUFBQUFBQUFBQ3dBQUFBQUtBQW9BQUFFL2hESlNhdTll +SkxNT3lZYmNveGthWjVvQ2tvSDZMNXdMTWZpV3FkNGJ0WmhteGJBDQpvRkNZ +NDdFSXFNSmd5V3cyQVRqajdhUmtBcTVZd0RNbDlWR3RLTzBTaXVvaVRWbHNj +c3h0OWM0SGdYeFVJQTBFQVZPVmZES1QNCjhIbDFCM2tEQVlZbGUyMDJYbkdH +Z29NSGhZY2tpV1Z1UjMrT1RnQ0dlWlJzbG90d2dKMmxuWWlnZlpkVGpRVUxy +N0FMQlpOMA0KcVR1cmpIZ0xLQXUwQjVXcW9wbTdKNzJldFFOOHQ4SWp1cnkr +d010dnc4L0h2N1lsZnMwQnhDYkdxTW1LMHlPT1EwR1RDZ3JSDQoyYmh3Skds +WEpRWUc2bU1Lb2VOb1dTYnpDV0lBQ2U1Snd4UW0zQWtEQWJVQVFDaVFoRFpF +QmVCbDZhZmdDc09CckQ0NWVkSXYNClFjZUdXU01ldnBPWWhsNkNreWRCSGhC +WlFtR0tqaWhWc2h5cGpCOUNsQUhaTVR1Z3pPVTdtemhCUGlTWjV1RE5uQTdi +L2FUWg0KMG1oTW5mbDBwREJGYTZiVUVsU1BXYjBxdFl1SHJ4bHdjUjE3WXNX +TXMyalRxbDNMRmtRRUFEcz0NCi0tdW5pcXVlLWJvdW5kYXJ5LTINCkNvbnRl +bnQtVHlwZTogaW1hZ2UvZ2lmDQpDb250ZW50LVRyYW5zZmVyLUVuY29kaW5n +OiBiYXNlNjQNCkNvbnRlbnQtRGlzcG9zaXRpb246IGlubGluZTsgZmlsZW5h +bWU9IjNkLWV5ZS5naWYiDQpTdWJqZWN0OiBQYXJ0IDIgb2YgdGhlIGlubmVy +IG1lc3NhZ2UgaXMgYW5vdGhlciBHSUYsICIzZC1leWUuZ2lmIg0KDQpSMGxH +T0RkaEtBQW9BUE1BQUFBQUFBQUF6TjN1Lzc2K3ZvaUlpRzV1YnN6ZDd2Ly8v +K2ZuNXdBQUFBQUFBQUFBQUFBQUFBQUENCkFBQUFBQUFBQUN3QUFBQUFLQUFv +QUFBRS9oREpTYXU5ZUpiTU95NGJNb3hrYVo1b0Nrb0Q2TDV3TE1maVduczQx +b1p0N2xNNw0KVnVqbkM5NklSVnNQV1FFNG54UGprdm1zUW11OG9jL0tCVVNW +V2s3WGVwR0dMZU5yeG94Sk8xTWpJTGp0aGcva1dYUTZ3Ty83DQorM2RDZVJS +amZBS0hpSW1KQVYrRENGMEJpVzVWQW8xQ0VsYVJoNU5qbGtlWW1weVRncGNU +QUtHaWFhU2Zwd0twVlFheFZhdEwNCnJVOEdhUWRPQkFRQUI3K3lYbGlYVHJn +QXhzVzR2RmFidjhCT3RCc0J0N2NHdndDSVQ5bk95TkVJeHVDNHpycUt6YzlY +Yk9ESg0KdnM3WTVld0gzZDdGeGUzakI0cmo4dDZQdU5hNnIyYmhLUVhOMTdG +WUNCTXFUR2lCelNOaHg1ZzBuRU1obHNTSmppUll2RGp3DQpFMGNkR3hRL2dz +d29zb0tVa211VTJGbkpjc1NLR1RCanlweEpzeWFJQ0FBNw0KLS11bmlxdWUt +Ym91bmRhcnktMi0tDQoNClRoZSBlcGlsb2d1ZSBmb3IgdGhlIGlubmVyIG11 +bHRpcGFydCBtZXNzYWdlLg0KDQotLXVuaXF1ZS1ib3VuZGFyeS0xDQpDb250 +ZW50LXR5cGU6IHRleHQvcmljaHRleHQNCg0KVGhpcyBpcyA8Ym9sZD5wYXJ0 +IDQgb2YgdGhlIG91dGVyIG1lc3NhZ2U8L2JvbGQ+DQo8c21hbGxlcj5hcyBk +ZWZpbmVkIGluIFJGQzEzNDE8L3NtYWxsZXI+PG5sPg0KPG5sPg0KSXNuJ3Qg +aXQgPGJpZ2dlcj48YmlnZ2VyPmNvb2w/PC9iaWdnZXI+PC9iaWdnZXI+DQoN +Ci0tdW5pcXVlLWJvdW5kYXJ5LTENCkNvbnRlbnQtVHlwZTogbWVzc2FnZS9y +ZmM4MjINCg0KRnJvbTogKG1haWxib3ggaW4gVVMtQVNDSUkpDQpUbzogKGFk +ZHJlc3MgaW4gVVMtQVNDSUkpDQpTdWJqZWN0OiBQYXJ0IDUgb2YgdGhlIG91 +dGVyIG1lc3NhZ2UgaXMgaXRzZWxmIGFuIFJGQzgyMiBtZXNzYWdlIQ0KQ29u +dGVudC1UeXBlOiBUZXh0L3BsYWluOyBjaGFyc2V0PUlTTy04ODU5LTENCkNv +bnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IFF1b3RlZC1wcmludGFibGUNCg0K +UGFydCA1IG9mIHRoZSBvdXRlciBtZXNzYWdlIGlzIGl0c2VsZiBhbiBSRkM4 +MjIgbWVzc2FnZSENCg0KLS11bmlxdWUtYm91bmRhcnktMS0tDQoNClRoZSBl +cGlsb2d1ZSBmb3IgdGhlIG91dGVyIG1lc3NhZ2UuDQo= +---490585488-806670346-834061839=:2195 +Content-Type: TEXT/PLAIN; charset=US-ASCII; name="Parser.n.out" +Content-Transfer-Encoding: BASE64 +Content-ID: +Content-Description: out from parser + +KiBXYWl0aW5nIGZvciBhIE1JTUUgbWVzc2FnZSBmcm9tIFNURElOLi4uDQo9 +PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09 +PT09PT09PT09PT09PT0NCkNvbnRlbnQtdHlwZTogbXVsdGlwYXJ0L21peGVk +DQpCb2R5LWZpbGU6IE5PTkUNClN1YmplY3Q6IEEgY29tcGxleCBuZXN0ZWQg +bXVsdGlwYXJ0IGV4YW1wbGUNCk51bS1wYXJ0czogMw0KLS0NCiAgICBDb250 +ZW50LXR5cGU6IHRleHQvcGxhaW4NCiAgICBCb2R5LWZpbGU6IC4vdGVzdG91 +dC9tc2ctMzUzOC0xLmRvYw0KICAgIC0tDQogICAgQ29udGVudC10eXBlOiB0 +ZXh0L3BsYWluDQogICAgQm9keS1maWxlOiAuL3Rlc3RvdXQvbXNnLTM1Mzgt +Mi5kb2MNCiAgICAtLQ0KICAgIENvbnRlbnQtdHlwZTogbXVsdGlwYXJ0L3Bh +cmFsbGVsDQogICAgQm9keS1maWxlOiBOT05FDQogICAgU3ViamVjdDogUGFy +dCAzIG9mIHRoZSBvdXRlciBtZXNzYWdlIGlzIG11bHRpcGFydCENCiAgICBO +dW0tcGFydHM6IDINCiAgICAtLQ0KICAgICAgICBDb250ZW50LXR5cGU6IGlt +YWdlL2dpZg0KICAgICAgICBCb2R5LWZpbGU6IC4vdGVzdG91dC8zZC1jb21w +cmVzcy5naWYNCiAgICAgICAgU3ViamVjdDogUGFydCAxIG9mIHRoZSBpbm5l +ciBtZXNzYWdlIGlzIGEgR0lGLCAiM2QtY29tcHJlc3MuZ2lmIg0KICAgICAg +ICAtLQ0KICAgICAgICBDb250ZW50LXR5cGU6IGltYWdlL2dpZg0KICAgICAg +ICBCb2R5LWZpbGU6IC4vdGVzdG91dC8zZC1leWUuZ2lmDQogICAgICAgIFN1 +YmplY3Q6IFBhcnQgMiBvZiB0aGUgaW5uZXIgbWVzc2FnZSBpcyBhbm90aGVy +IEdJRiwgIjNkLWV5ZS5naWYiDQogICAgICAgIC0tDQo9PT09PT09PT09PT09 +PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09 +PT0NCg0K +---490585488-806670346-834061839=:2195-- diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-igor2-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/multi-igor2-expected.json new file mode 100644 index 00000000000..9c4c6c7a947 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-igor2-expected.json @@ -0,0 +1,61 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "-490585488-806670346-834061839=:2195", + "alternativeBoundary": null, + "sender": { + "name": "Starovoitov Igor", + "mailAddress": "igor@fripp.aic.synapse.ru", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "eryq@rhine.gsfc.nasa.gov", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 834061839000, + "subject": "Need help", + "plainBodyText": "Dear Sir,\r\n\r\nI have a problem with Your MIME-Parser-1.9\r\nand multipart-nested messages. \r\nNot all parts are parsed.\r\n\r\nHere my Makefile, Your own multipart-nested.msg\r\nand its out after \"make test\".\r\nSome my messages not completely parsed too. \r\n\r\nIs this a bug?\r\n\r\nThank You for help.\r\n\r\n\r\nIgor Starovoytov.", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "Makefile", + "data": "Iy0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQ0KIyBNYWtlZmlsZSBmb3IgTUlNRTo6DQojLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQoNCiMgV2hlcmUgdG8gaW5zdGFsbCB0aGUgbGlicmFyaWVzOg0KU0lURV9QRVJMID0gL3Vzci9saWIvcGVybDUNCg0KIyBXaGF0IFBlcmw1IGlzIGNhbGxlZCBvbiB5b3VyIHN5c3RlbSAobm8gbmVlZCB0byBnaXZlIGVudGlyZSBwYXRoKToNClBFUkw1ICAgICA9IHBlcmwNCg0KIyBZb3UgcHJvYmFibHkgd29uJ3QgbmVlZCB0byBjaGFuZ2UgdGhlc2UuLi4NCk1PRFMgICAgICA9IERlY29kZXIucG0gRW50aXR5LnBtIEhlYWQucG0gUGFyc2VyLnBtIEJhc2U2NC5wbSBRdW90ZWRQcmludC5wbQ0KU0hFTEwgICAgID0gL2Jpbi9zaA0KDQojLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQojIEZvciBpbnN0YWxsZXJzLi4uDQojLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQoNCmhlbHA6CQ0KCUBlY2hvICJWYWxpZCB0YXJnZXRzOiB0ZXN0IGNsZWFuIGluc3RhbGwiDQoNCmNsZWFuOg0KCXJtIC1mIHRlc3RvdXQvKg0KDQp0ZXN0Og0KIwlAZWNobyAiVEVTVElORyBIZWFkLnBtLi4uIg0KIwkke1BFUkw1fSBNSU1FL0hlYWQucG0gICA8IHRlc3Rpbi9maXJzdC5oZHIgICAgICAgPiB0ZXN0b3V0L0hlYWQub3V0DQojCUBlY2hvICJURVNUSU5HIERlY29kZXIucG0uLi4iDQojCSR7UEVSTDV9IE1JTUUvRGVjb2Rlci5wbSA8IHRlc3Rpbi9xdW90LXByaW50LmJvZHkgPiB0ZXN0b3V0L0RlY29kZXIub3V0DQojCUBlY2hvICJURVNUSU5HIFBhcnNlci5wbSAoc2ltcGxlKS4uLiINCiMJJHtQRVJMNX0gTUlNRS9QYXJzZXIucG0gPCB0ZXN0aW4vc2ltcGxlLm1zZyAgICAgID4gdGVzdG91dC9QYXJzZXIucy5vdXQNCiMJQGVjaG8gIlRFU1RJTkcgUGFyc2VyLnBtIChtdWx0aXBhcnQpLi4uIg0KIwkke1BFUkw1fSBNSU1FL1BhcnNlci5wbSA8IHRlc3Rpbi9tdWx0aS0yZ2lmcy5tc2cgPiB0ZXN0b3V0L1BhcnNlci5tLm91dA0KCUBlY2hvICJURVNUSU5HIFBhcnNlci5wbSAobXVsdGlfbmVzdGVkLm1zZykuLi4iDQoJJHtQRVJMNX0gTUlNRS9QYXJzZXIucG0gPCB0ZXN0aW4vbXVsdGktbmVzdGVkLm1zZyA+IHRlc3RvdXQvUGFyc2VyLm4ub3V0DQoJQGVjaG8gIkFsbCB0ZXN0cyBwYXNzZWQuLi4gc2VlIC4vdGVzdG91dC9NT0RVTEUqLm91dCBmb3Igb3V0cHV0Ig0KDQppbnN0YWxsOg0KCUBpZiBbICEgLWQgJHtTSVRFX1BFUkx9IF07IHRoZW4gXA0KCSAgICBlY2hvICJQbGVhc2UgZWRpdCB0aGUgU0lURV9QRVJMIGluIHlvdXIgTWFrZWZpbGUiOyBleGl0IC0xOyBcDQogICAgICAgIGZpICAgICAgICAgIA0KCUBpZiBbICEgLXcgJHtTSVRFX1BFUkx9IF07IHRoZW4gXA0KCSAgICBlY2hvICJObyBwZXJtaXNzaW9uLi4uIHNob3VsZCB5b3UgYmUgcm9vdD8iOyBleGl0IC0xOyBcDQogICAgICAgIGZpICAgICAgICAgIA0KCUBpZiBbICEgLWQgJHtTSVRFX1BFUkx9L01JTUUgXTsgdGhlbiBcDQoJICAgIG1rZGlyICR7U0lURV9QRVJMfS9NSU1FOyBcDQogICAgICAgIGZpDQoJaW5zdGFsbCAtbSAwNjQ0IE1JTUUvKi5wbSAke1NJVEVfUEVSTH0vTUlNRQ0KDQoNCiMtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NCiMgRm9yIGRldmVsb3BlciBvbmx5Li4uDQojLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQoNClBPRDJIVE1MX0ZMQUdTID0gLS1wb2RwYXRoPS4gLS1mbHVzaCAtLWh0bWxyb290PS4uDQpIVE1MUyAgICAgICAgICA9ICR7TU9EUzoucG09Lmh0bWx9DQpWUEFUSCAgICAgICAgICA9IE1JTUUNCg0KLlNVRkZJWEVTOiAucG0gLnBvZCAuaHRtbA0KDQojIHYuMS44IGdlbmVyYXRlZCAzMCBBcHIgOTYNCiMgdi4xLjkgaXMgb25seSBiZWNhdXNlIDEuOCBmYWlsZWQgQ1BBTiBpbmdlc3Rpb24NCmRpc3Q6IGRvY3VtZW50ZWQJDQoJVkVSU0lPTj0xLjkgOyBcDQoJbWtkaXN0IC10Z3ogTUlNRS1wYXJzZXItJCRWRVJTSU9OIDsgXA0KCWNwIE1LRElTVC9NSU1FLXBhcnNlci0kJFZFUlNJT04udGd6ICR7SE9NRX0vcHVibGljX2h0bWwvY3Bhbg0KCQ0KZG9jdW1lbnRlZDogJHtIVE1MU30gJHtNT0RTfQ0KDQoucG0uaHRtbDoNCglwb2QyaHRtbCAke1BPRDJIVE1MX0ZMQUdTfSBcDQoJCS0tdGl0bGU9TUlNRTo6JCogXA0KCQktLWluZmlsZT0kPCBcDQoJCS0tb3V0ZmlsZT1kb2NzLyQqLmh0bWwNCg0KIy0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQ0K", + "mimeType": "text/plain", + "charset": "us-ascii", + "contentId": "Pine.LNX.3.91.960606155039.2195B@fripp.aic.synapse.ru", + "calendarMethod": null + }, + { + "name": "multi-nested.msg", + "data": "TUlNRS1WZXJzaW9uOiAxLjANCkZyb206IExvcmQgSm9obiBXaG9yZmluIDx3aG9yZmluQHlveW9keW5lLmNvbT4NClRvOiA8am9obi15YXlhQHlveW9keW5lLmNvbT4NClN1YmplY3Q6IEEgY29tcGxleCBuZXN0ZWQgbXVsdGlwYXJ0IGV4YW1wbGUNCkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L21peGVkOw0KICAgICBib3VuZGFyeT11bmlxdWUtYm91bmRhcnktMQ0KDQpUaGUgcHJlYW1ibGUgb2YgdGhlIG91dGVyIG11bHRpcGFydCBtZXNzYWdlLg0KTWFpbCByZWFkZXJzIHRoYXQgdW5kZXJzdGFuZCBtdWx0aXBhcnQgZm9ybWF0DQpzaG91bGQgaWdub3JlIHRoaXMgcHJlYW1ibGUuDQpJZiB5b3UgYXJlIHJlYWRpbmcgdGhpcyB0ZXh0LCB5b3UgbWlnaHQgd2FudCB0bw0KY29uc2lkZXIgY2hhbmdpbmcgdG8gYSBtYWlsIHJlYWRlciB0aGF0IHVuZGVyc3RhbmRzDQpob3cgdG8gcHJvcGVybHkgZGlzcGxheSBtdWx0aXBhcnQgbWVzc2FnZXMuDQotLXVuaXF1ZS1ib3VuZGFyeS0xDQoNClBhcnQgMSBvZiB0aGUgb3V0ZXIgbWVzc2FnZS4NCltOb3RlIHRoYXQgdGhlIHByZWNlZGluZyBibGFuayBsaW5lIG1lYW5zDQpubyBoZWFkZXIgZmllbGRzIHdlcmUgZ2l2ZW4gYW5kIHRoaXMgaXMgdGV4dCwNCndpdGggY2hhcnNldCBVUyBBU0NJSS4gIEl0IGNvdWxkIGhhdmUgYmVlbg0KZG9uZSB3aXRoIGV4cGxpY2l0IHR5cGluZyBhcyBpbiB0aGUgbmV4dCBwYXJ0Ll0NCg0KLS11bmlxdWUtYm91bmRhcnktMQ0KQ29udGVudC10eXBlOiB0ZXh0L3BsYWluOyBjaGFyc2V0PVVTLUFTQ0lJDQoNClBhcnQgMiBvZiB0aGUgb3V0ZXIgbWVzc2FnZS4NClRoaXMgY291bGQgaGF2ZSBiZWVuIHBhcnQgb2YgdGhlIHByZXZpb3VzIHBhcnQsDQpidXQgaWxsdXN0cmF0ZXMgZXhwbGljaXQgdmVyc3VzIGltcGxpY2l0DQp0eXBpbmcgb2YgYm9keSBwYXJ0cy4NCg0KLS11bmlxdWUtYm91bmRhcnktMQ0KU3ViamVjdDogUGFydCAzIG9mIHRoZSBvdXRlciBtZXNzYWdlIGlzIG11bHRpcGFydCENCkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L3BhcmFsbGVsOw0KICAgICBib3VuZGFyeT11bmlxdWUtYm91bmRhcnktMg0KDQpBIG9uZS1saW5lIHByZWFtYmxlIGZvciB0aGUgaW5uZXIgbXVsdGlwYXJ0IG1lc3NhZ2UuDQotLXVuaXF1ZS1ib3VuZGFyeS0yDQpDb250ZW50LVR5cGU6IGltYWdlL2dpZg0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogYmFzZTY0DQpDb250ZW50LURpc3Bvc2l0aW9uOiBpbmxpbmU7IGZpbGVuYW1lPSIzZC1jb21wcmVzcy5naWYiDQpTdWJqZWN0OiBQYXJ0IDEgb2YgdGhlIGlubmVyIG1lc3NhZ2UgaXMgYSBHSUYsICIzZC1jb21wcmVzcy5naWYiDQoNClIwbEdPRGRoS0FBb0FPTUFBQUFBQUFBQWdCNlEveTlQVDI1dWJuQ0FrS0JTTGI2K3Z1Zm41L1hlcy8rbEFQLzZ6UUFBQUFBQQ0KQUFBQUFBQUFBQ3dBQUFBQUtBQW9BQUFFL2hESlNhdTllSkxNT3lZYmNveGthWjVvQ2tvSDZMNXdMTWZpV3FkNGJ0WmhteGJBDQpvRkNZNDdFSXFNSmd5V3cyQVRqajdhUmtBcTVZd0RNbDlWR3RLTzBTaXVvaVRWbHNjc3h0OWM0SGdYeFVJQTBFQVZPVmZES1QNCjhIbDFCM2tEQVlZbGUyMDJYbkdHZ29NSGhZY2tpV1Z1UjMrT1RnQ0dlWlJzbG90d2dKMmxuWWlnZlpkVGpRVUxyN0FMQlpOMA0KcVR1cmpIZ0xLQXUwQjVXcW9wbTdKNzJldFFOOHQ4SWp1cnkrd010dnc4L0h2N1lsZnMwQnhDYkdxTW1LMHlPT1EwR1RDZ3JSDQoyYmh3SkdsWEpRWUc2bU1Lb2VOb1dTYnpDV0lBQ2U1Snd4UW0zQWtEQWJVQVFDaVFoRFpFQmVCbDZhZmdDc09CckQ0NWVkSXYNClFjZUdXU01ldnBPWWhsNkNreWRCSGhCWlFtR0tqaWhWc2h5cGpCOUNsQUhaTVR1Z3pPVTdtemhCUGlTWjV1RE5uQTdiL2FUWg0KMG1oTW5mbDBwREJGYTZiVUVsU1BXYjBxdFl1SHJ4bHdjUjE3WXNXTXMyalRxbDNMRmtRRUFEcz0NCi0tdW5pcXVlLWJvdW5kYXJ5LTINCkNvbnRlbnQtVHlwZTogaW1hZ2UvZ2lmDQpDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiBiYXNlNjQNCkNvbnRlbnQtRGlzcG9zaXRpb246IGlubGluZTsgZmlsZW5hbWU9IjNkLWV5ZS5naWYiDQpTdWJqZWN0OiBQYXJ0IDIgb2YgdGhlIGlubmVyIG1lc3NhZ2UgaXMgYW5vdGhlciBHSUYsICIzZC1leWUuZ2lmIg0KDQpSMGxHT0RkaEtBQW9BUE1BQUFBQUFBQUF6TjN1Lzc2K3ZvaUlpRzV1YnN6ZDd2Ly8vK2ZuNXdBQUFBQUFBQUFBQUFBQUFBQUENCkFBQUFBQUFBQUN3QUFBQUFLQUFvQUFBRS9oREpTYXU5ZUpiTU95NGJNb3hrYVo1b0Nrb0Q2TDV3TE1maVduczQxb1p0N2xNNw0KVnVqbkM5NklSVnNQV1FFNG54UGprdm1zUW11OG9jL0tCVVNWV2s3WGVwR0dMZU5yeG94Sk8xTWpJTGp0aGcva1dYUTZ3Ty83DQorM2RDZVJSamZBS0hpSW1KQVYrRENGMEJpVzVWQW8xQ0VsYVJoNU5qbGtlWW1weVRncGNUQUtHaWFhU2Zwd0twVlFheFZhdEwNCnJVOEdhUWRPQkFRQUI3K3lYbGlYVHJnQXhzVzR2RmFidjhCT3RCc0J0N2NHdndDSVQ5bk95TkVJeHVDNHpycUt6YzlYYk9ESg0KdnM3WTVld0gzZDdGeGUzakI0cmo4dDZQdU5hNnIyYmhLUVhOMTdGWUNCTXFUR2lCelNOaHg1ZzBuRU1obHNTSmppUll2RGp3DQpFMGNkR3hRL2dzd29zb0tVa211VTJGbkpjc1NLR1RCanlweEpzeWFJQ0FBNw0KLS11bmlxdWUtYm91bmRhcnktMi0tDQoNClRoZSBlcGlsb2d1ZSBmb3IgdGhlIGlubmVyIG11bHRpcGFydCBtZXNzYWdlLg0KDQotLXVuaXF1ZS1ib3VuZGFyeS0xDQpDb250ZW50LXR5cGU6IHRleHQvcmljaHRleHQNCg0KVGhpcyBpcyA8Ym9sZD5wYXJ0IDQgb2YgdGhlIG91dGVyIG1lc3NhZ2U8L2JvbGQ+DQo8c21hbGxlcj5hcyBkZWZpbmVkIGluIFJGQzEzNDE8L3NtYWxsZXI+PG5sPg0KPG5sPg0KSXNuJ3QgaXQgPGJpZ2dlcj48YmlnZ2VyPmNvb2w/PC9iaWdnZXI+PC9iaWdnZXI+DQoNCi0tdW5pcXVlLWJvdW5kYXJ5LTENCkNvbnRlbnQtVHlwZTogbWVzc2FnZS9yZmM4MjINCg0KRnJvbTogKG1haWxib3ggaW4gVVMtQVNDSUkpDQpUbzogKGFkZHJlc3MgaW4gVVMtQVNDSUkpDQpTdWJqZWN0OiBQYXJ0IDUgb2YgdGhlIG91dGVyIG1lc3NhZ2UgaXMgaXRzZWxmIGFuIFJGQzgyMiBtZXNzYWdlIQ0KQ29udGVudC1UeXBlOiBUZXh0L3BsYWluOyBjaGFyc2V0PUlTTy04ODU5LTENCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IFF1b3RlZC1wcmludGFibGUNCg0KUGFydCA1IG9mIHRoZSBvdXRlciBtZXNzYWdlIGlzIGl0c2VsZiBhbiBSRkM4MjIgbWVzc2FnZSENCg0KLS11bmlxdWUtYm91bmRhcnktMS0tDQoNClRoZSBlcGlsb2d1ZSBmb3IgdGhlIG91dGVyIG1lc3NhZ2UuDQo=", + "mimeType": "text/plain", + "charset": "us-ascii", + "contentId": "Pine.LNX.3.91.960606155039.2195C@fripp.aic.synapse.ru", + "calendarMethod": null + }, + { + "name": "Parser.n.out", + "data": "KiBXYWl0aW5nIGZvciBhIE1JTUUgbWVzc2FnZSBmcm9tIFNURElOLi4uDQo9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCkNvbnRlbnQtdHlwZTogbXVsdGlwYXJ0L21peGVkDQpCb2R5LWZpbGU6IE5PTkUNClN1YmplY3Q6IEEgY29tcGxleCBuZXN0ZWQgbXVsdGlwYXJ0IGV4YW1wbGUNCk51bS1wYXJ0czogMw0KLS0NCiAgICBDb250ZW50LXR5cGU6IHRleHQvcGxhaW4NCiAgICBCb2R5LWZpbGU6IC4vdGVzdG91dC9tc2ctMzUzOC0xLmRvYw0KICAgIC0tDQogICAgQ29udGVudC10eXBlOiB0ZXh0L3BsYWluDQogICAgQm9keS1maWxlOiAuL3Rlc3RvdXQvbXNnLTM1MzgtMi5kb2MNCiAgICAtLQ0KICAgIENvbnRlbnQtdHlwZTogbXVsdGlwYXJ0L3BhcmFsbGVsDQogICAgQm9keS1maWxlOiBOT05FDQogICAgU3ViamVjdDogUGFydCAzIG9mIHRoZSBvdXRlciBtZXNzYWdlIGlzIG11bHRpcGFydCENCiAgICBOdW0tcGFydHM6IDINCiAgICAtLQ0KICAgICAgICBDb250ZW50LXR5cGU6IGltYWdlL2dpZg0KICAgICAgICBCb2R5LWZpbGU6IC4vdGVzdG91dC8zZC1jb21wcmVzcy5naWYNCiAgICAgICAgU3ViamVjdDogUGFydCAxIG9mIHRoZSBpbm5lciBtZXNzYWdlIGlzIGEgR0lGLCAiM2QtY29tcHJlc3MuZ2lmIg0KICAgICAgICAtLQ0KICAgICAgICBDb250ZW50LXR5cGU6IGltYWdlL2dpZg0KICAgICAgICBCb2R5LWZpbGU6IC4vdGVzdG91dC8zZC1leWUuZ2lmDQogICAgICAgIFN1YmplY3Q6IFBhcnQgMiBvZiB0aGUgaW5uZXIgbWVzc2FnZSBpcyBhbm90aGVyIEdJRiwgIjNkLWV5ZS5naWYiDQogICAgICAgIC0tDQo9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCg0K", + "mimeType": "text/plain", + "charset": "us-ascii", + "contentId": "Pine.LNX.3.91.960606155039.2195D@fripp.aic.synapse.ru", + "calendarMethod": null + } + ], + "mailHeaders": "Date: Thu, 6 Jun 1996 15:50:39 +0400 (MOW DST)\nFrom: Starovoitov Igor \nTo: eryq@rhine.gsfc.nasa.gov\nSubject: Need help\nMIME-Version: 1.0\nContent-Type: MULTIPART/MIXED; BOUNDARY=\"-490585488-806670346-834061839=:2195\"", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-igor2.msg b/packages/node-mimimi/test/mimetools-testmsgs/multi-igor2.msg new file mode 100644 index 00000000000..195cf489dbd --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-igor2.msg @@ -0,0 +1,198 @@ +Date: Thu, 6 Jun 1996 15:50:39 +0400 (MOW DST) +From: Starovoitov Igor +To: eryq@rhine.gsfc.nasa.gov +Subject: Need help +MIME-Version: 1.0 +Content-Type: MULTIPART/MIXED; BOUNDARY="-490585488-806670346-834061839=:2195" + + This message is in MIME format. The first part should be readable text, + while the remaining parts are likely unreadable without MIME-aware tools. + Send mail to mime@docserver.cac.washington.edu for more info. + +---490585488-806670346-834061839=:2195 +Content-Type: TEXT/PLAIN; charset=US-ASCII + +Dear Sir, + +I have a problem with Your MIME-Parser-1.9 +and multipart-nested messages. +Not all parts are parsed. + +Here my Makefile, Your own multipart-nested.msg +and its out after "make test". +Some my messages not completely parsed too. + +Is this a bug? + +Thank You for help. + + +Igor Starovoytov. +---490585488-806670346-834061839=:2195 +Content-Type: TEXT/PLAIN; charset=US-ASCII; name=Makefile +Content-Transfer-Encoding: BASE64 +Content-ID: +Content-Description: Makefile + +Iy0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t +LS0tLS0tLS0tLS0tLS0tLQ0KIyBNYWtlZmlsZSBmb3IgTUlNRTo6DQojLS0t +LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t +LS0tLS0tLS0tLS0tDQoNCiMgV2hlcmUgdG8gaW5zdGFsbCB0aGUgbGlicmFy +aWVzOg0KU0lURV9QRVJMID0gL3Vzci9saWIvcGVybDUNCg0KIyBXaGF0IFBl +cmw1IGlzIGNhbGxlZCBvbiB5b3VyIHN5c3RlbSAobm8gbmVlZCB0byBnaXZl +IGVudGlyZSBwYXRoKToNClBFUkw1ICAgICA9IHBlcmwNCg0KIyBZb3UgcHJv +YmFibHkgd29uJ3QgbmVlZCB0byBjaGFuZ2UgdGhlc2UuLi4NCk1PRFMgICAg +ICA9IERlY29kZXIucG0gRW50aXR5LnBtIEhlYWQucG0gUGFyc2VyLnBtIEJh +c2U2NC5wbSBRdW90ZWRQcmludC5wbQ0KU0hFTEwgICAgID0gL2Jpbi9zaA0K +DQojLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t +LS0tLS0tLS0tLS0tLS0tLS0tDQojIEZvciBpbnN0YWxsZXJzLi4uDQojLS0t +LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t +LS0tLS0tLS0tLS0tDQoNCmhlbHA6CQ0KCUBlY2hvICJWYWxpZCB0YXJnZXRz +OiB0ZXN0IGNsZWFuIGluc3RhbGwiDQoNCmNsZWFuOg0KCXJtIC1mIHRlc3Rv +dXQvKg0KDQp0ZXN0Og0KIwlAZWNobyAiVEVTVElORyBIZWFkLnBtLi4uIg0K +Iwkke1BFUkw1fSBNSU1FL0hlYWQucG0gICA8IHRlc3Rpbi9maXJzdC5oZHIg +ICAgICAgPiB0ZXN0b3V0L0hlYWQub3V0DQojCUBlY2hvICJURVNUSU5HIERl +Y29kZXIucG0uLi4iDQojCSR7UEVSTDV9IE1JTUUvRGVjb2Rlci5wbSA8IHRl +c3Rpbi9xdW90LXByaW50LmJvZHkgPiB0ZXN0b3V0L0RlY29kZXIub3V0DQoj +CUBlY2hvICJURVNUSU5HIFBhcnNlci5wbSAoc2ltcGxlKS4uLiINCiMJJHtQ +RVJMNX0gTUlNRS9QYXJzZXIucG0gPCB0ZXN0aW4vc2ltcGxlLm1zZyAgICAg +ID4gdGVzdG91dC9QYXJzZXIucy5vdXQNCiMJQGVjaG8gIlRFU1RJTkcgUGFy +c2VyLnBtIChtdWx0aXBhcnQpLi4uIg0KIwkke1BFUkw1fSBNSU1FL1BhcnNl +ci5wbSA8IHRlc3Rpbi9tdWx0aS0yZ2lmcy5tc2cgPiB0ZXN0b3V0L1BhcnNl +ci5tLm91dA0KCUBlY2hvICJURVNUSU5HIFBhcnNlci5wbSAobXVsdGlfbmVz +dGVkLm1zZykuLi4iDQoJJHtQRVJMNX0gTUlNRS9QYXJzZXIucG0gPCB0ZXN0 +aW4vbXVsdGktbmVzdGVkLm1zZyA+IHRlc3RvdXQvUGFyc2VyLm4ub3V0DQoJ +QGVjaG8gIkFsbCB0ZXN0cyBwYXNzZWQuLi4gc2VlIC4vdGVzdG91dC9NT0RV +TEUqLm91dCBmb3Igb3V0cHV0Ig0KDQppbnN0YWxsOg0KCUBpZiBbICEgLWQg +JHtTSVRFX1BFUkx9IF07IHRoZW4gXA0KCSAgICBlY2hvICJQbGVhc2UgZWRp +dCB0aGUgU0lURV9QRVJMIGluIHlvdXIgTWFrZWZpbGUiOyBleGl0IC0xOyBc +DQogICAgICAgIGZpICAgICAgICAgIA0KCUBpZiBbICEgLXcgJHtTSVRFX1BF +Ukx9IF07IHRoZW4gXA0KCSAgICBlY2hvICJObyBwZXJtaXNzaW9uLi4uIHNo +b3VsZCB5b3UgYmUgcm9vdD8iOyBleGl0IC0xOyBcDQogICAgICAgIGZpICAg +ICAgICAgIA0KCUBpZiBbICEgLWQgJHtTSVRFX1BFUkx9L01JTUUgXTsgdGhl +biBcDQoJICAgIG1rZGlyICR7U0lURV9QRVJMfS9NSU1FOyBcDQogICAgICAg +IGZpDQoJaW5zdGFsbCAtbSAwNjQ0IE1JTUUvKi5wbSAke1NJVEVfUEVSTH0v +TUlNRQ0KDQoNCiMtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t +LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NCiMgRm9yIGRldmVsb3BlciBv +bmx5Li4uDQojLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t +LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQoNClBPRDJIVE1MX0ZMQUdTID0g +LS1wb2RwYXRoPS4gLS1mbHVzaCAtLWh0bWxyb290PS4uDQpIVE1MUyAgICAg +ICAgICA9ICR7TU9EUzoucG09Lmh0bWx9DQpWUEFUSCAgICAgICAgICA9IE1J +TUUNCg0KLlNVRkZJWEVTOiAucG0gLnBvZCAuaHRtbA0KDQojIHYuMS44IGdl +bmVyYXRlZCAzMCBBcHIgOTYNCiMgdi4xLjkgaXMgb25seSBiZWNhdXNlIDEu +OCBmYWlsZWQgQ1BBTiBpbmdlc3Rpb24NCmRpc3Q6IGRvY3VtZW50ZWQJDQoJ +VkVSU0lPTj0xLjkgOyBcDQoJbWtkaXN0IC10Z3ogTUlNRS1wYXJzZXItJCRW +RVJTSU9OIDsgXA0KCWNwIE1LRElTVC9NSU1FLXBhcnNlci0kJFZFUlNJT04u +dGd6ICR7SE9NRX0vcHVibGljX2h0bWwvY3Bhbg0KCQ0KZG9jdW1lbnRlZDog +JHtIVE1MU30gJHtNT0RTfQ0KDQoucG0uaHRtbDoNCglwb2QyaHRtbCAke1BP +RDJIVE1MX0ZMQUdTfSBcDQoJCS0tdGl0bGU9TUlNRTo6JCogXA0KCQktLWlu +ZmlsZT0kPCBcDQoJCS0tb3V0ZmlsZT1kb2NzLyQqLmh0bWwNCg0KIy0tLS0t +LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t +LS0tLS0tLS0tLQ0K +---490585488-806670346-834061839=:2195 +Content-Type: TEXT/PLAIN; charset=US-ASCII; name="multi-nested.msg" +Content-Transfer-Encoding: BASE64 +Content-ID: +Content-Description: test message + +TUlNRS1WZXJzaW9uOiAxLjANCkZyb206IExvcmQgSm9obiBXaG9yZmluIDx3 +aG9yZmluQHlveW9keW5lLmNvbT4NClRvOiA8am9obi15YXlhQHlveW9keW5l +LmNvbT4NClN1YmplY3Q6IEEgY29tcGxleCBuZXN0ZWQgbXVsdGlwYXJ0IGV4 +YW1wbGUNCkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L21peGVkOw0KICAgICBi +b3VuZGFyeT11bmlxdWUtYm91bmRhcnktMQ0KDQpUaGUgcHJlYW1ibGUgb2Yg +dGhlIG91dGVyIG11bHRpcGFydCBtZXNzYWdlLg0KTWFpbCByZWFkZXJzIHRo +YXQgdW5kZXJzdGFuZCBtdWx0aXBhcnQgZm9ybWF0DQpzaG91bGQgaWdub3Jl +IHRoaXMgcHJlYW1ibGUuDQpJZiB5b3UgYXJlIHJlYWRpbmcgdGhpcyB0ZXh0 +LCB5b3UgbWlnaHQgd2FudCB0bw0KY29uc2lkZXIgY2hhbmdpbmcgdG8gYSBt +YWlsIHJlYWRlciB0aGF0IHVuZGVyc3RhbmRzDQpob3cgdG8gcHJvcGVybHkg +ZGlzcGxheSBtdWx0aXBhcnQgbWVzc2FnZXMuDQotLXVuaXF1ZS1ib3VuZGFy +eS0xDQoNClBhcnQgMSBvZiB0aGUgb3V0ZXIgbWVzc2FnZS4NCltOb3RlIHRo +YXQgdGhlIHByZWNlZGluZyBibGFuayBsaW5lIG1lYW5zDQpubyBoZWFkZXIg +ZmllbGRzIHdlcmUgZ2l2ZW4gYW5kIHRoaXMgaXMgdGV4dCwNCndpdGggY2hh +cnNldCBVUyBBU0NJSS4gIEl0IGNvdWxkIGhhdmUgYmVlbg0KZG9uZSB3aXRo +IGV4cGxpY2l0IHR5cGluZyBhcyBpbiB0aGUgbmV4dCBwYXJ0Ll0NCg0KLS11 +bmlxdWUtYm91bmRhcnktMQ0KQ29udGVudC10eXBlOiB0ZXh0L3BsYWluOyBj +aGFyc2V0PVVTLUFTQ0lJDQoNClBhcnQgMiBvZiB0aGUgb3V0ZXIgbWVzc2Fn +ZS4NClRoaXMgY291bGQgaGF2ZSBiZWVuIHBhcnQgb2YgdGhlIHByZXZpb3Vz +IHBhcnQsDQpidXQgaWxsdXN0cmF0ZXMgZXhwbGljaXQgdmVyc3VzIGltcGxp +Y2l0DQp0eXBpbmcgb2YgYm9keSBwYXJ0cy4NCg0KLS11bmlxdWUtYm91bmRh +cnktMQ0KU3ViamVjdDogUGFydCAzIG9mIHRoZSBvdXRlciBtZXNzYWdlIGlz +IG11bHRpcGFydCENCkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L3BhcmFsbGVs +Ow0KICAgICBib3VuZGFyeT11bmlxdWUtYm91bmRhcnktMg0KDQpBIG9uZS1s +aW5lIHByZWFtYmxlIGZvciB0aGUgaW5uZXIgbXVsdGlwYXJ0IG1lc3NhZ2Uu +DQotLXVuaXF1ZS1ib3VuZGFyeS0yDQpDb250ZW50LVR5cGU6IGltYWdlL2dp +Zg0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogYmFzZTY0DQpDb250ZW50 +LURpc3Bvc2l0aW9uOiBpbmxpbmU7IGZpbGVuYW1lPSIzZC1jb21wcmVzcy5n +aWYiDQpTdWJqZWN0OiBQYXJ0IDEgb2YgdGhlIGlubmVyIG1lc3NhZ2UgaXMg +YSBHSUYsICIzZC1jb21wcmVzcy5naWYiDQoNClIwbEdPRGRoS0FBb0FPTUFB +QUFBQUFBQWdCNlEveTlQVDI1dWJuQ0FrS0JTTGI2K3Z1Zm41L1hlcy8rbEFQ +LzZ6UUFBQUFBQQ0KQUFBQUFBQUFBQ3dBQUFBQUtBQW9BQUFFL2hESlNhdTll +SkxNT3lZYmNveGthWjVvQ2tvSDZMNXdMTWZpV3FkNGJ0WmhteGJBDQpvRkNZ +NDdFSXFNSmd5V3cyQVRqajdhUmtBcTVZd0RNbDlWR3RLTzBTaXVvaVRWbHNj +c3h0OWM0SGdYeFVJQTBFQVZPVmZES1QNCjhIbDFCM2tEQVlZbGUyMDJYbkdH +Z29NSGhZY2tpV1Z1UjMrT1RnQ0dlWlJzbG90d2dKMmxuWWlnZlpkVGpRVUxy +N0FMQlpOMA0KcVR1cmpIZ0xLQXUwQjVXcW9wbTdKNzJldFFOOHQ4SWp1cnkr +d010dnc4L0h2N1lsZnMwQnhDYkdxTW1LMHlPT1EwR1RDZ3JSDQoyYmh3Skds +WEpRWUc2bU1Lb2VOb1dTYnpDV0lBQ2U1Snd4UW0zQWtEQWJVQVFDaVFoRFpF +QmVCbDZhZmdDc09CckQ0NWVkSXYNClFjZUdXU01ldnBPWWhsNkNreWRCSGhC +WlFtR0tqaWhWc2h5cGpCOUNsQUhaTVR1Z3pPVTdtemhCUGlTWjV1RE5uQTdi +L2FUWg0KMG1oTW5mbDBwREJGYTZiVUVsU1BXYjBxdFl1SHJ4bHdjUjE3WXNX +TXMyalRxbDNMRmtRRUFEcz0NCi0tdW5pcXVlLWJvdW5kYXJ5LTINCkNvbnRl +bnQtVHlwZTogaW1hZ2UvZ2lmDQpDb250ZW50LVRyYW5zZmVyLUVuY29kaW5n +OiBiYXNlNjQNCkNvbnRlbnQtRGlzcG9zaXRpb246IGlubGluZTsgZmlsZW5h +bWU9IjNkLWV5ZS5naWYiDQpTdWJqZWN0OiBQYXJ0IDIgb2YgdGhlIGlubmVy +IG1lc3NhZ2UgaXMgYW5vdGhlciBHSUYsICIzZC1leWUuZ2lmIg0KDQpSMGxH +T0RkaEtBQW9BUE1BQUFBQUFBQUF6TjN1Lzc2K3ZvaUlpRzV1YnN6ZDd2Ly8v +K2ZuNXdBQUFBQUFBQUFBQUFBQUFBQUENCkFBQUFBQUFBQUN3QUFBQUFLQUFv +QUFBRS9oREpTYXU5ZUpiTU95NGJNb3hrYVo1b0Nrb0Q2TDV3TE1maVduczQx +b1p0N2xNNw0KVnVqbkM5NklSVnNQV1FFNG54UGprdm1zUW11OG9jL0tCVVNW +V2s3WGVwR0dMZU5yeG94Sk8xTWpJTGp0aGcva1dYUTZ3Ty83DQorM2RDZVJS +amZBS0hpSW1KQVYrRENGMEJpVzVWQW8xQ0VsYVJoNU5qbGtlWW1weVRncGNU +QUtHaWFhU2Zwd0twVlFheFZhdEwNCnJVOEdhUWRPQkFRQUI3K3lYbGlYVHJn +QXhzVzR2RmFidjhCT3RCc0J0N2NHdndDSVQ5bk95TkVJeHVDNHpycUt6YzlY +Yk9ESg0KdnM3WTVld0gzZDdGeGUzakI0cmo4dDZQdU5hNnIyYmhLUVhOMTdG +WUNCTXFUR2lCelNOaHg1ZzBuRU1obHNTSmppUll2RGp3DQpFMGNkR3hRL2dz +d29zb0tVa211VTJGbkpjc1NLR1RCanlweEpzeWFJQ0FBNw0KLS11bmlxdWUt +Ym91bmRhcnktMi0tDQoNClRoZSBlcGlsb2d1ZSBmb3IgdGhlIGlubmVyIG11 +bHRpcGFydCBtZXNzYWdlLg0KDQotLXVuaXF1ZS1ib3VuZGFyeS0xDQpDb250 +ZW50LXR5cGU6IHRleHQvcmljaHRleHQNCg0KVGhpcyBpcyA8Ym9sZD5wYXJ0 +IDQgb2YgdGhlIG91dGVyIG1lc3NhZ2U8L2JvbGQ+DQo8c21hbGxlcj5hcyBk +ZWZpbmVkIGluIFJGQzEzNDE8L3NtYWxsZXI+PG5sPg0KPG5sPg0KSXNuJ3Qg +aXQgPGJpZ2dlcj48YmlnZ2VyPmNvb2w/PC9iaWdnZXI+PC9iaWdnZXI+DQoN +Ci0tdW5pcXVlLWJvdW5kYXJ5LTENCkNvbnRlbnQtVHlwZTogbWVzc2FnZS9y +ZmM4MjINCg0KRnJvbTogKG1haWxib3ggaW4gVVMtQVNDSUkpDQpUbzogKGFk +ZHJlc3MgaW4gVVMtQVNDSUkpDQpTdWJqZWN0OiBQYXJ0IDUgb2YgdGhlIG91 +dGVyIG1lc3NhZ2UgaXMgaXRzZWxmIGFuIFJGQzgyMiBtZXNzYWdlIQ0KQ29u +dGVudC1UeXBlOiBUZXh0L3BsYWluOyBjaGFyc2V0PUlTTy04ODU5LTENCkNv +bnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IFF1b3RlZC1wcmludGFibGUNCg0K +UGFydCA1IG9mIHRoZSBvdXRlciBtZXNzYWdlIGlzIGl0c2VsZiBhbiBSRkM4 +MjIgbWVzc2FnZSENCg0KLS11bmlxdWUtYm91bmRhcnktMS0tDQoNClRoZSBl +cGlsb2d1ZSBmb3IgdGhlIG91dGVyIG1lc3NhZ2UuDQo= +---490585488-806670346-834061839=:2195 +Content-Type: TEXT/PLAIN; charset=US-ASCII; name="Parser.n.out" +Content-Transfer-Encoding: BASE64 +Content-ID: +Content-Description: out from parser + +KiBXYWl0aW5nIGZvciBhIE1JTUUgbWVzc2FnZSBmcm9tIFNURElOLi4uDQo9 +PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09 +PT09PT09PT09PT09PT0NCkNvbnRlbnQtdHlwZTogbXVsdGlwYXJ0L21peGVk +DQpCb2R5LWZpbGU6IE5PTkUNClN1YmplY3Q6IEEgY29tcGxleCBuZXN0ZWQg +bXVsdGlwYXJ0IGV4YW1wbGUNCk51bS1wYXJ0czogMw0KLS0NCiAgICBDb250 +ZW50LXR5cGU6IHRleHQvcGxhaW4NCiAgICBCb2R5LWZpbGU6IC4vdGVzdG91 +dC9tc2ctMzUzOC0xLmRvYw0KICAgIC0tDQogICAgQ29udGVudC10eXBlOiB0 +ZXh0L3BsYWluDQogICAgQm9keS1maWxlOiAuL3Rlc3RvdXQvbXNnLTM1Mzgt +Mi5kb2MNCiAgICAtLQ0KICAgIENvbnRlbnQtdHlwZTogbXVsdGlwYXJ0L3Bh +cmFsbGVsDQogICAgQm9keS1maWxlOiBOT05FDQogICAgU3ViamVjdDogUGFy +dCAzIG9mIHRoZSBvdXRlciBtZXNzYWdlIGlzIG11bHRpcGFydCENCiAgICBO +dW0tcGFydHM6IDINCiAgICAtLQ0KICAgICAgICBDb250ZW50LXR5cGU6IGlt +YWdlL2dpZg0KICAgICAgICBCb2R5LWZpbGU6IC4vdGVzdG91dC8zZC1jb21w +cmVzcy5naWYNCiAgICAgICAgU3ViamVjdDogUGFydCAxIG9mIHRoZSBpbm5l +ciBtZXNzYWdlIGlzIGEgR0lGLCAiM2QtY29tcHJlc3MuZ2lmIg0KICAgICAg +ICAtLQ0KICAgICAgICBDb250ZW50LXR5cGU6IGltYWdlL2dpZg0KICAgICAg +ICBCb2R5LWZpbGU6IC4vdGVzdG91dC8zZC1leWUuZ2lmDQogICAgICAgIFN1 +YmplY3Q6IFBhcnQgMiBvZiB0aGUgaW5uZXIgbWVzc2FnZSBpcyBhbm90aGVy +IEdJRiwgIjNkLWV5ZS5naWYiDQogICAgICAgIC0tDQo9PT09PT09PT09PT09 +PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09 +PT0NCg0K +---490585488-806670346-834061839=:2195-- diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-nested-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/multi-nested-expected.json new file mode 100644 index 00000000000..ee4955fe791 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-nested-expected.json @@ -0,0 +1,69 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "unique-boundary-2", + "alternativeBoundary": null, + "sender": { + "name": "Lord John Whorfin", + "mailAddress": "whorfin@yoyodyne.com", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "john-yaya@yoyodyne.com", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "A complex nested multipart example", + "plainBodyText": "Part 1 of the outer message.\r\n[Note that the preceding blank line means\r\nno header fields were given and this is text,\r\nwith charset US ASCII. It could have been\r\ndone with explicit typing as in the next part.]\r\nPart 2 of the outer message.\r\nThis could have been part of the previous part,\r\nbut illustrates explicit versus implicit\r\ntyping of body parts.\r\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "3d-vise.gif", + "data": "R0lGODdhKAAoAOMAAAAAAAAAgB6Q/y9PT25ubnCAkKBSLb6+vufn5/Xes/+lAP/6zQAAAAAAAAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJLMOyYbcoxkaZ5oCkoH6L5wLMfiWqd4btZhmxbAoFCY47EIqMJgyWw2ATjj7aRkAq5YwDMl9VGtKO0SiuoiTVlscsxt9c4HgXxUIA0EAVOVfDKT8Hl1B3kDAYYle202XnGGgoMHhYckiWVuR3+OTgCGeZRslotwgJ2lnYigfZdTjQULr7ALBZN0qTurjHgLKAu0B5Wqopm7J72etQN8t8Ijury+wMtvw8/Hv7Ylfs0BxCbGqMmK0yOOQ0GTCgrR2bhwJGlXJQYG6mMKoeNoWSbzCWIACe5JwxQm3AkDAbUAQCiQhDZEBeBl6afgCsOBrD45edIvQceGWSMevpOYhl6CkydBHhBZQmGKjihVshypjB9ClAHZMTugzOU7mzhBPiSZ5uDNnA7b/aTZ0mhMnfl0pDBFa6bUElSPWb0qtYuHrxlwcR17YsWMs2jTql3LFkQEADs=", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + }, + { + "name": "3d-eye.gif", + "data": "R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7+3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatLrU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJvs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjwE0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + }, + { + "name": "unknown.txt", + "data": "VGhpcyBpcyA8Ym9sZD5wYXJ0IDQgb2YgdGhlIG91dGVyIG1lc3NhZ2U8L2JvbGQ+DQo8c21hbGxlcj5hcyBkZWZpbmVkIGluIFJGQzEzNDE8L3NtYWxsZXI+PG5sPg0KPG5sPg0KSXNuJ3QgaXQgPGJpZ2dlcj48YmlnZ2VyPmNvb2w/PC9iaWdnZXI+PC9iaWdnZXI+DQo=", + "mimeType": "text/richtext", + "charset": null, + "contentId": "", + "calendarMethod": null + }, + { + "name": "_evil_filename", + "data": "RnJvbTogKG1haWxib3ggaW4gVVMtQVNDSUkpDQpUbzogKGFkZHJlc3MgaW4gVVMtQVNDSUkpDQpTdWJqZWN0OiBQYXJ0IDUgb2YgdGhlIG91dGVyIG1lc3NhZ2UgaXMgaXRzZWxmIGFuIFJGQzgyMiBtZXNzYWdlIQ0KQ29udGVudC1UeXBlOiBUZXh0L3BsYWluOyBjaGFyc2V0PUlTTy04ODU5LTENCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IFF1b3RlZC1wcmludGFibGUNCg0KUGFydCA1IG9mIHRoZSBvdXRlciBtZXNzYWdlIGlzIGl0c2VsZiBhbiBSRkM4MjIgbWVzc2FnZSENCg==", + "mimeType": "message/rfc822", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "MIME-Version: 1.0\nFrom: Lord John Whorfin \nTo: \nSubject: A complex nested multipart example\nContent-Type: multipart/mixed;\r\n boundary=unique-boundary-1", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-nested.msg b/packages/node-mimimi/test/mimetools-testmsgs/multi-nested.msg new file mode 100644 index 00000000000..baecbda08e2 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-nested.msg @@ -0,0 +1,89 @@ +MIME-Version: 1.0 +From: Lord John Whorfin +To: +Subject: A complex nested multipart example +Content-Type: multipart/mixed; + boundary=unique-boundary-1 + +The preamble of the outer multipart message. +Mail readers that understand multipart format +should ignore this preamble. +If you are reading this text, you might want to +consider changing to a mail reader that understands +how to properly display multipart messages. +--unique-boundary-1 + +Part 1 of the outer message. +[Note that the preceding blank line means +no header fields were given and this is text, +with charset US ASCII. It could have been +done with explicit typing as in the next part.] + +--unique-boundary-1 +Content-type: text/plain; charset=US-ASCII + +Part 2 of the outer message. +This could have been part of the previous part, +but illustrates explicit versus implicit +typing of body parts. + +--unique-boundary-1 +Subject: Part 3 of the outer message is multipart! +Content-Type: multipart/parallel; + boundary=unique-boundary-2 + +A one-line preamble for the inner multipart message. +--unique-boundary-2 +Content-Type: image/gif +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="3d-vise.gif" +Subject: Part 1 of the inner message is a GIF, "3d-vise.gif" + +R0lGODdhKAAoAOMAAAAAAAAAgB6Q/y9PT25ubnCAkKBSLb6+vufn5/Xes/+lAP/6zQAAAAAA +AAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJLMOyYbcoxkaZ5oCkoH6L5wLMfiWqd4btZhmxbA +oFCY47EIqMJgyWw2ATjj7aRkAq5YwDMl9VGtKO0SiuoiTVlscsxt9c4HgXxUIA0EAVOVfDKT +8Hl1B3kDAYYle202XnGGgoMHhYckiWVuR3+OTgCGeZRslotwgJ2lnYigfZdTjQULr7ALBZN0 +qTurjHgLKAu0B5Wqopm7J72etQN8t8Ijury+wMtvw8/Hv7Ylfs0BxCbGqMmK0yOOQ0GTCgrR +2bhwJGlXJQYG6mMKoeNoWSbzCWIACe5JwxQm3AkDAbUAQCiQhDZEBeBl6afgCsOBrD45edIv +QceGWSMevpOYhl6CkydBHhBZQmGKjihVshypjB9ClAHZMTugzOU7mzhBPiSZ5uDNnA7b/aTZ +0mhMnfl0pDBFa6bUElSPWb0qtYuHrxlwcR17YsWMs2jTql3LFkQEADs= +--unique-boundary-2 +Content-Type: image/gif +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="3d-eye.gif" +Subject: Part 2 of the inner message is another GIF, "3d-eye.gif" + +R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAA +AAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7 +VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7 ++3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatL +rU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJ +vs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjw +E0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7 +--unique-boundary-2-- + +The epilogue for the inner multipart message. + +--unique-boundary-1 +Content-type: text/richtext + +This is part 4 of the outer message +as defined in RFC1341 + +Isn't it cool? + +--unique-boundary-1 +Content-Type: message/rfc822; name="/evil/filename"; + +From: (mailbox in US-ASCII) +To: (address in US-ASCII) +Subject: Part 5 of the outer message is itself an RFC822 message! +Content-Type: Text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: Quoted-printable + +Part 5 of the outer message is itself an RFC822 message! + +--unique-boundary-1-- + +The epilogue for the outer message. + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-nested2-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/multi-nested2-expected.json new file mode 100644 index 00000000000..ee4955fe791 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-nested2-expected.json @@ -0,0 +1,69 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "unique-boundary-2", + "alternativeBoundary": null, + "sender": { + "name": "Lord John Whorfin", + "mailAddress": "whorfin@yoyodyne.com", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "john-yaya@yoyodyne.com", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "A complex nested multipart example", + "plainBodyText": "Part 1 of the outer message.\r\n[Note that the preceding blank line means\r\nno header fields were given and this is text,\r\nwith charset US ASCII. It could have been\r\ndone with explicit typing as in the next part.]\r\nPart 2 of the outer message.\r\nThis could have been part of the previous part,\r\nbut illustrates explicit versus implicit\r\ntyping of body parts.\r\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "3d-vise.gif", + "data": "R0lGODdhKAAoAOMAAAAAAAAAgB6Q/y9PT25ubnCAkKBSLb6+vufn5/Xes/+lAP/6zQAAAAAAAAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJLMOyYbcoxkaZ5oCkoH6L5wLMfiWqd4btZhmxbAoFCY47EIqMJgyWw2ATjj7aRkAq5YwDMl9VGtKO0SiuoiTVlscsxt9c4HgXxUIA0EAVOVfDKT8Hl1B3kDAYYle202XnGGgoMHhYckiWVuR3+OTgCGeZRslotwgJ2lnYigfZdTjQULr7ALBZN0qTurjHgLKAu0B5Wqopm7J72etQN8t8Ijury+wMtvw8/Hv7Ylfs0BxCbGqMmK0yOOQ0GTCgrR2bhwJGlXJQYG6mMKoeNoWSbzCWIACe5JwxQm3AkDAbUAQCiQhDZEBeBl6afgCsOBrD45edIvQceGWSMevpOYhl6CkydBHhBZQmGKjihVshypjB9ClAHZMTugzOU7mzhBPiSZ5uDNnA7b/aTZ0mhMnfl0pDBFa6bUElSPWb0qtYuHrxlwcR17YsWMs2jTql3LFkQEADs=", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + }, + { + "name": "3d-eye.gif", + "data": "R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7+3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatLrU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJvs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjwE0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + }, + { + "name": "unknown.txt", + "data": "VGhpcyBpcyA8Ym9sZD5wYXJ0IDQgb2YgdGhlIG91dGVyIG1lc3NhZ2U8L2JvbGQ+DQo8c21hbGxlcj5hcyBkZWZpbmVkIGluIFJGQzEzNDE8L3NtYWxsZXI+PG5sPg0KPG5sPg0KSXNuJ3QgaXQgPGJpZ2dlcj48YmlnZ2VyPmNvb2w/PC9iaWdnZXI+PC9iaWdnZXI+DQo=", + "mimeType": "text/richtext", + "charset": null, + "contentId": "", + "calendarMethod": null + }, + { + "name": "_evil_filename", + "data": "RnJvbTogKG1haWxib3ggaW4gVVMtQVNDSUkpDQpUbzogKGFkZHJlc3MgaW4gVVMtQVNDSUkpDQpTdWJqZWN0OiBQYXJ0IDUgb2YgdGhlIG91dGVyIG1lc3NhZ2UgaXMgaXRzZWxmIGFuIFJGQzgyMiBtZXNzYWdlIQ0KQ29udGVudC1UeXBlOiBUZXh0L3BsYWluOyBjaGFyc2V0PUlTTy04ODU5LTENCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IFF1b3RlZC1wcmludGFibGUNCg0KUGFydCA1IG9mIHRoZSBvdXRlciBtZXNzYWdlIGlzIGl0c2VsZiBhbiBSRkM4MjIgbWVzc2FnZSENCg==", + "mimeType": "message/rfc822", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "MIME-Version: 1.0\nFrom: Lord John Whorfin \nTo: \nSubject: A complex nested multipart example\nContent-Type: multipart/mixed;\r\n boundary=unique-boundary-1", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-nested2.msg b/packages/node-mimimi/test/mimetools-testmsgs/multi-nested2.msg new file mode 100644 index 00000000000..baecbda08e2 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-nested2.msg @@ -0,0 +1,89 @@ +MIME-Version: 1.0 +From: Lord John Whorfin +To: +Subject: A complex nested multipart example +Content-Type: multipart/mixed; + boundary=unique-boundary-1 + +The preamble of the outer multipart message. +Mail readers that understand multipart format +should ignore this preamble. +If you are reading this text, you might want to +consider changing to a mail reader that understands +how to properly display multipart messages. +--unique-boundary-1 + +Part 1 of the outer message. +[Note that the preceding blank line means +no header fields were given and this is text, +with charset US ASCII. It could have been +done with explicit typing as in the next part.] + +--unique-boundary-1 +Content-type: text/plain; charset=US-ASCII + +Part 2 of the outer message. +This could have been part of the previous part, +but illustrates explicit versus implicit +typing of body parts. + +--unique-boundary-1 +Subject: Part 3 of the outer message is multipart! +Content-Type: multipart/parallel; + boundary=unique-boundary-2 + +A one-line preamble for the inner multipart message. +--unique-boundary-2 +Content-Type: image/gif +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="3d-vise.gif" +Subject: Part 1 of the inner message is a GIF, "3d-vise.gif" + +R0lGODdhKAAoAOMAAAAAAAAAgB6Q/y9PT25ubnCAkKBSLb6+vufn5/Xes/+lAP/6zQAAAAAA +AAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJLMOyYbcoxkaZ5oCkoH6L5wLMfiWqd4btZhmxbA +oFCY47EIqMJgyWw2ATjj7aRkAq5YwDMl9VGtKO0SiuoiTVlscsxt9c4HgXxUIA0EAVOVfDKT +8Hl1B3kDAYYle202XnGGgoMHhYckiWVuR3+OTgCGeZRslotwgJ2lnYigfZdTjQULr7ALBZN0 +qTurjHgLKAu0B5Wqopm7J72etQN8t8Ijury+wMtvw8/Hv7Ylfs0BxCbGqMmK0yOOQ0GTCgrR +2bhwJGlXJQYG6mMKoeNoWSbzCWIACe5JwxQm3AkDAbUAQCiQhDZEBeBl6afgCsOBrD45edIv +QceGWSMevpOYhl6CkydBHhBZQmGKjihVshypjB9ClAHZMTugzOU7mzhBPiSZ5uDNnA7b/aTZ +0mhMnfl0pDBFa6bUElSPWb0qtYuHrxlwcR17YsWMs2jTql3LFkQEADs= +--unique-boundary-2 +Content-Type: image/gif +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="3d-eye.gif" +Subject: Part 2 of the inner message is another GIF, "3d-eye.gif" + +R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAA +AAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7 +VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7 ++3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatL +rU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJ +vs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjw +E0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7 +--unique-boundary-2-- + +The epilogue for the inner multipart message. + +--unique-boundary-1 +Content-type: text/richtext + +This is part 4 of the outer message +as defined in RFC1341 + +Isn't it cool? + +--unique-boundary-1 +Content-Type: message/rfc822; name="/evil/filename"; + +From: (mailbox in US-ASCII) +To: (address in US-ASCII) +Subject: Part 5 of the outer message is itself an RFC822 message! +Content-Type: Text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: Quoted-printable + +Part 5 of the outer message is itself an RFC822 message! + +--unique-boundary-1-- + +The epilogue for the outer message. + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-nested3-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/multi-nested3-expected.json new file mode 100644 index 00000000000..8931ad4cab9 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-nested3-expected.json @@ -0,0 +1,69 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "unique-boundary-2", + "alternativeBoundary": null, + "sender": { + "name": "Lord John Whorfin", + "mailAddress": "whorfin@yoyodyne.com", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "john-yaya@yoyodyne.com", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "A complex nested multipart example", + "plainBodyText": "Part 1 of the outer message.\r\n[Note that the preceding blank line means\r\nno header fields were given and this is text,\r\nwith charset US ASCII. It could have been\r\ndone with explicit typing as in the next part.]\r\nPart 2 of the outer message.\r\nThis could have been part of the previous part,\r\nbut illustrates explicit versus implicit\r\ntyping of body parts.\r\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "3d-vise.gif", + "data": "R0lGODdhKAAoAOMAAAAAAAAAgB6Q/y9PT25ubnCAkKBSLb6+vufn5/Xes/+lAP/6zQAAAAAAAAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJLMOyYbcoxkaZ5oCkoH6L5wLMfiWqd4btZhmxbAoFCY47EIqMJgyWw2ATjj7aRkAq5YwDMl9VGtKO0SiuoiTVlscsxt9c4HgXxUIA0EAVOVfDKT8Hl1B3kDAYYle202XnGGgoMHhYckiWVuR3+OTgCGeZRslotwgJ2lnYigfZdTjQULr7ALBZN0qTurjHgLKAu0B5Wqopm7J72etQN8t8Ijury+wMtvw8/Hv7Ylfs0BxCbGqMmK0yOOQ0GTCgrR2bhwJGlXJQYG6mMKoeNoWSbzCWIACe5JwxQm3AkDAbUAQCiQhDZEBeBl6afgCsOBrD45edIvQceGWSMevpOYhl6CkydBHhBZQmGKjihVshypjB9ClAHZMTugzOU7mzhBPiSZ5uDNnA7b/aTZ0mhMnfl0pDBFa6bUElSPWb0qtYuHrxlwcR17YsWMs2jTql3LFkQEADs=", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + }, + { + "name": "3d-eye.gif", + "data": "R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7+3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatLrU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJvs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjwE0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + }, + { + "name": "unknown.txt", + "data": "VGhpcyBpcyA8Ym9sZD5wYXJ0IDQgb2YgdGhlIG91dGVyIG1lc3NhZ2U8L2JvbGQ+DQo8c21hbGxlcj5hcyBkZWZpbmVkIGluIFJGQzEzNDE8L3NtYWxsZXI+PG5sPg0KPG5sPg0KSXNuJ3QgaXQgPGJpZ2dlcj48YmlnZ2VyPmNvb2w/PC9iaWdnZXI+PC9iaWdnZXI+DQo=", + "mimeType": "text/richtext", + "charset": null, + "contentId": "", + "calendarMethod": null + }, + { + "name": "nice.name", + "data": "RnJvbTogKG1haWxib3ggaW4gVVMtQVNDSUkpDQpUbzogKGFkZHJlc3MgaW4gVVMtQVNDSUkpDQpTdWJqZWN0OiBQYXJ0IDUgb2YgdGhlIG91dGVyIG1lc3NhZ2UgaXMgaXRzZWxmIGFuIFJGQzgyMiBtZXNzYWdlIQ0KQ29udGVudC1UeXBlOiBUZXh0L3BsYWluOyBjaGFyc2V0PUlTTy04ODU5LTENCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IFF1b3RlZC1wcmludGFibGUNCg0KUGFydCA1IG9mIHRoZSBvdXRlciBtZXNzYWdlIGlzIGl0c2VsZiBhbiBSRkM4MjIgbWVzc2FnZSENCg==", + "mimeType": "message/rfc822", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "MIME-Version: 1.0\nFrom: Lord John Whorfin \nTo: \nSubject: A complex nested multipart example\nContent-Type: multipart/mixed;\r\n boundary=unique-boundary-1", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-nested3.msg b/packages/node-mimimi/test/mimetools-testmsgs/multi-nested3.msg new file mode 100644 index 00000000000..a3f2fd7d389 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-nested3.msg @@ -0,0 +1,89 @@ +MIME-Version: 1.0 +From: Lord John Whorfin +To: +Subject: A complex nested multipart example +Content-Type: multipart/mixed; + boundary=unique-boundary-1 + +The preamble of the outer multipart message. +Mail readers that understand multipart format +should ignore this preamble. +If you are reading this text, you might want to +consider changing to a mail reader that understands +how to properly display multipart messages. +--unique-boundary-1 + +Part 1 of the outer message. +[Note that the preceding blank line means +no header fields were given and this is text, +with charset US ASCII. It could have been +done with explicit typing as in the next part.] + +--unique-boundary-1 +Content-type: text/plain; charset=US-ASCII + +Part 2 of the outer message. +This could have been part of the previous part, +but illustrates explicit versus implicit +typing of body parts. + +--unique-boundary-1 +Subject: Part 3 of the outer message is multipart! +Content-Type: multipart/parallel; + boundary=unique-boundary-2 + +A one-line preamble for the inner multipart message. +--unique-boundary-2 +Content-Type: image/gif +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="3d-vise.gif" +Subject: Part 1 of the inner message is a GIF, "3d-vise.gif" + +R0lGODdhKAAoAOMAAAAAAAAAgB6Q/y9PT25ubnCAkKBSLb6+vufn5/Xes/+lAP/6zQAAAAAA +AAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJLMOyYbcoxkaZ5oCkoH6L5wLMfiWqd4btZhmxbA +oFCY47EIqMJgyWw2ATjj7aRkAq5YwDMl9VGtKO0SiuoiTVlscsxt9c4HgXxUIA0EAVOVfDKT +8Hl1B3kDAYYle202XnGGgoMHhYckiWVuR3+OTgCGeZRslotwgJ2lnYigfZdTjQULr7ALBZN0 +qTurjHgLKAu0B5Wqopm7J72etQN8t8Ijury+wMtvw8/Hv7Ylfs0BxCbGqMmK0yOOQ0GTCgrR +2bhwJGlXJQYG6mMKoeNoWSbzCWIACe5JwxQm3AkDAbUAQCiQhDZEBeBl6afgCsOBrD45edIv +QceGWSMevpOYhl6CkydBHhBZQmGKjihVshypjB9ClAHZMTugzOU7mzhBPiSZ5uDNnA7b/aTZ +0mhMnfl0pDBFa6bUElSPWb0qtYuHrxlwcR17YsWMs2jTql3LFkQEADs= +--unique-boundary-2 +Content-Type: image/gif +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="3d-eye.gif" +Subject: Part 2 of the inner message is another GIF, "3d-eye.gif" + +R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAA +AAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7 +VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7 ++3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatL +rU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJ +vs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjw +E0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7 +--unique-boundary-2-- + +The epilogue for the inner multipart message. + +--unique-boundary-1 +Content-type: text/richtext + +This is part 4 of the outer message +as defined in RFC1341 + +Isn't it cool? + +--unique-boundary-1 +Content-Type: message/rfc822; name="nice.name"; + +From: (mailbox in US-ASCII) +To: (address in US-ASCII) +Subject: Part 5 of the outer message is itself an RFC822 message! +Content-Type: Text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: Quoted-printable + +Part 5 of the outer message is itself an RFC822 message! + +--unique-boundary-1-- + +The epilogue for the outer message. + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-simple-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/multi-simple-expected.json new file mode 100644 index 00000000000..67830392659 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-simple-expected.json @@ -0,0 +1,36 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "simple boundary", + "alternativeBoundary": null, + "sender": { + "name": "Nathaniel Borenstein", + "mailAddress": "nsb@bellcore.com", + "valid": true + }, + "toRecipients": [ + { + "name": "Ned Freed", + "mailAddress": "ned@innosoft.com", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "Sample message", + "plainBodyText": "This is implicitly typed plain ASCII text.\r\nIt does NOT end with a linebreak.This is explicitly typed plain ASCII text.\r\nIt DOES end with a linebreak.\r\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [], + "mailHeaders": "From: Nathaniel Borenstein \nTo: Ned Freed \nSubject: Sample message\nMIME-Version: 1.0\nContent-type: multipart/mixed; boundary=\"simple\r\n boundary\"", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-simple.msg b/packages/node-mimimi/test/mimetools-testmsgs/multi-simple.msg new file mode 100644 index 00000000000..5f8d0bde2cc --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-simple.msg @@ -0,0 +1,23 @@ +From: Nathaniel Borenstein +To: Ned Freed +Subject: Sample message +MIME-Version: 1.0 +Content-type: multipart/mixed; boundary="simple + boundary" + +This is the preamble. It is to be ignored, though it +is a handy place for mail composers to include an +explanatory note to non-MIME conformant readers. +--simple boundary + +This is implicitly typed plain ASCII text. +It does NOT end with a linebreak. +--simple boundary +Content-type: text/plain; charset=us-ascii + +This is explicitly typed plain ASCII text. +It DOES end with a linebreak. + +--simple boundary-- +This is the epilogue. It is also to be ignored. + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-weirdspace-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/multi-weirdspace-expected.json new file mode 100644 index 00000000000..410e8ef9e9e --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-weirdspace-expected.json @@ -0,0 +1,53 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": "------------299A70B339B65A93542D2AE", + "alternativeBoundary": null, + "sender": { + "name": "Eryq", + "mailAddress": "eryq@rhine.gsfc.nasa.gov", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "john-bigboote@eryq.pr.mcs.net", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 829203030000, + "subject": "Two images for you...", + "plainBodyText": "When unpacked, this message should produce two GIF files:\r\n\r\n\t* The 1st should be called \"3d-compress.gif\"\r\n\t* The 2nd should be called \"3d-eye.gif\"\r\n\r\nThere is an empty preamble, and linear space after the bounds.\r\n\r\n-- \r\n ____ __\r\n / __/__________/_/ Eryq (eryq@rhine.gsfc.nasa.gov)\r\n / __/ _/ / / , / Hughes STX Corporation, NASA/Goddard\r\n/___/_/ \\ /\\ /___ \r\n /_/ /_____/ http://selsvr.stx.com/~eryq/\r\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "3d-compress.gif", + "data": "R0lGODdhKAAoAOMAAAAAAAAAgB6Q/y9PT25ubnCAkKBSLb6+vufn5/Xes/+lAP/6zQAAAAAAAAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJLMOyYbcoxkaZ5oCkoH6L5wLMfiWqd4btZhmxbAoFCY47EIqMJgyWw2ATjj7aRkAq5YwDMl9VGtKO0SiuoiTVlscsxt9c4HgXxUIA0EAVOVfDKT8Hl1B3kDAYYle202XnGGgoMHhYckiWVuR3+OTgCGeZRslotwgJ2lnYigfZdTjQULr7ALBZN0qTurjHgLKAu0B5Wqopm7J72etQN8t8Ijury+wMtvw8/Hv7Ylfs0BxCbGqMmK0yOOQ0GTCgrR2bhwJGlXJQYG6mMKoeNoWSbzCWIACe5JwxQm3AkDAbUAQCiQhDZEBeBl6afgCsOBrD45edIvQceGWSMevpOYhl6CkydBHhBZQmGKjihVshypjB9ClAHZMTugzOU7mzhBPiSZ5uDNnA7b/aTZ0mhMnfl0pDBFa6bUElSPWb0qtYuHrxlwcR17YsWMs2jTql3LFkQEADs=", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + }, + { + "name": "3d-eye.gif", + "data": "R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7+3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatLrU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJvs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjwE0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "Return-Path: eryq@rhine.gsfc.nasa.gov\nSender: john-bigboote\nDate: Thu, 11 Apr 1996 01:10:30 -0500\nFrom: Eryq \nOrganization: Yoyodyne Propulsion Systems\nX-Mailer: Mozilla 2.0 (X11; I; Linux 1.1.18 i486)\nMIME-Version: 1.0\nTo: john-bigboote@eryq.pr.mcs.net\nSubject: Two images for you...\nContent-Type: multipart/mixed; boundary=\"------------299A70B339B65A93542D2AE\"", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/multi-weirdspace.msg b/packages/node-mimimi/test/mimetools-testmsgs/multi-weirdspace.msg new file mode 100644 index 00000000000..dd6b31be1b2 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/multi-weirdspace.msg @@ -0,0 +1,56 @@ +Return-Path: eryq@rhine.gsfc.nasa.gov +Sender: john-bigboote +Date: Thu, 11 Apr 1996 01:10:30 -0500 +From: Eryq +Organization: Yoyodyne Propulsion Systems +X-Mailer: Mozilla 2.0 (X11; I; Linux 1.1.18 i486) +MIME-Version: 1.0 +To: john-bigboote@eryq.pr.mcs.net +Subject: Two images for you... +Content-Type: multipart/mixed; boundary="------------299A70B339B65A93542D2AE" + +--------------299A70B339B65A93542D2AE +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +When unpacked, this message should produce two GIF files: + + * The 1st should be called "3d-compress.gif" + * The 2nd should be called "3d-eye.gif" + +There is an empty preamble, and linear space after the bounds. + +-- + ____ __ + / __/__________/_/ Eryq (eryq@rhine.gsfc.nasa.gov) + / __/ _/ / / , / Hughes STX Corporation, NASA/Goddard +/___/_/ \ /\ /___ + /_/ /_____/ http://selsvr.stx.com/~eryq/ + +--------------299A70B339B65A93542D2AE +Content-Type: image/gif +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="3d-compress.gif" + +R0lGODdhKAAoAOMAAAAAAAAAgB6Q/y9PT25ubnCAkKBSLb6+vufn5/Xes/+lAP/6zQAAAAAA +AAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJLMOyYbcoxkaZ5oCkoH6L5wLMfiWqd4btZhmxbA +oFCY47EIqMJgyWw2ATjj7aRkAq5YwDMl9VGtKO0SiuoiTVlscsxt9c4HgXxUIA0EAVOVfDKT +8Hl1B3kDAYYle202XnGGgoMHhYckiWVuR3+OTgCGeZRslotwgJ2lnYigfZdTjQULr7ALBZN0 +qTurjHgLKAu0B5Wqopm7J72etQN8t8Ijury+wMtvw8/Hv7Ylfs0BxCbGqMmK0yOOQ0GTCgrR +2bhwJGlXJQYG6mMKoeNoWSbzCWIACe5JwxQm3AkDAbUAQCiQhDZEBeBl6afgCsOBrD45edIv +QceGWSMevpOYhl6CkydBHhBZQmGKjihVshypjB9ClAHZMTugzOU7mzhBPiSZ5uDNnA7b/aTZ +0mhMnfl0pDBFa6bUElSPWb0qtYuHrxlwcR17YsWMs2jTql3LFkQEADs= +--------------299A70B339B65A93542D2AE +Content-Type: image/gif; name="3d-eye.gif" +Content-Transfer-Encoding: base64 + +R0lGODdhKAAoAPMAAAAAAAAAzN3u/76+voiIiG5ubszd7v///+fn5wAAAAAAAAAAAAAAAAAA +AAAAAAAAACwAAAAAKAAoAAAE/hDJSau9eJbMOy4bMoxkaZ5oCkoD6L5wLMfiWns41oZt7lM7 +VujnC96IRVsPWQE4nxPjkvmsQmu8oc/KBUSVWk7XepGGLeNrxoxJO1MjILjthg/kWXQ6wO/7 ++3dCeRRjfAKHiImJAV+DCF0BiW5VAo1CElaRh5NjlkeYmpyTgpcTAKGiaaSfpwKpVQaxVatL +rU8GaQdOBAQAB7+yXliXTrgAxsW4vFabv8BOtBsBt7cGvwCIT9nOyNEIxuC4zrqKzc9XbODJ +vs7Y5ewH3d7Fxe3jB4rj8t6PuNa6r2bhKQXN17FYCBMqTGiBzSNhx5g0nEMhlsSJjiRYvDjw +E0cdGxQ/gswosoKUkmuU2FnJcsSKGTBjypxJsyaICAA7 +--------------299A70B339B65A93542D2AE-- + + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/not-mime-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/not-mime-expected.json new file mode 100644 index 00000000000..aadc5e40c99 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/not-mime-expected.json @@ -0,0 +1,36 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": null, + "alternativeBoundary": null, + "sender": { + "name": "Sebastian Rahtz", + "mailAddress": "s.rahtz@elsevier.co.uk", + "valid": true + }, + "toRecipients": [ + { + "name": "Eryq", + "mailAddress": "eryq@rhine.stx.com", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 792841098000, + "subject": "Re: HELP! Problems installing PSNFSS, and other querys", + "plainBodyText": "try reading the LaTeX Companion for more details\n\nignore the checksum error in lucida.dtx. i'll fix it\n\nsebastian\nSebastian Rahtz s.rahtz@elsevier.co.uk\nProduction Methods Group +44 1865 843662\nElsevier Science Ltd\nKidlington\nOxford, UK\n\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [], + "mailHeaders": "Return-Path: \nTo: Eryq \nFrom: s.rahtz@elsevier.co.uk (Sebastian Rahtz)\nSubject: Re: HELP! Problems installing PSNFSS, and other querys\nDate: Wed, 15 Feb 1995 09:38:18", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/not-mime.msg b/packages/node-mimimi/test/mimetools-testmsgs/not-mime.msg new file mode 100644 index 00000000000..6cd3daf5873 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/not-mime.msg @@ -0,0 +1,17 @@ +Return-Path: +To: Eryq +From: s.rahtz@elsevier.co.uk (Sebastian Rahtz) +Subject: Re: HELP! Problems installing PSNFSS, and other querys +Date: Wed, 15 Feb 1995 09:38:18 + +try reading the LaTeX Companion for more details + +ignore the checksum error in lucida.dtx. i'll fix it + +sebastian +Sebastian Rahtz s.rahtz@elsevier.co.uk +Production Methods Group +44 1865 843662 +Elsevier Science Ltd +Kidlington +Oxford, UK + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/re-fwd-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/re-fwd-expected.json new file mode 100644 index 00000000000..8427f878199 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/re-fwd-expected.json @@ -0,0 +1,39 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": null, + "alternativeBoundary": null, + "sender": { + "name": "", + "mailAddress": "user2", + "valid": false + }, + "toRecipients": [], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 960261611000, + "subject": "Re: Fwd: hello world", + "plainBodyText": null, + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "Fwd_ hello world.eml", + "data": "Q29udGVudC1EaXNwb3NpdGlvbjogaW5saW5lDQpDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiBiaW5hcnkNCkNvbnRlbnQtVHlwZTogbWVzc2FnZS9yZmM4MjINCk1JTUUtVmVyc2lvbjogMS4wDQpYLU1haWxlcjogTUlNRTo6TGl0ZSAxLjE0NyAgKEIyLjA5OyBRMi4wMykNCkRhdGU6IFR1ZSwgNiBKdW4gMjAwMCAwMzoyMDoxMSBVVA0KRnJvbTogdXNlcjENClRvOiB1c2VyMg0KU3ViamVjdDogRndkOiBoZWxsbyB3b3JsZA0KDQpDb250ZW50LURpc3Bvc2l0aW9uOiBpbmxpbmUNCkNvbnRlbnQtTGVuZ3RoOiA2MA0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogYmluYXJ5DQpDb250ZW50LVR5cGU6IHRleHQvcGxhaW4NCk1JTUUtVmVyc2lvbjogMS4wDQpYLU1haWxlcjogTUlNRTo6TGl0ZSAxLjE0NyAgKEIyLjA5OyBRMi4wMykNCkRhdGU6IFR1ZSwgNiBKdW4gMjAwMCAwMzoyMDoxMSBVVA0KRnJvbTogdXNlcjANClRvOiB1c2VyMQ0KU3ViamVjdDogaGVsbG8gd29ybGQNCg0KVGhpcyBpcyB0aGUgb3JpZ2luYWwgbWVzc2FnZS4NCkxldCdzIHNlZSBpZiB3ZSBjYW4gZW1iZWQgaXQhDQo=", + "mimeType": "message/rfc822", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "Content-Disposition: inline\nContent-Transfer-Encoding: binary\nContent-Type: message/rfc822\nMIME-Version: 1.0\nX-Mailer: MIME::Lite 1.147 (B2.09; Q2.03)\nDate: Tue, 6 Jun 2000 03:20:11 UT\nFrom: user2\nTo: user0\nSubject: Re: Fwd: hello world", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/re-fwd.msg b/packages/node-mimimi/test/mimetools-testmsgs/re-fwd.msg new file mode 100644 index 00000000000..025010c8426 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/re-fwd.msg @@ -0,0 +1,33 @@ +Content-Disposition: inline +Content-Transfer-Encoding: binary +Content-Type: message/rfc822 +MIME-Version: 1.0 +X-Mailer: MIME::Lite 1.147 (B2.09; Q2.03) +Date: Tue, 6 Jun 2000 03:20:11 UT +From: user2 +To: user0 +Subject: Re: Fwd: hello world + +Content-Disposition: inline +Content-Transfer-Encoding: binary +Content-Type: message/rfc822 +MIME-Version: 1.0 +X-Mailer: MIME::Lite 1.147 (B2.09; Q2.03) +Date: Tue, 6 Jun 2000 03:20:11 UT +From: user1 +To: user2 +Subject: Fwd: hello world + +Content-Disposition: inline +Content-Length: 60 +Content-Transfer-Encoding: binary +Content-Type: text/plain +MIME-Version: 1.0 +X-Mailer: MIME::Lite 1.147 (B2.09; Q2.03) +Date: Tue, 6 Jun 2000 03:20:11 UT +From: user0 +To: user1 +Subject: hello world + +This is the original message. +Let's see if we can embed it! diff --git a/packages/node-mimimi/test/mimetools-testmsgs/russian-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/russian-expected.json new file mode 100644 index 00000000000..b70a087a78d --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/russian-expected.json @@ -0,0 +1,39 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": null, + "alternativeBoundary": null, + "sender": { + "name": "", + "mailAddress": "", + "valid": false + }, + "toRecipients": [], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "Greetings", + "plainBodyText": null, + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "Бписок.doc", + "data": "U2FsdXRhdGlvbnMNCg0KDQo=", + "mimeType": "text/plain", + "charset": "us-ascii", + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "Content-Type: text/plain; charset=\"US-ASCII\"; name==?koi8-r?B?89DJ08/LLmRvYw==?=\nContent-Disposition: attachment; filename==?koi8-r?B?89DJ08/LLmRvYw==?=\nSubject: Greetings", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/russian.msg b/packages/node-mimimi/test/mimetools-testmsgs/russian.msg new file mode 100644 index 00000000000..275bec60145 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/russian.msg @@ -0,0 +1,7 @@ +Content-Type: text/plain; charset="US-ASCII"; name==?koi8-r?B?89DJ08/LLmRvYw==?= +Content-Disposition: attachment; filename==?koi8-r?B?89DJ08/LLmRvYw==?= +Subject: Greetings + +Salutations + + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/sig-uu-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/sig-uu-expected.json new file mode 100644 index 00000000000..c8f05d6abe1 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/sig-uu-expected.json @@ -0,0 +1,30 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": null, + "alternativeBoundary": null, + "sender": { + "name": "", + "mailAddress": "", + "valid": false + }, + "toRecipients": [], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "Here's my UU'ed sig!", + "plainBodyText": "Well, I don't know much about how these things are output, \nso here goes... first off, my .sig file:\n\nbegin 644 .signature\nM(\"!?7U\\@(%\\@7R!?(\"`@7R`@7U]?(%\\@(\"!%7$*?\"`@7U\\O?\"!\\('P@?%]\\('P@?%]\\('P@(\"`*(%Q?\nM7U]\\?%]\\(\"!<7U\\L('Q<7U\\L('Q?7U\\O7\"`@5FES:70@4U1214545TE312P@\nM0VAI8V%G;R=S(&YE=W-P87!E2\\*(\"`@(\"`@(\"`@(\"!\\7U]?+R`@(\"!\\\nM7U]?7U]?+R!O9B!T:&4@:&]M96QE+Z3Q4C@U3S$I@0XC#J?UFD2]3I.JU5O#+R4CLD4,;IK7I>%[EX[+M\\,\n+C/A\\<0=Y^J4)`#L`\nend\n\n\nDone!\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [], + "mailHeaders": "Content-type: text/plain\nSubject: Here's my UU'ed sig!", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/sig-uu.msg b/packages/node-mimimi/test/mimetools-testmsgs/sig-uu.msg new file mode 100644 index 00000000000..ec122c8afd3 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/sig-uu.msg @@ -0,0 +1,29 @@ +Content-type: text/plain +Subject: Here's my UU'ed sig! + +Well, I don't know much about how these things are output, +so here goes... first off, my .sig file: + +begin 644 .signature +M("!?7U\@(%\@7R!?("`@7R`@7U]?(%\@("!%2\*("`@("`@("`@("!\7U]?+R`@("!\ +M7U]?7U]?+R!O9B!T:&4@:&]M96QE+Z3Q4C@U3S$I@0XC#J?UFD2]3I.JU5O#+R4CLD4,;IK7I>%[EX[+M\, ++C/A\<0=Y^J4)`#L` +end + + +Done! diff --git a/packages/node-mimimi/test/mimetools-testmsgs/simple-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/simple-expected.json new file mode 100644 index 00000000000..53182ea750a --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/simple-expected.json @@ -0,0 +1,47 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": null, + "alternativeBoundary": null, + "sender": { + "name": "", + "mailAddress": "eryq@rhine.gsfc.nasa.gov", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "sitaram@selsvr.stx.com", + "valid": true + } + ], + "ccRecipients": [ + { + "name": "", + "mailAddress": "johnson@killians.gsfc.nasa.gov", + "valid": true + }, + { + "name": "", + "mailAddress": "harvel@killians.gsfc.nasa.gov", + "valid": true + } + ], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 819511140000, + "subject": "Request for Leave", + "plainBodyText": "I will be taking vacation from Friday, 12/22/95, through 12/26/95. \r\nI will be back on Wednesday, 12/27/95.\r\n\r\nAdvance notice: I may take a second stretch of vacation after that, \r\naround New Year's.\r\n\r\nThanks,\r\n ____ __\r\n| _/__________/_/ Eryq (eryq@rhine.gsfc.nasa.gov)\r\n| _| _/ | | . | Hughes STX Corporation, NASA/Goddard Space Flight Cntr.\r\n|___|_|\\_ |_ |___ \r\n | | |____/ http://selsvr.stx.com/~eryq/\r\n `-'\r\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [], + "mailHeaders": "Return-Path: eryq@rhine.gsfc.nasa.gov\nDate: Wed, 20 Dec 95 19:59 CST\nFrom: eryq@rhine.gsfc.nasa.gov\nTo: sitaram@selsvr.stx.com\nCc: johnson@killians.gsfc.nasa.gov,harvel@killians.gsfc.nasa.gov, eryq\nSubject: Request for Leave", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/simple.msg b/packages/node-mimimi/test/mimetools-testmsgs/simple.msg new file mode 100644 index 00000000000..7b97a44d01a --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/simple.msg @@ -0,0 +1,20 @@ +Return-Path: eryq@rhine.gsfc.nasa.gov +Date: Wed, 20 Dec 95 19:59 CST +From: eryq@rhine.gsfc.nasa.gov +To: sitaram@selsvr.stx.com +Cc: johnson@killians.gsfc.nasa.gov,harvel@killians.gsfc.nasa.gov, eryq +Subject: Request for Leave + +I will be taking vacation from Friday, 12/22/95, through 12/26/95. +I will be back on Wednesday, 12/27/95. + +Advance notice: I may take a second stretch of vacation after that, +around New Year's. + +Thanks, + ____ __ +| _/__________/_/ Eryq (eryq@rhine.gsfc.nasa.gov) +| _| _/ | | . | Hughes STX Corporation, NASA/Goddard Space Flight Cntr. +|___|_|\_ |_ |___ + | | |____/ http://selsvr.stx.com/~eryq/ + `-' diff --git a/packages/node-mimimi/test/mimetools-testmsgs/ticket-60931-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/ticket-60931-expected.json new file mode 100644 index 00000000000..5754c940e49 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/ticket-60931-expected.json @@ -0,0 +1,30 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": null, + "alternativeBoundary": "90e6ba4fc6ea25d329048ec69d99", + "sender": { + "name": "", + "mailAddress": "", + "valid": false + }, + "toRecipients": [], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "", + "plainBodyText": "HELLO\n", + "htmlBodyText": "HELLO
\n", + "attachedMessages": [], + "attachedFiles": [], + "mailHeaders": "MIME-Version: 1.0\nReceived: by 10.220.78.157 with HTTP; Thu, 26 Aug 2010 21:33:17 -0700 (PDT)\nContent-Type: multipart/alternative; boundary=90e6ba4fc6ea25d329048ec69d99", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/ticket-60931.msg b/packages/node-mimimi/test/mimetools-testmsgs/ticket-60931.msg new file mode 100644 index 00000000000..fd9bac4e915 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/ticket-60931.msg @@ -0,0 +1,15 @@ +MIME-Version: 1.0 +Received: by 10.220.78.157 with HTTP; Thu, 26 Aug 2010 21:33:17 -0700 (PDT) +Content-Type: multipart/alternative; boundary=90e6ba4fc6ea25d329048ec69d99 + +--90e6ba4fc6ea25d329048ec69d99 +Content-Type: text/plain; charset=ISO-8859-1 + +HELLO + +--90e6ba4fc6ea25d329048ec69d99 +Content-Type: text/html; charset=ISO-8859-1 + +HELLO
+ +--90e6ba4fc6ea25d329048ec69d99-- diff --git a/packages/node-mimimi/test/mimetools-testmsgs/twopart-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/twopart-expected.json new file mode 100644 index 00000000000..d1c6072d714 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/twopart-expected.json @@ -0,0 +1,45 @@ +{ + "exception": null, + "result": { + "id": "102.10200000000105", + "boundary": "=_wowlgpostcardsender102.10200000000105_=", + "alternativeBoundary": null, + "sender": { + "name": "", + "mailAddress": "0647842285@uk.wowlg.com", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "wowlgcard@cpostale.com", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 1105513692000, + "subject": "", + "plainBodyText": "JOY LEE;Batiment Le Rabelais 22 Ave. des Nations ZI PARIS NORD II; VILLEPINTE 93240;Greece#This is test message.\nTΓ©rminos y Condiciones\nΒΏContraseΓ±a? Álbum\nΓͺtes le propriΓ©taire\nfΓΌr geschΓ€ftliche\n\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "_home1_eucyon_data_img_event_postcard_uk7200_110_6.jpg", + "data": "/9j/4AAQSkZJRgABAgEASABIAAD/7Ri2UGhvdG9zaG9wIDMuMAA4QklNA+0KUmVzb2x1dGlvbgAAAAAQAEgAAAABAAIASAAAAAEAAjhCSU0EDRhGWCBHbG9iYWwgTGlnaHRpbmcgQW5nbGUAAAAABAAAAHg4QklNBBkSRlggR2xvYmFsIEFsdGl0dWRlAAAAAAQAAAAeOEJJTQPzC1ByaW50IEZsYWdzAAAACQAAAAAAAAAAAQA4QklNBAoOQ29weXJpZ2h0IEZsYWcAAAAAAQAAOEJJTScQFEphcGFuZXNlIFByaW50IEZsYWdzAAAAAAoAAQAAAAAAAAACOEJJTQP1F0NvbG9yIEhhbGZ0b25lIFNldHRpbmdzAAAASAAvZmYAAQBsZmYABgAAAAAAAQAvZmYAAQChmZoABgAAAAAAAQAyAAAAAQBaAAAABgAAAAAAAQA1AAAAAQAtAAAABgAAAAAAAThCSU0D+BdDb2xvciBUcmFuc2ZlciBTZXR0aW5ncwAAAHAAAP////////////////////////////8D6AAAAAD/////////////////////////////A+gAAAAA/////////////////////////////wPoAAAAAP////////////////////////////8D6AAAOEJJTQQIBkd1aWRlcwAAAAAQAAAAAQAAAkAAAAJAAAAAADhCSU0EHg1VUkwgb3ZlcnJpZGVzAAAABAAAAAA4QklNBBoGU2xpY2VzAAAAAHUAAAAGAAAAAAAAAAAAAADwAAABXgAAAAoAVQBuAHQAaQB0AGwAZQBkAC0AMgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABXgAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOEJJTQQREUlDQyBVbnRhZ2dlZCBGbGFnAAAAAQEAOEJJTQQUF0xheWVyIElEIEdlbmVyYXRvciBCYXNlAAAABAAAAAI4QklNBAwVTmV3IFdpbmRvd3MgVGh1bWJuYWlsAAAVDQAAAAEAAABwAAAATQAAAVAAAGUQAAAU8QAYAAH/2P/gABBKRklGAAECAQBIAEgAAP/uAA5BZG9iZQBkgAAAAAH/2wCEAAwICAgJCAwJCQwRCwoLERUPDAwPFRgTExUTExgR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", + "mimeType": "image/jpeg", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "From 0647842285@uk.wowlg.com Wed Jan 12 08:08:13 2005\nReturn-Path: <0647842285@uk.wowlg.com>\nDelivered-To: wowlgcard@cpostale.com\nReceived: from WOWLG POSTCARD (unknown [57.67.194.147])\r\n\tby lbn-int.qualimucho.com (Postfix) with SMTP id B33BE3F06\r\n\tfor ; Wed, 12 Jan 2005 08:08:12 +0100 (CET)\nMIME-Version: 1.0 \nFrom: 0647842285@uk.wowlg.com\nTo: wowlgcard@cpostale.com\nMessage-Id:102.10200000000105\nContent-Type: multipart/mixed; boundary=\"=_wowlgpostcardsender102.10200000000105_=\" \nDate: Wed, 12 Jan 2005 08:08:12 +0100 (CET)", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/twopart.msg b/packages/node-mimimi/test/mimetools-testmsgs/twopart.msg new file mode 100644 index 00000000000..aac328706b8 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/twopart.msg @@ -0,0 +1,571 @@ +From 0647842285@uk.wowlg.com Wed Jan 12 08:08:13 2005 +Return-Path: <0647842285@uk.wowlg.com> +Delivered-To: wowlgcard@cpostale.com +Received: from WOWLG POSTCARD (unknown [57.67.194.147]) + by lbn-int.qualimucho.com (Postfix) with SMTP id B33BE3F06 + for ; Wed, 12 Jan 2005 08:08:12 +0100 (CET) +MIME-Version: 1.0 +From: 0647842285@uk.wowlg.com +To: wowlgcard@cpostale.com +Message-Id:102.10200000000105 +Content-Type: multipart/mixed; boundary="=_wowlgpostcardsender102.10200000000105_=" +Date: Wed, 12 Jan 2005 08:08:12 +0100 (CET) + +This is a multi-part message in MIME format... + +--=_wowlgpostcardsender102.10200000000105_= +Content-Type: text/plain; charset="ISO-8859-1" +Content-Disposition: inline +Content-Transfer-Encoding: 8bit + +JOY LEE;Batiment Le Rabelais 22 Ave. des Nations ZI PARIS NORD II; VILLEPINTE 93240;Greece#This is test message. +Tιrminos y Condiciones +ΏContraseρa? Αlbum +κtes le propriιtaire +fόr geschδftliche + + +--=_wowlgpostcardsender102.10200000000105_= +Content-Type:image/jpeg; name="/home1/eucyon/data/img_event/postcard/uk7200_110_6.jpg" +Content-Disposition: attachment;filename="/home1/eucyon/data/img_event/postcard/uk7200_110_6.jpg" +Content-transfer-encoding: base64 + +/9j/4AAQSkZJRgABAgEASABIAAD/7Ri2UGhvdG9zaG9wIDMuMAA4QklNA+0KUmVzb2x1dGlvbgAA +AAAQAEgAAAABAAIASAAAAAEAAjhCSU0EDRhGWCBHbG9iYWwgTGlnaHRpbmcgQW5nbGUAAAAABAAA +AHg4QklNBBkSRlggR2xvYmFsIEFsdGl0dWRlAAAAAAQAAAAeOEJJTQPzC1ByaW50IEZsYWdzAAAA +CQAAAAAAAAAAAQA4QklNBAoOQ29weXJpZ2h0IEZsYWcAAAAAAQAAOEJJTScQFEphcGFuZXNlIFBy +aW50IEZsYWdzAAAAAAoAAQAAAAAAAAACOEJJTQP1F0NvbG9yIEhhbGZ0b25lIFNldHRpbmdzAAAA +SAAvZmYAAQBsZmYABgAAAAAAAQAvZmYAAQChmZoABgAAAAAAAQAyAAAAAQBaAAAABgAAAAAAAQA1 +AAAAAQAtAAAABgAAAAAAAThCSU0D+BdDb2xvciBUcmFuc2ZlciBTZXR0aW5ncwAAAHAAAP////// +//////////////////////8D6AAAAAD/////////////////////////////A+gAAAAA//////// +/////////////////////wPoAAAAAP////////////////////////////8D6AAAOEJJTQQIBkd1 +aWRlcwAAAAAQAAAAAQAAAkAAAAJAAAAAADhCSU0EHg1VUkwgb3ZlcnJpZGVzAAAABAAAAAA4QklN +BBoGU2xpY2VzAAAAAHUAAAAGAAAAAAAAAAAAAADwAAABXgAAAAoAVQBuAHQAaQB0AGwAZQBkAC0A +MgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABXgAAAPAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAOEJJTQQREUlDQyBVbnRhZ2dlZCBGbGFnAAAAAQEAOEJJTQQUF0xh +eWVyIElEIEdlbmVyYXRvciBCYXNlAAAABAAAAAI4QklNBAwVTmV3IFdpbmRvd3MgVGh1bWJuYWls +AAAVDQAAAAEAAABwAAAATQAAAVAAAGUQAAAU8QAYAAH/2P/gABBKRklGAAECAQBIAEgAAP/uAA5B +ZG9iZQBkgAAAAAH/2wCEAAwICAgJCAwJCQwRCwoLERUPDAwPFRgTExUTExgRDAwMDAwMEQwMDAwM +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 +IxoVFRojIhcXFxcXIhEMDAwMDAwRDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAEUExMWGRYb +FxcbFA4ODhQUDg4ODhQRDAwMDAwREQwMDAwMDBEMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM +/8AAEQgA8AFeAwEiAAIRAQMRAf/dAAQAFv/EAT8AAAEFAQEBAQEBAAAAAAAAAAMAAQIEBQYHCAkK +CwEAAQUBAQEBAQEAAAAAAAAAAQACAwQFBgcICQoLEAABBAEDAgQCBQcGCAUDDDMBAAIRAwQhEjEF +QVFhEyJxgTIGFJGhsUIjJBVSwWIzNHKC0UMHJZJT8OHxY3M1FqKygyZEk1RkRcKjdDYX0lXiZfKz +hMPTdePzRieUpIW0lcTU5PSltcXV5fVWZnaGlqa2xtbm9jdHV2d3h5ent8fX5/cRAAICAQIEBAME +BQYHBwYFNQEAAhEDITESBEFRYXEiEwUygZEUobFCI8FS0fAzJGLhcoKSQ1MVY3M08SUGFqKygwcm +NcLSRJNUoxdkRVU2dGXi8rOEw9N14/NGlKSFtJXE1OT0pbXF1eX1VmZ2hpamtsbW5vYnN0dXZ3eH +l6e3x//aAAwDAQACEQMRAD8A7bYFKAkkSAiigs5u5pbxPgsXLYytxG6VrvG9hBJY3ueNFzuSaxY5 +tR36Fw/eLWfSe2tvvf8A+fFl/EIkygBH1fv8X/qNt8pqSb08v+6Q2WMAOvCgMq6v+be5p7wS1V3Z +DCSBrGh00lDdZx+VVowIrcF0aBFbti7NybG7bLXOb4ElV2NN7y3dta0SXf8AfPzvzEGxxI+78idh +aaN1d4bbWDupLfpH97fu/wCt+p/o1OISlrfr/rLZERG1Rv8ARDp2lux0z6RgAxLdoP0fas9zy93i +OCeJktb6ntRLXSweEaQZ/t/nsQnktdPZ/wBIa/5vv/8ARajhGkg0EkwWhokyBPxPt9yiXPbuEQ4a +OB51/eYovJAYeTE/eVVyMrbVO4usbMg66H+X/OP+knxiSxymALTt6rm4mSRTe4A+4Ncd9f7tn6F/ +8v8A4uxdV0frbc+arG7Mhokx9B4/er/cXn92ZvrFbQCJBPciPzmfuI+B1G7FvZfXBe3TaeCP3Vfg +ZRA/50WjLhlf736M31BJc1jfW7GcIyKLKj+8yLWf+irv/AlsYvVunZZ20ZDXO/dMsd/23d6b1NY6 +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 +AIJJuOTEonpe7YPmNfD6P76PCVP/2Q== +--=_wowlgpostcardsender102.10200000000105_=-- diff --git a/packages/node-mimimi/test/mimetools-testmsgs/uu-junk-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/uu-junk-expected.json new file mode 100644 index 00000000000..da584495c1c --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/uu-junk-expected.json @@ -0,0 +1,36 @@ +{ + "exception": null, + "result": { + "id": "200004251134.GAA10264@webmail.uwohali.com", + "boundary": "---------------------------7d033e3733c", + "alternativeBoundary": null, + "sender": { + "name": "ADJE Webmail Tech Support", + "mailAddress": "support@webmail.uwohali.com", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "eryq@zeegee.com", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 956662483000, + "subject": "mime::parser", + "plainBodyText": "Eryq -\r\n\r\nI occasionally receive an email (see below) like this one, which\r\nMIME::Parser does not parse. Any ideas? Is this a valid way to send\r\nan attachment, or is the problem on the \"sender's\" side? Thanks for\r\nyour time!\r\n\r\n Mike\r\n\r\n-->> Promote YOUR web site! FREE Perl CGI scripts add WEB ACCESS to your\r\n-->> E-Mail accounts! Download today!! http://webmail.uwohali.com\r\nHere's what he's talking about. I've uuencoded the ZeeGee\r\nlogo and another GIF file below.\r\n\r\nbegin 644 up.gif\r\nM1TE&.#=A$P`3`*$``/___P```(\"`@,#`P\"P`````$P`3```\"1X2/F<'MSTQ0\r\nM%(@)YMB\\;W%)@$<.(*:5W2F2@<=F8]>LH4P[7)P.T&NZI7Z,(&JF^@B121Y3\r\n4Y4SNEJ\"J]8JZ:JTH(K$\"/A0``#L`\r\n`\r\nend\r\n\r\nbegin 644 zeegee.gif\r\nM1TE&.#=A6P!P`/<```````@(\"!`0$#D`(3D`*4(`*1@8&$H`*4H`,5(`,5(`\r\nM.5H`,2$A(5H`.5((,6,`.6,`0EH(.6L`0F,(.6,(0BDI*5H0.6L(0FL(2F,0\r\nM.6,00G,(2C$Q,4(I,6L02GL(4G,04G,02FL80H0(4H0(6E(I.7,80HP(6CDY\r\nM.6LA0D(Y.90(6I0(8XP06G,A2H084H086I008U(Y0GLA4D)\"0G,I2H0A2G,I\r\nM4H0A4H0A6H0I4I0A8TI*2FLY4GLQ6G,Y4I0I8U)24F-*4EI22I0Q8Y0Q:YPQ\r\nM:Y0Y6I0Y8X1\"6I0Y:UI:6HQ\"6H1*8Z4YV-C8WM:\r\nM8Z5\"X1C:Y1:ZU2WM[>Y1S[UC>XQ[\r\nM>YQS>[5K>[UCE)Q[>X2$A+UKA)Q[E*5[>[UKC*5[A*5[E)R$>[USC*5[G-YC\r\nMC(R,C*U[G*6$A,YKE+U[C*V$A+5[G,YSC*V$C+5[I=YKE)24E*V,C+6$I;V$\r\nMI;6,C+6,E-9[G+V,C*V4C+V,E-Y[E-Y[G)R>$I;V^,I:VM\r\nMK<:EI>4O>^4M=:EI=:EK>>>EK?>>EM=ZMK>>EO=ZMM>^EK?^^EO?^MK>>MM>^ESO>EO?^E\r\nMO>^MO?^EQN>UM?^ESO>MQL;&QN^UM=Z]M>^UO?^MO?>UO>>]O>^]O?^USO^U\r\nMQO>]O<[.SN?&O?>]QO^]QO^]SO?&QO^]WO?&SM;6UO_&QO_&SO_&WO_&Y__.\r\nMSN_6SO_.UM[>WO_.WO_6UO_6WO_6Y^?GY__>WO_>Y__GWO_GY__G[__G]^_O\r\nM[__O[_?W]__W]__W_____RP`````6P!P``<(_P#_\"1Q(L*#!@P@3*ES(L*'#\r\nMAQ`C2IQ(L:+%BQ@S:I08J:-'2\"!#BAP9DI')1(12JES)LF6@ES!C^IE)L^9,\r\nM/3ASZE2XH:?/GAB\"8KA`M\"B%HT@A*(7PH('3IU`;*)BJ(('5JPBR:M5ZH&N!\r\nMKV#!$AA+8(#9LSQ__A0ZM\"A1I$F7-HT:E6K5JUBW379\r\nMU[\"/(]_0V3-SN:,=/_^&[/MW];YCL6>/C;S[7]U66H(+\\-6C;;?&%)R%>U)U7\r\nM@$(C*(CA=H9YQR&$$4;76XA]D5BBB?NAJ-Q[<.76HHL@9I5`!M.9=Z\",,]*H\r\nM77LW(I9CA]#Q:)4-9#22R2:*\".+\"9'P1.4*1-&;8X'?./4?78PM$T8@K=4A1\r\nM@P4U,*$($WN-F!`)6]9I9'9>!E7;BBQZJ(`5I6RBA@B1I7\"(!7LI1`*==7+9\r\nMI8UZJKADGXV]4`HQ9U@@759()+'5`8HNVJBC)N89J9*3BMG``W8\\H\\@,]('_\r\nMB$,3>H6Z**-VWKG@9AJ\"&:!3+\\CBRA/B.6F5!F_4FM`)MXK:J*Z[$M8K@`%:\r\nM$4TC)M05JU6'*(O0\"$>01)`(*@1NNN,_J\r\nM:NJ>H+VP##'$RL=;!F/D5:^]]SJ;ZYW[2GH4$>2$,!DDTBD`)G&&W,\\*K0-NV4'.89XH-C)8RKP!*%XL8QPPAV3\r\nM6RZ#21HB#AO4JDJ:&!D0F(#//[O\\,L.0ZHG*-EB@FZK2#5R`!7T*L?!SU%)[\r\nM_#'1&,BRC1(X/DAQ`Q'H@(48+@Q85=ABC]WRK5-3_[U=*MP4P>^DO]90BSWW\r\nMW*.+!G;CS<(*>MM;]I8P(W=U$>?ZVI0&M'S#3C_WL+/*`DY-Y3@+>4?.<=\"5\r\nMJW6U$?V%[.\"#7[3B2S7L?).-+STCL$@#;Q81_K@P<`&RB,=QV!@.Q3X\"S2`\r\nM8%IP:8(02C`$(0AA\"\"5(P/\\0(L`2IDYUXH*!';\"A0/4UL('O4$<$7SC!=8`#\r\nM$!>47?0\\4`49J.\"'*G!`5/_J94(!@@^%BWJ#-<[1PG:\\,![Q>$<^@@'%>-!0\r\nM@>C`QAF^!!B`$(>XK!.LH(@!)\"`)@'\"*;IP#'2U\\(3SBD0YYS***\r\nM5KSB.LX!CD_D`'IPT4(*(L`DC9T1C2?\\612@X48FQI&&\\CC&._`HQQJB@X_4\r\nM4`(@*:\"#)$CO5SY#9\"+!E09&@N.-\"EQ@`]NA#F2L@Y*53.4YSM$-:H3A2V(0\r\nMP=8:`+5#HA%A>8\"&-;HQ#E0^$A[M(`@P0;_X$`*?&(*\r\nMU,PH2E(F`QK:(*8QT^=$>&Q#&N!H9B6?&8H99O@$F47\r\nM@*'_#&J$LYCC5!\\WEH$-==*0G;.4IC:@`0Q-L@4'1T!57/`)+E\\*T`FX``8T\r\nM_-F-4XYS'=R0QC#*6<55KB^5T$QH-Q::C%/H('9DT(##D*(H`EJ4!3$812TT\r\nMRE%P`!2.ZPB'-&SAQ'C0XQ[XN$<]Z(%,E&)QEN-0*#24`8Q+A(`[.V#\"821*\r\nM@5`1\\`0\"Y`,M=KI1:X33HV\\4QS-F<0XGU@,?_A#(/N[!5*=>4J7:>\"=5:3&&\r\nMGIS!`YF#BZV^BKH=T&*L_!1F.#LZRW,TPQ8L;`<]\\%&0?-0#'JE$QUWYV(V5\r\nMZA48M/`$\"(QPA!1)RE:+LBD@8G'8Q%+#K)T]93.:P0LX_\\*C'OTP\"#[B`LEQB'>\\U@BN<`^KBDY<05KGTM)R$=:\"3IC\"%*I@K72_6=9BA((:\r\nML06'=N-QCWV8>!]UY:,TQTM>:$`C&<`X;RQ,P0E$\\\"I)6AK!A,'E!$YXXL+0\r\nMC2XP.$R,69Q\"&TB.+1/+*<$]KGBE>4VPBV$,6O326!(^N+%R7C.J'<>!$YRP\r\nM<'V%#(Q>%.,0U'BM61?KT_!\"=?_%2(ZR@I5!Y<.N0A6>X,0CFH`V/6%G7,L]\r\nM!)C#C&'61G<8R`\"$,EQ<5FNL.JZ:?+MX*!\r\nM)\"9Q\"3#_.,.TR$4Q*)$+?BZ:T1M-%\r\nM0%T$21A;$J8N]\"YF<0E<2U<9K[:UM%T,;6?7XM*9SC,G)%$(.,PO0\\/NLA*.\r\nM;>QDLZ(8=3CL3IT-#&%`&]KN?KNNM\\2M?5A=WYG7^O;U'[%YG_J`1]5V%HB-?\"%\\#PA#T97:%W&'QN\\!Y7M0>++]3M^5/_?RE=?YA3W!]8P_0O!T<`,8JN`\"(Y7=\r\nM(OQ(O>I7S_K6N_[UL(?]1A32>GW$_O:XSST_9D][U=M>]ZLO\"/!?S_O>\\^/W\r\nMPU_(\\%5?_(',0Q\\$\\?WR$<(/=\\SC^0))OD4&P8#N>]_[CG!'__2/O_S=%\\08\r\nM/!```-8?`!08XQ_`OT@;UD__^FO\"_`,A__3S3X7Z&Z#^2X!\\L8<1Y@`*!FB`\r\nMFA```,``YF`0YB=[^<=\\V<<#Z\\+!^CE`0\r\nMW@`*FG\"!U]=Z_U\"`FJ`)&)AZ`U$&ZX<'$9AZMZ\"`K]!ZT_\"\"H`!]PG=]/9B\"\r\nM0.@0\\Z\"`/#`/`^$.*%!_`!`$WA\"#J><.%%A_/.`-0.@.\"K@%XZ=Z\\V!]7F@,\r\nM'%!_`4`%^*'MQ\"#YI\"'=^B'\r\nMM[![(R@`^'=[\\_`*\"FB'>+A^%0!]_/]P@G7(`#S``.O'`&V($-.P?EOP@)3(\r\nM`=XP$--0`0#``<9@>Z*(`MZ@>MXPAA6`@11(!=D'?_`7@O\"G#].@?CQ@#JIW\r\nMBT_8@L:`B^(G$(Z`BPPABA7PB0*A\"788C,ZG?GC@#LIHB06A#_\\W\"/PPAGC@\r\nMA>ZPC=RXC>;PA?V'`LRW>[>P?FG8?QP0@OPP@@#0@`GA\"\"1($$$``#SP@O;X\r\nM@C0``#3@#?,8!/=HC_G(`_S0A&6@>J_@A/77!N9`B5OPCYK@\")0X\"+=`B53@\r\nM\"/\\(D0`P\"`FA#[BHA`,QA@A9?Q5@#B`9DA7(#V](`[]7CB;Y\"MY@DO6W!0<)\r\nMD^M7!@G1?W'_6!!-6`$HL`14\\)-`N01M,)`+&`1+<)1(>91#J8P!D(K'UXWN\r\nMD(;J]X7KQP-'\"914<)6O,)-!@)59J968J(FTN`7T.`W,*'QDF8NL%WT<28\\\"\r\nMZ'OSZ('\\0(GW1Q#Z,`_F-PW_IPG19PZ7:!!CR`%G*1`L\"0K?.!#ZD)6ZF(D`\r\nM``K/9WZ)N00QJ(Q/B)>KIP]O*`\"ZN(X+Z)'_<)>OX),\"\\8;2^)E1J0FB>1\"@\r\nM```!L(=0.0W3,(\\!H)\"I9PRG.!\"RV08Q.)$`((ZJ-W]P6`:O8`Z:4`9Y&(BI\r\nM1XT+N(/\\8`YXH(`V^)G_QP\"EJ`_>T`;0B1#`&9*TF8^L:0\"1Z)GZ_[\"&`0\">\r\nME6B9JN<(B>B$##`-P=>'ZR<`!I\"(2T`0=!B?\\[E^L(@0$/E]WF<`=?D/@Y\"'\r\nM<+@%E\\@/;5\"'!?J6J3$H-\">LSB`.$VQB%([B'7QB\"\r\nM:!A]*)I_M_\"\"@3@/AQF%U@<*GYB!MNB#[O^H$.7H\"%@H$-9'$*\"@H`!0`;>@\r\nMA.Q(?V19?PP0`\"[*`Z\"ZA`!@D_P`\"@2ZG!#Z#WJ)`I28C1JXAO07!']IEP'@\r\nMB<^G#VU(@T'0HO-P\"Z+H\"'YYA*TX#R?8?V4`\"N6HD0*A#PH(\"@,!CZDX\"*.8\r\nMJ8?XJ=VWI?^WCW>9@P!Z?>XP@J6)$(@8`$N@@AY9CEP8?:)Z\"]`G`.DX$,HH\r\nMA_]0`31@?DP9!-E'`PRPI?K(IZ+8K[\\8`-\"G#^[0?2[Z#R.XGPGAH)0H`&5@\r\nM#@U(`TUI$,[*`^('K_AWD/3ZG,'(`Q6P!83X#UC:!OK@H7]YD&+ZBS0@J=1*\r\nMKP3A#A40`&=Z$*__0(F]J@^C.)@\"$00&\\'X&4`$;\"P\"O(*D92;(!@`=<^G[*\r\nM:`[N(`#Z:A#F$*;_<`L\"4)];ZJ%.\"Y7A\"@!ENJ)`.`]CZ`B9&`0\\^P_S5Z5!\r\nM6X0'6;0\"P0$\"\"8_B%P\";\"+(D\"P!MA#*.)SF5XXVR0$,F+#_((D\"80`,(+G.A[WQ^N\\CCA_#-M]^%>.D_L/\r\nM\\&B^`T&!4BH0%%@&OS<-3=A^4PH`Z7N^'2A^U:<)#&``LAM]\\\\BI!3%_Y9F?\r\nMFS@0;4H0Y0BS=UO`#>RB*/F=_R<`J&J^+VF3!(&(=FB>*'#!!G&\"FL\"X2]@&\r\nM61FQ!?$*7SL//_C#;:@/_SN'6_\"3@P!]M_\"EB.J`@_\"36T#\"5EK%5GS%6)S%\r\n36KS%7-S%7OS%8!S&8IP1`<$`.P``\r\n`\r\nend\r\n\r\n\r\n\r\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [], + "mailHeaders": "From - Fri Jun 9 23:17:56 2000\nReturn-Path: \nReceived: from virtual.mrf.mail.rcn.net ([207.172.4.103])\r\n by mta02.mrf.mail.rcn.net\r\n (InterMail vM.4.01.02.27 201-229-119-110) with ESMTP\r\n id <20000425112650.ZPUD516.mta02.mrf.mail.rcn.net@virtual.mrf.mail.rcn.net>\r\n for ; Tue, 25 Apr 2000 07:26:50 -0400\nReceived: from [205.139.141.226] (helo=webmail.uwohali.com ident=root)\r\n\tby virtual.mrf.mail.rcn.net with esmtp (Exim 2.12 #3)\r\n\tid 12k3VX-00012G-00\r\n\tfor eryq@zeegee.com; Tue, 25 Apr 2000 07:27:59 -0400\nReceived: from webmail.uwohali.com (nobody@localhost [127.0.0.1])\r\n\tby webmail.uwohali.com (8.8.7/8.8.7) with SMTP id GAA10264\r\n\tfor ; Tue, 25 Apr 2000 06:34:43 -0500\nDate: Tue, 25 Apr 2000 06:34:43 -0500\nMessage-Id: <200004251134.GAA10264@webmail.uwohali.com>\nFrom: \"ADJE Webmail Tech Support\" \nTo: eryq@zeegee.com\nSubject: mime::parser\nContent-type: multipart/mixed; boundary=\"---------------------------7d033e3733c\"\nMime-Version: 1.0\nX-Mozilla-Status: 8001", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/uu-junk-target-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/uu-junk-target-expected.json new file mode 100644 index 00000000000..e86271d8618 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/uu-junk-target-expected.json @@ -0,0 +1,53 @@ +{ + "exception": null, + "result": { + "id": "200004251134.GAA10264@webmail.uwohali.com", + "boundary": "----------=_960622044-2175-0", + "alternativeBoundary": null, + "sender": { + "name": "ADJE Webmail Tech Support", + "mailAddress": "support@webmail.uwohali.com", + "valid": true + }, + "toRecipients": [ + { + "name": "", + "mailAddress": "eryq@zeegee.com", + "valid": true + } + ], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": 956662483000, + "subject": "mime::parser", + "plainBodyText": "Eryq -\r\n\r\nI occasionally receive an email (see below) like this one, which\r\nMIME::Parser does not parse. Any ideas? Is this a valid way to send\r\nan attachment, or is the problem on the \"sender's\" side? Thanks for\r\nyour time!\r\n\r\n Mike\r\n\r\n-->> Promote YOUR web site! FREE Perl CGI scripts add WEB ACCESS to your\r\n-->> E-Mail accounts! Download today!! http://webmail.uwohali.com\r\nHere's what he's talking about. I've uuencoded the ZeeGee\r\nlogo and another GIF file below.\r\n\r\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [ + { + "name": "up.gif", + "data": "R0lGODdhEwATAKEAAP///wAAAICAgMDAwCwAAAAAEwATAAACR4SPmcHtz0xQFIgJ5ti8b3FJgEcOIKaV3SmSgcdmY9esoUw7XJwO0Gu6pX6MIGqm+giRSR5T5UzulqCq9Yq6aq0oIrECPhQAADs=", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + }, + { + "name": "zeegee.gif", + "data": "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", + "mimeType": "image/gif", + "charset": null, + "contentId": "", + "calendarMethod": null + } + ], + "mailHeaders": "Return-Path: \nReceived: from virtual.mrf.mail.rcn.net ([207.172.4.103])\r\n by mta02.mrf.mail.rcn.net\r\n (InterMail vM.4.01.02.27 201-229-119-110) with ESMTP\r\n id <20000425112650.ZPUD516.mta02.mrf.mail.rcn.net@virtual.mrf.mail.rcn.net>\r\n for ; Tue, 25 Apr 2000 07:26:50 -0400\nReceived: from [205.139.141.226] (helo=webmail.uwohali.com ident=root)\r\n\tby virtual.mrf.mail.rcn.net with esmtp (Exim 2.12 #3)\r\n\tid 12k3VX-00012G-00\r\n\tfor eryq@zeegee.com; Tue, 25 Apr 2000 07:27:59 -0400\nReceived: from webmail.uwohali.com (nobody@localhost [127.0.0.1])\r\n\tby webmail.uwohali.com (8.8.7/8.8.7) with SMTP id GAA10264\r\n\tfor ; Tue, 25 Apr 2000 06:34:43 -0500\nDate: Tue, 25 Apr 2000 06:34:43 -0500\nMessage-Id: <200004251134.GAA10264@webmail.uwohali.com>\nFrom: \"ADJE Webmail Tech Support\" \nTo: eryq@zeegee.com\nSubject: mime::parser\nContent-type: multipart/mixed; boundary=\"---------------------------7d033e3733c\"\nMime-Version: 1.0\nX-Mozilla-Status: 8001", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/uu-junk-target.msg b/packages/node-mimimi/test/mimetools-testmsgs/uu-junk-target.msg new file mode 100644 index 00000000000..3f60d78d02d --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/uu-junk-target.msg @@ -0,0 +1,182 @@ +Return-Path: +Received: from virtual.mrf.mail.rcn.net ([207.172.4.103]) + by mta02.mrf.mail.rcn.net + (InterMail vM.4.01.02.27 201-229-119-110) with ESMTP + id <20000425112650.ZPUD516.mta02.mrf.mail.rcn.net@virtual.mrf.mail.rcn.net> + for ; Tue, 25 Apr 2000 07:26:50 -0400 +Received: from [205.139.141.226] (helo=webmail.uwohali.com ident=root) + by virtual.mrf.mail.rcn.net with esmtp (Exim 2.12 #3) + id 12k3VX-00012G-00 + for eryq@zeegee.com; Tue, 25 Apr 2000 07:27:59 -0400 +Received: from webmail.uwohali.com (nobody@localhost [127.0.0.1]) + by webmail.uwohali.com (8.8.7/8.8.7) with SMTP id GAA10264 + for ; Tue, 25 Apr 2000 06:34:43 -0500 +Date: Tue, 25 Apr 2000 06:34:43 -0500 +Message-Id: <200004251134.GAA10264@webmail.uwohali.com> +From: "ADJE Webmail Tech Support" +To: eryq@zeegee.com +Subject: mime::parser +Content-type: multipart/mixed; boundary="---------------------------7d033e3733c" +Mime-Version: 1.0 +X-Mozilla-Status: 8001 + +-----------------------------7d033e3733c +Content-Type: text/plain + +Eryq - + +I occasionally receive an email (see below) like this one, which +MIME::Parser does not parse. Any ideas? Is this a valid way to send +an attachment, or is the problem on the "sender's" side? Thanks for +your time! + + Mike + +-->> Promote YOUR web site! FREE Perl CGI scripts add WEB ACCESS to your +-->> E-Mail accounts! Download today!! http://webmail.uwohali.com + +-----------------------------7d033e3733c +Content-type: multipart/mixed; boundary="----------=_960622044-2175-0" + +The following is a multipart MIME message which was extracted +from a uuencoded message. + +------------=_960622044-2175-0 + +Here's what he's talking about. I've uuencoded the ZeeGee +logo and another GIF file below. + + +------------=_960622044-2175-0 +Content-Type: image/gif; name="up.gif"; x-unix-mode="0644" +Content-Disposition: inline; filename="up.gif" +Content-Transfer-Encoding: base64 +Mime-Version: 1.0 +X-Mailer: MIME-tools 5.208 (Entity 5.204) + +R0lGODdhEwATAKEAAP///wAAAICAgMDAwCwAAAAAEwATAAACR4SPmcHtz0xQ +FIgJ5ti8b3FJgEcOIKaV3SmSgcdmY9esoUw7XJwO0Gu6pX6MIGqm+giRSR5T +5UzulqCq9Yq6aq0oIrECPhQAADs= + +------------=_960622044-2175-0 +Content-Type: image/gif; name="zeegee.gif"; x-unix-mode="0644" +Content-Disposition: inline; filename="zeegee.gif" +Content-Transfer-Encoding: base64 +Mime-Version: 1.0 +X-Mailer: MIME-tools 5.208 (Entity 5.204) + +R0lGODdhWwBwAPcAAAAAAAgICBAQEDkAITkAKUIAKRgYGEoAKUoAMVIAMVIA +OVoAMSEhIVoAOVIIMWMAOWMAQloIOWsAQmMIOWMIQikpKVoQOWsIQmsISmMQ +OWMQQnMISjExMUIpMWsQSnsIUnMQUnMQSmsYQoQIUoQIWlIpOXMYQowIWjk5 +OWshQkI5OZQIWpQIY4wQWnMhSoQYUoQYWpQQY1I5QnshUkJCQnMpSoQhSnMp +UoQhUoQhWoQpUpQhY0pKSms5UnsxWnM5UpQpY1JSUmNKUlpSSpQxY5Qxa5wx +a5Q5WpQ5Y4RCWpQ5a1paWoxCWoRKY6U5c5xCY4xKa605a5RKY5xCe2NjY3ta +Y6VCc5RSa6VKa3tjY61Ka2tra61Kc61Ke4Rja5Rac3tra6VSe61Sc6Vaa7VS +c3Nzc4xra61ac61ahLVahK1jc4xzc61je3t7e5Rzc61jhJxzc61re71je4x7 +e5xze7Vre71jlJx7e4SEhL1rhJx7lKV7e71rjKV7hKV7lJyEe71zjKV7nN5j +jIyMjK17nKWEhM5rlL17jK2EhLV7nM5zjK2EjLV7pd5rlJSUlK2MjLWEpb2E +pbWMjLWMlNZ7nL2MjK2UjL2MlN57lN57nJycnN6ElL2UlMaUlMaUnNaMnKWl +peeEpb2cnM6UnNaUnO+ErcacnNaUpd6Mtd6Mvc6cnM6cpd6Upb2lpe+Mpa2t +rcalpc6lpeeUve+UtdalpdalreecrbW1td6lrd6lpd6ltfecpeelrfecrdat +reeltd6treelvd6tte+lrf+ctf+cvb29ve+lvf+cxuetreette+lzvelvf+l +ve+tvf+lxue1tf+lzvetxsbGxu+1td69te+1vf+tvfe1vee9ve+9vf+1zv+1 +xve9vc7OzufGvfe9xv+9xv+9zvfGxv+93vfGztbW1v/Gxv/Gzv/G3v/G5//O +zu/Wzv/O1t7e3v/O3v/W1v/W3v/W5+fn5//e3v/e5//n3v/n5//n7//n9+/v +7//v7/f39//39//3/////ywAAAAAWwBwAAcI/wD/CRxIsKDBgwgTKlzIsKHD +hxAjSpxIsaLFixgzapQYqaNHSCBDihwZkpHJRIRSqlzJsmWglzBj+plJs+ZM +PThz6lS4oafPnhiCYrhAtCiFo0ghKIXwoIHTp1AbKJiqIIHVqwiyatV6oGuB +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 +WrzFXNzFXvzFYBzGYpwRAcEAOw== + +------------=_960622044-2175-0-- + +-----------------------------7d033e3733c-- + + + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/uu-junk.msg b/packages/node-mimimi/test/mimetools-testmsgs/uu-junk.msg new file mode 100644 index 00000000000..f674fc3a314 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/uu-junk.msg @@ -0,0 +1,168 @@ +From - Fri Jun 9 23:17:56 2000 +Return-Path: +Received: from virtual.mrf.mail.rcn.net ([207.172.4.103]) + by mta02.mrf.mail.rcn.net + (InterMail vM.4.01.02.27 201-229-119-110) with ESMTP + id <20000425112650.ZPUD516.mta02.mrf.mail.rcn.net@virtual.mrf.mail.rcn.net> + for ; Tue, 25 Apr 2000 07:26:50 -0400 +Received: from [205.139.141.226] (helo=webmail.uwohali.com ident=root) + by virtual.mrf.mail.rcn.net with esmtp (Exim 2.12 #3) + id 12k3VX-00012G-00 + for eryq@zeegee.com; Tue, 25 Apr 2000 07:27:59 -0400 +Received: from webmail.uwohali.com (nobody@localhost [127.0.0.1]) + by webmail.uwohali.com (8.8.7/8.8.7) with SMTP id GAA10264 + for ; Tue, 25 Apr 2000 06:34:43 -0500 +Date: Tue, 25 Apr 2000 06:34:43 -0500 +Message-Id: <200004251134.GAA10264@webmail.uwohali.com> +From: "ADJE Webmail Tech Support" +To: eryq@zeegee.com +Subject: mime::parser +Content-type: multipart/mixed; boundary="---------------------------7d033e3733c" +Mime-Version: 1.0 +X-Mozilla-Status: 8001 + +-----------------------------7d033e3733c +Content-Type: text/plain + +Eryq - + +I occasionally receive an email (see below) like this one, which +MIME::Parser does not parse. Any ideas? Is this a valid way to send +an attachment, or is the problem on the "sender's" side? Thanks for +your time! + + Mike + +-->> Promote YOUR web site! FREE Perl CGI scripts add WEB ACCESS to your +-->> E-Mail accounts! Download today!! http://webmail.uwohali.com + +-----------------------------7d033e3733c + +Here's what he's talking about. I've uuencoded the ZeeGee +logo and another GIF file below. + +begin 644 up.gif +M1TE&.#=A$P`3`*$``/___P```("`@,#`P"P`````$P`3```"1X2/F<'MSTQ0 +M%(@)YMB\;W%)@$<.(*:5W2F2@<=F8]>LH4P[7)P.T&NZI7Z,(&JF^@B121Y3 +4Y4SNEJ"J]8JZ:JTH(K$"/A0``#L` +` +end + +begin 644 zeegee.gif +M1TE&.#=A6P!P`/<```````@("!`0$#D`(3D`*4(`*1@8&$H`*4H`,5(`,5(` +M.5H`,2$A(5H`.5((,6,`.6,`0EH(.6L`0F,(.6,(0BDI*5H0.6L(0FL(2F,0 +M.6,00G,(2C$Q,4(I,6L02GL(4G,04G,02FL80H0(4H0(6E(I.7,80HP(6CDY +M.6LA0D(Y.90(6I0(8XP06G,A2H084H086I008U(Y0GLA4D)"0G,I2H0A2G,I +M4H0A4H0A6H0I4I0A8TI*2FLY4GLQ6G,Y4I0I8U)24F-*4EI22I0Q8Y0Q:YPQ +M:Y0Y6I0Y8X1"6I0Y:UI:6HQ"6H1*8Z4YV-C8WM: +M8Z5"X1C:Y1:ZU2WM[>Y1S[UC>XQ[ +M>YQS>[5K>[UCE)Q[>X2$A+UKA)Q[E*5[>[UKC*5[A*5[E)R$>[USC*5[G-YC +MC(R,C*U[G*6$A,YKE+U[C*V$A+5[G,YSC*V$C+5[I=YKE)24E*V,C+6$I;V$ +MI;6,C+6,E-9[G+V,C*V4C+V,E-Y[E-Y[G)R>$I;V^,I:VM +MK<:EI>4O>^4M=:EI=:EK>>>EK?>>EM=ZMK>>EO=ZMM>^EK?^^EO?^MK>>MM>^ESO>EO?^E +MO>^MO?^EQN>UM?^ESO>MQL;&QN^UM=Z]M>^UO?^MO?>UO>>]O>^]O?^USO^U +MQO>]O<[.SN?&O?>]QO^]QO^]SO?&QO^]WO?&SM;6UO_&QO_&SO_&WO_&Y__. +MSN_6SO_.UM[>WO_.WO_6UO_6WO_6Y^?GY__>WO_>Y__GWO_GY__G[__G]^_O +M[__O[_?W]__W]__W_____RP`````6P!P``<(_P#_"1Q(L*#!@P@3*ES(L*'# +MAQ`C2IQ(L:+%BQ@S:I08J:-'2"!#BAP9DI')1(12JES)LF6@ES!C^IE)L^9, +M/3ASZE2XH:?/GAB"8KA`M"B%HT@A*(7PH('3IU`;*)BJ(('5JPBR:M5ZH&N! +MKV#!$AA+8(#9LSQ__A0ZM"A1I$F7-HT:E6K5JUBW379 +MU["/(]_0V3-SN:,=/_^&[/MW];YCL6>/C;S[7]U66H(+\-6C;;?&%)R%>U)U7 +M@$(C*(CA=H9YQR&$$4;76XA]D5BBB?NAJ-Q[<.76HHL@9I5`!M.9=Z",,]*H +M77LW(I9CA]#Q:)4-9#22R2:*".+"9'P1.4*1-&;8X'?./4?78PM$T8@K=4A1 +M@P4U,*$($WN-F!`)6]9I9'9>!E7;BBQZJ(`5I6RBA@B1I7"(!7LI1`*==7+9 +MI8UZJKADGXV]4`HQ9U@@759()+'5`8HNVJBC)N89J9*3BMG``W8\H\@,]('_ +MB$,3>H6Z**-VWKG@9AJ"&:!3+\CBRA/B.6F5!F_4FM`)MXK:J*Z[$M8K@`%: +M$4TC)M05JU6'*(O0"$>01)`(*@1NNN,_J +M:NJ>H+VP##'$RL=;!F/D5:^]]SJ;ZYW[2GH4$>2$,!DDTBD`)G&&W,\*K0-NV4'.89XH-C)8RKP!*%XL8QPPAV3 +M6RZ#21HB#AO4JDJ:&!D0F(#//[O\,L.0ZHG*-EB@FZK2#5R`!7T*L?!SU%)[ +M_#'1&,BRC1(X/DAQ`Q'H@(48+@Q85=ABC]WRK5-3_[U=*MP4P>^DO]90BSWW +MW*.+!G;CS<(*>MM;]I8P(W=U$>?ZVI0&M'S#3C_WL+/*`DY-Y3@+>4?.<="5 +MJW6U$?V%[."#7[3B2S7L?).-+STCL$@#;Q81_K@P<`&RB,=QV!@.Q3X"S2` +M8%IP:8(02C`$(0AA""5(P/\0(L`2IDYUXH*!';"A0/4UL('O4$<$7SC!=8`# +M$!>47?0\4`49J."'*G!`5/_J94(!@@^%BWJ#-<[1PG:\,![Q>$<^@@'%>-!0 +M@>C`QAF^!!B`$(>XK!.LH(@!)"`)@'"*;IP#'2U\(3SBD0YYS*** +M5KSB.LX!CD_D`'IPT4(*(L`DC9T1C2?\612@X48FQI&&\CC&._`HQQJB@X_4 +M4`(@*:"#)$CO5SY#9"+!E09&@N.-"EQ@`]NA#F2L@Y*53.4YSM$-:H3A2V(0 +MP=8:`+5#HA%A>8"&-;HQ#E0^$A[M(`@P0;_X$`*?&(* +MU,PH2E(F`QK:(*8QT^=$>&Q#&N!H9B6?&8H99O@$F47 +M@*'_#&J$LYCC5!\WEH$-==*0G;.4IC:@`0Q-L@4'1T!57/`)+E\*T`FX``8T +M_-F-4XYS'=R0QC#*6<55KB^5T$QH-Q::C%/H('9DT(##D*(H`EJ4!3$812TT +MRE%P`!2.ZPB'-&SAQ'C0XQ[XN$<]Z(%,E&)QEN-0*#24`8Q+A(`[.V#"821* +M@5`1\`0"Y`,M=KI1:X33HV\4QS-F<0XGU@,?_A#(/N[!5*=>4J7:>"=5:3&& +MGIS!`YF#BZV^BKH=T&*L_!1F.#LZRW,TPQ8L;`<]\%&0?-0#'JE$QUWYV(V5 +MZA48M/`$"(QPA!1)RE:+LBD@8G'8Q%+#K)T]93.:P0LX_\*C'OTP"#[B`LEQB'>\U@BN<`^KBDY<05KGTM)R$=:"3IC"%*I@K72_6=9BA((: +ML06'=N-QCWV8>!]UY:,TQTM>:$`C&<`X;RQ,P0E$\"I)6AK!A,'E!$YXXL+0 +MC2XP.$R,69Q"&TB.+1/+*<$]KGBE>4VPBV$,6O326!(^N+%R7C.J'<>!$YRP +M<'V%#(Q>%.,0U'BM61?KT_!"=?_%2(ZR@I5!Y<.N0A6>X,0CFH`V/6%G7,L] +M!)C#C&'61G<8R`"$,EQ<5FNL.JZ:?+MX*! +M)"9Q"3#_.,.TR$4Q*)$+?BZ:T1M-% +M0%T$21A;$J8N]"YF<0E<2U<9K[:UM%T,;6?7XM*9SC,G)%$(.,PO0\/NLA*. +M;>QDLZ(8=3CL3IT-#&%`&]KN?KNNM\2M?5A=WYG7^O;U'[%YG_J`1]5V%HB-?"%\#PA#T97:%W&'QN\!Y7M0>++]3M^5/_?RE=?YA3W!]8P_0O!T<`,8JN`"(Y7= +M(OQ(O>I7S_K6N_[UL(?]1A32>GW$_O:XSST_9D][U=M>]ZLO"/!?S_O>\^/W +MPU_(\%5?_(',0Q\$\?WR$<(/=\SC^0))OD4&P8#N>]_[CG!'__2/O_S=%\08 +M/!```-8?`!08XQ_`OT@;UD__^FO"_`,A__3S3X7Z&Z#^2X!\L8<1Y@`*!FB` +MFA```,``YF`0YB=[^<=\V<<#Z\+!^CE`0 +MW@`*FG"!U]=Z_U"`FJ`)&)AZ`U$&ZX<'$9AZMZ"`K]!ZT_""H`!]PG=]/9B" +M0.@0\Z"`/#`/`^$.*%!_`!`$WA"#J><.%%A_/.`-0.@."K@%XZ=Z\V!]7F@, +M'%!_`4`%^*'MQ"#YI"'=^B' +MM[![(R@`^'=[\_`*"FB'>+A^%0!]_/]P@G7(`#S``.O'`&V($-.P?EOP@)3( +M`=XP$--0`0#``<9@>Z*(`MZ@>MXPAA6`@11(!=D'?_`7@O"G#].@?CQ@#JIW +MBT_8@L:`B^(G$(Z`BPPABA7PB0*A"788C,ZG?GC@#LIHB06A#_\W"/PPAGC@ +MA>ZPC=RXC>;PA?V'`LRW>[>P?FG8?QP0@OPP@@#0@`GA""1($$$``#SP@O;X +M@C0``#3@#?,8!/=HC_G(`_S0A&6@>J_@A/77!N9`B5OPCYK@")0X"+=`B53@ +M"/\(D0`P"`FA#[BHA`,QA@A9?Q5@#B`9DA7(#V](`[]7CB;Y"MY@DO6W!0<) +MD^M7!@G1?W'_6!!-6`$HL`14\)-`N01M,)`+&`1+<)1(>91#J8P!D(K'UXWN +MD(;J]X7KQP-'"914<)6O,)-!@)59J968J(FTN`7T.`W,*'QDF8NL%WT<28\" +MZ'OSZ('\0(GW1Q#Z,`_F-PW_IPG19PZ7:!!CR`%G*1`L"0K?.!#ZD)6ZF(D` +M``K/9WZ)N00QJ(Q/B)>KIP]O*`"ZN(X+Z)'_<)>OX),"\8;2^)E1J0FB>1"@ +M```!L(=0.0W3,(\!H)"I9PRG.!"RV08Q.)$`((ZJ-W]P6`:O8`Z:4`9Y&(BI +M1XT+N(/\8`YXH(`V^)G_QP"EJ`_>T`;0B1#`&9*TF8^L:0"1Z)GZ_["&`0"> +ME6B9JN<(B>B$##`-P=>'ZR<`!I"(2T`0=!B?\[E^L(@0$/E]WF<`=?D/@Y"' +M<+@%E\@/;5"'!?J6J3$H-">LSB`.$VQB%([B'7QB" +M:!A]*)I_M_""@3@/AQF%U@<*GYB!MNB#[O^H$.7H"%@H$-9'$*"@H`!0`;>@ +MA.Q(?V19?PP0`"[*`Z"ZA`!@D_P`"@2ZG!#Z#WJ)`I28C1JXAO07!']IEP'@ +MB<^G#VU(@T'0HO-P"Z+H"'YYA*TX#R?8?V4`"N6HD0*A#PH("@,!CZDX"*.8 +MJ8?XJ=VWI?^WCW>9@P!Z?>XP@J6)$(@8`$N@@AY9CEP8?:)Z"]`G`.DX$,HH +MA_]0`31@?DP9!-E'`PRPI?K(IZ+8K[\8`-"G#^[0?2[Z#R.XGPGAH)0H`&5@ +M#@U(`TUI$,[*`^('K_AWD/3ZG,'(`Q6P!83X#UC:!OK@H7]YD&+ZBS0@J=1* +MKP3A#A40`&=Z$*__0(F]J@^C.)@"$00&\'X&4`$;"P"O(*D92;(!@`=<^G[* +M:`[N(`#Z:A#F$*;_<`L"4)];ZJ%."Y7A"@!ENJ)`.`]CZ`B9&`0\^P_S5Z5! +M6X0'6;0"P0$""8_B%P";"+(D"P!MA#*.)SF5XXVR0$,F+#_((D"80`,(+G.A[WQ^N\CCA_#-M]^%>.D_L/ +M\&B^`T&!4BH0%%@&OS<-3=A^4PH`Z7N^'2A^U:<)#&``LAM]\\BI!3%_Y9F? +MFS@0;4H0Y0BS=UO`#>RB*/F=_R<`J&J^+VF3!(&(=FB>*'#!!G&"FL"X2]@& +M61FQ!?$*7SL//_C#;:@/_SN'6_"3@P!]M_"EB.J`@_"36T#"5EK%5GS%6)S% +36KS%7-S%7OS%8!S&8IP1`<$`.P`` +` +end + + + + +-----------------------------7d033e3733c-- + + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/uu-zeegee-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/uu-zeegee-expected.json new file mode 100644 index 00000000000..1e8bb1da04d --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/uu-zeegee-expected.json @@ -0,0 +1,30 @@ +{ + "exception": null, + "result": { + "id": null, + "boundary": null, + "alternativeBoundary": null, + "sender": { + "name": "", + "mailAddress": "me", + "valid": false + }, + "toRecipients": [], + "ccRecipients": [], + "bccRecipients": [], + "replyTo": [], + "inReplyTo": null, + "references": [], + "autoSubmitted": null, + "sentDate": null, + "subject": "uudecoding", + "plainBodyText": "I've uuencoded the ZeeGee logo and another GIF file below.\r\n\r\nbegin 644 up.gif\r\nM1TE&.#=A$P`3`*$``/___P```(\"`@,#`P\"P`````$P`3```\"1X2/F<'MSTQ0\r\nM%(@)YMB\\;W%)@$<.(*:5W2F2@<=F8]>LH4P[7)P.T&NZI7Z,(&JF^@B121Y3\r\n4Y4SNEJ\"J]8JZ:JTH(K$\"/A0``#L`\r\n`\r\nend\r\n\r\nbegin 644 zeegee.gif\r\nM1TE&.#=A6P!P`/<```````@(\"!`0$#D`(3D`*4(`*1@8&$H`*4H`,5(`,5(`\r\nM.5H`,2$A(5H`.5((,6,`.6,`0EH(.6L`0F,(.6,(0BDI*5H0.6L(0FL(2F,0\r\nM.6,00G,(2C$Q,4(I,6L02GL(4G,04G,02FL80H0(4H0(6E(I.7,80HP(6CDY\r\nM.6LA0D(Y.90(6I0(8XP06G,A2H084H086I008U(Y0GLA4D)\"0G,I2H0A2G,I\r\nM4H0A4H0A6H0I4I0A8TI*2FLY4GLQ6G,Y4I0I8U)24F-*4EI22I0Q8Y0Q:YPQ\r\nM:Y0Y6I0Y8X1\"6I0Y:UI:6HQ\"6H1*8Z4YV-C8WM:\r\nM8Z5\"X1C:Y1:ZU2WM[>Y1S[UC>XQ[\r\nM>YQS>[5K>[UCE)Q[>X2$A+UKA)Q[E*5[>[UKC*5[A*5[E)R$>[USC*5[G-YC\r\nMC(R,C*U[G*6$A,YKE+U[C*V$A+5[G,YSC*V$C+5[I=YKE)24E*V,C+6$I;V$\r\nMI;6,C+6,E-9[G+V,C*V4C+V,E-Y[E-Y[G)R>$I;V^,I:VM\r\nMK<:EI>4O>^4M=:EI=:EK>>>EK?>>EM=ZMK>>EO=ZMM>^EK?^^EO?^MK>>MM>^ESO>EO?^E\r\nMO>^MO?^EQN>UM?^ESO>MQL;&QN^UM=Z]M>^UO?^MO?>UO>>]O>^]O?^USO^U\r\nMQO>]O<[.SN?&O?>]QO^]QO^]SO?&QO^]WO?&SM;6UO_&QO_&SO_&WO_&Y__.\r\nMSN_6SO_.UM[>WO_.WO_6UO_6WO_6Y^?GY__>WO_>Y__GWO_GY__G[__G]^_O\r\nM[__O[_?W]__W]__W_____RP`````6P!P``<(_P#_\"1Q(L*#!@P@3*ES(L*'#\r\nMAQ`C2IQ(L:+%BQ@S:I08J:-'2\"!#BAP9DI')1(12JES)LF6@ES!C^IE)L^9,\r\nM/3ASZE2XH:?/GAB\"8KA`M\"B%HT@A*(7PH('3IU`;*)BJ(('5JPBR:M5ZH&N!\r\nMKV#!$AA+8(#9LSQ__A0ZM\"A1I$F7-HT:E6K5JUBW379\r\nMU[\"/(]_0V3-SN:,=/_^&[/MW];YCL6>/C;S[7]U66H(+\\-6C;;?&%)R%>U)U7\r\nM@$(C*(CA=H9YQR&$$4;76XA]D5BBB?NAJ-Q[<.76HHL@9I5`!M.9=Z\",,]*H\r\nM77LW(I9CA]#Q:)4-9#22R2:*\".+\"9'P1.4*1-&;8X'?./4?78PM$T8@K=4A1\r\nM@P4U,*$($WN-F!`)6]9I9'9>!E7;BBQZJ(`5I6RBA@B1I7\"(!7LI1`*==7+9\r\nMI8UZJKADGXV]4`HQ9U@@759()+'5`8HNVJBC)N89J9*3BMG``W8\\H\\@,]('_\r\nMB$,3>H6Z**-VWKG@9AJ\"&:!3+\\CBRA/B.6F5!F_4FM`)MXK:J*Z[$M8K@`%:\r\nM$4TC)M05JU6'*(O0\"$>01)`(*@1NNN,_J\r\nM:NJ>H+VP##'$RL=;!F/D5:^]]SJ;ZYW[2GH4$>2$,!DDTBD`)G&&W,\\*K0-NV4'.89XH-C)8RKP!*%XL8QPPAV3\r\nM6RZ#21HB#AO4JDJ:&!D0F(#//[O\\,L.0ZHG*-EB@FZK2#5R`!7T*L?!SU%)[\r\nM_#'1&,BRC1(X/DAQ`Q'H@(48+@Q85=ABC]WRK5-3_[U=*MP4P>^DO]90BSWW\r\nMW*.+!G;CS<(*>MM;]I8P(W=U$>?ZVI0&M'S#3C_WL+/*`DY-Y3@+>4?.<=\"5\r\nMJW6U$?V%[.\"#7[3B2S7L?).-+STCL$@#;Q81_K@P<`&RB,=QV!@.Q3X\"S2`\r\nM8%IP:8(02C`$(0AA\"\"5(P/\\0(L`2IDYUXH*!';\"A0/4UL('O4$<$7SC!=8`#\r\nM$!>47?0\\4`49J.\"'*G!`5/_J94(!@@^%BWJ#-<[1PG:\\,![Q>$<^@@'%>-!0\r\nM@>C`QAF^!!B`$(>XK!.LH(@!)\"`)@'\"*;IP#'2U\\(3SBD0YYS***\r\nM5KSB.LX!CD_D`'IPT4(*(L`DC9T1C2?\\612@X48FQI&&\\CC&._`HQQJB@X_4\r\nM4`(@*:\"#)$CO5SY#9\"+!E09&@N.-\"EQ@`]NA#F2L@Y*53.4YSM$-:H3A2V(0\r\nMP=8:`+5#HA%A>8\"&-;HQ#E0^$A[M(`@P0;_X$`*?&(*\r\nMU,PH2E(F`QK:(*8QT^=$>&Q#&N!H9B6?&8H99O@$F47\r\nM@*'_#&J$LYCC5!\\WEH$-==*0G;.4IC:@`0Q-L@4'1T!57/`)+E\\*T`FX``8T\r\nM_-F-4XYS'=R0QC#*6<55KB^5T$QH-Q::C%/H('9DT(##D*(H`EJ4!3$812TT\r\nMRE%P`!2.ZPB'-&SAQ'C0XQ[XN$<]Z(%,E&)QEN-0*#24`8Q+A(`[.V#\"821*\r\nM@5`1\\`0\"Y`,M=KI1:X33HV\\4QS-F<0XGU@,?_A#(/N[!5*=>4J7:>\"=5:3&&\r\nMGIS!`YF#BZV^BKH=T&*L_!1F.#LZRW,TPQ8L;`<]\\%&0?-0#'JE$QUWYV(V5\r\nMZA48M/`$\"(QPA!1)RE:+LBD@8G'8Q%+#K)T]93.:P0LX_\\*C'OTP\"#[B`LEQB'>\\U@BN<`^KBDY<05KGTM)R$=:\"3IC\"%*I@K72_6=9BA((:\r\nML06'=N-QCWV8>!]UY:,TQTM>:$`C&<`X;RQ,P0E$\\\"I)6AK!A,'E!$YXXL+0\r\nMC2XP.$R,69Q\"&TB.+1/+*<$]KGBE>4VPBV$,6O326!(^N+%R7C.J'<>!$YRP\r\nM<'V%#(Q>%.,0U'BM61?KT_!\"=?_%2(ZR@I5!Y<.N0A6>X,0CFH`V/6%G7,L]\r\nM!)C#C&'61G<8R`\"$,EQ<5FNL.JZ:?+MX*!\r\nM)\"9Q\"3#_.,.TR$4Q*)$+?BZ:T1M-%\r\nM0%T$21A;$J8N]\"YF<0E<2U<9K[:UM%T,;6?7XM*9SC,G)%$(.,PO0\\/NLA*.\r\nM;>QDLZ(8=3CL3IT-#&%`&]KN?KNNM\\2M?5A=WYG7^O;U'[%YG_J`1]5V%HB-?\"%\\#PA#T97:%W&'QN\\!Y7M0>++]3M^5/_?RE=?YA3W!]8P_0O!T<`,8JN`\"(Y7=\r\nM(OQ(O>I7S_K6N_[UL(?]1A32>GW$_O:XSST_9D][U=M>]ZLO\"/!?S_O>\\^/W\r\nMPU_(\\%5?_(',0Q\\$\\?WR$<(/=\\SC^0))OD4&P8#N>]_[CG!'__2/O_S=%\\08\r\nM/!```-8?`!08XQ_`OT@;UD__^FO\"_`,A__3S3X7Z&Z#^2X!\\L8<1Y@`*!FB`\r\nMFA```,``YF`0YB=[^<=\\V<<#Z\\+!^CE`0\r\nMW@`*FG\"!U]=Z_U\"`FJ`)&)AZ`U$&ZX<'$9AZMZ\"`K]!ZT_\"\"H`!]PG=]/9B\"\r\nM0.@0\\Z\"`/#`/`^$.*%!_`!`$WA\"#J><.%%A_/.`-0.@.\"K@%XZ=Z\\V!]7F@,\r\nM'%!_`4`%^*'MQ\"#YI\"'=^B'\r\nMM[![(R@`^'=[\\_`*\"FB'>+A^%0!]_/]P@G7(`#S``.O'`&V($-.P?EOP@)3(\r\nM`=XP$--0`0#``<9@>Z*(`MZ@>MXPAA6`@11(!=D'?_`7@O\"G#].@?CQ@#JIW\r\nMBT_8@L:`B^(G$(Z`BPPABA7PB0*A\"788C,ZG?GC@#LIHB06A#_\\W\"/PPAGC@\r\nMA>ZPC=RXC>;PA?V'`LRW>[>P?FG8?QP0@OPP@@#0@`GA\"\"1($$$``#SP@O;X\r\nM@C0``#3@#?,8!/=HC_G(`_S0A&6@>J_@A/77!N9`B5OPCYK@\")0X\"+=`B53@\r\nM\"/\\(D0`P\"`FA#[BHA`,QA@A9?Q5@#B`9DA7(#V](`[]7CB;Y\"MY@DO6W!0<)\r\nMD^M7!@G1?W'_6!!-6`$HL`14\\)-`N01M,)`+&`1+<)1(>91#J8P!D(K'UXWN\r\nMD(;J]X7KQP-'\"914<)6O,)-!@)59J968J(FTN`7T.`W,*'QDF8NL%WT<28\\\"\r\nMZ'OSZ('\\0(GW1Q#Z,`_F-PW_IPG19PZ7:!!CR`%G*1`L\"0K?.!#ZD)6ZF(D`\r\nM``K/9WZ)N00QJ(Q/B)>KIP]O*`\"ZN(X+Z)'_<)>OX),\"\\8;2^)E1J0FB>1\"@\r\nM```!L(=0.0W3,(\\!H)\"I9PRG.!\"RV08Q.)$`((ZJ-W]P6`:O8`Z:4`9Y&(BI\r\nM1XT+N(/\\8`YXH(`V^)G_QP\"EJ`_>T`;0B1#`&9*TF8^L:0\"1Z)GZ_[\"&`0\">\r\nME6B9JN<(B>B$##`-P=>'ZR<`!I\"(2T`0=!B?\\[E^L(@0$/E]WF<`=?D/@Y\"'\r\nM<+@%E\\@/;5\"'!?J6J3$H-\">LSB`.$VQB%([B'7QB\"\r\nM:!A]*)I_M_\"\"@3@/AQF%U@<*GYB!MNB#[O^H$.7H\"%@H$-9'$*\"@H`!0`;>@\r\nMA.Q(?V19?PP0`\"[*`Z\"ZA`!@D_P`\"@2ZG!#Z#WJ)`I28C1JXAO07!']IEP'@\r\nMB<^G#VU(@T'0HO-P\"Z+H\"'YYA*TX#R?8?V4`\"N6HD0*A#PH(\"@,!CZDX\"*.8\r\nMJ8?XJ=VWI?^WCW>9@P!Z?>XP@J6)$(@8`$N@@AY9CEP8?:)Z\"]`G`.DX$,HH\r\nMA_]0`31@?DP9!-E'`PRPI?K(IZ+8K[\\8`-\"G#^[0?2[Z#R.XGPGAH)0H`&5@\r\nM#@U(`TUI$,[*`^('K_AWD/3ZG,'(`Q6P!83X#UC:!OK@H7]YD&+ZBS0@J=1*\r\nMKP3A#A40`&=Z$*__0(F]J@^C.)@\"$00&\\'X&4`$;\"P\"O(*D92;(!@`=<^G[*\r\nM:`[N(`#Z:A#F$*;_<`L\"4)];ZJ%.\"Y7A\"@!ENJ)`.`]CZ`B9&`0\\^P_S5Z5!\r\nM6X0'6;0\"P0$\"\"8_B%P\";\"+(D\"P!MA#*.)SF5XXVR0$,F+#_((D\"80`,(+G.A[WQ^N\\CCA_#-M]^%>.D_L/\r\nM\\&B^`T&!4BH0%%@&OS<-3=A^4PH`Z7N^'2A^U:<)#&``LAM]\\\\BI!3%_Y9F?\r\nMFS@0;4H0Y0BS=UO`#>RB*/F=_R<`J&J^+VF3!(&(=FB>*'#!!G&\"FL\"X2]@&\r\nM61FQ!?$*7SL//_C#;:@/_SN'6_\"3@P!]M_\"EB.J`@_\"36T#\"5EK%5GS%6)S%\r\n36KS%7-S%7OS%8!S&8IP1`<$`.P``\r\n`\r\nend\r\n\r\n", + "htmlBodyText": null, + "attachedMessages": [], + "attachedFiles": [], + "mailHeaders": "From: me\nTo: you\nSubject: uudecoding", + "spfResult": "None", + "listUnsubscribe": false, + "mailAuthenticationResult": null + } +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/uu-zeegee.msg b/packages/node-mimimi/test/mimetools-testmsgs/uu-zeegee.msg new file mode 100644 index 00000000000..6d67a1f2761 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/uu-zeegee.msg @@ -0,0 +1,125 @@ +From: me +To: you +Subject: uudecoding + +I've uuencoded the ZeeGee logo and another GIF file below. + +begin 644 up.gif +M1TE&.#=A$P`3`*$``/___P```("`@,#`P"P`````$P`3```"1X2/F<'MSTQ0 +M%(@)YMB\;W%)@$<.(*:5W2F2@<=F8]>LH4P[7)P.T&NZI7Z,(&JF^@B121Y3 +4Y4SNEJ"J]8JZ:JTH(K$"/A0``#L` +` +end + +begin 644 zeegee.gif +M1TE&.#=A6P!P`/<```````@("!`0$#D`(3D`*4(`*1@8&$H`*4H`,5(`,5(` +M.5H`,2$A(5H`.5((,6,`.6,`0EH(.6L`0F,(.6,(0BDI*5H0.6L(0FL(2F,0 +M.6,00G,(2C$Q,4(I,6L02GL(4G,04G,02FL80H0(4H0(6E(I.7,80HP(6CDY +M.6LA0D(Y.90(6I0(8XP06G,A2H084H086I008U(Y0GLA4D)"0G,I2H0A2G,I +M4H0A4H0A6H0I4I0A8TI*2FLY4GLQ6G,Y4I0I8U)24F-*4EI22I0Q8Y0Q:YPQ +M:Y0Y6I0Y8X1"6I0Y:UI:6HQ"6H1*8Z4YV-C8WM: +M8Z5"X1C:Y1:ZU2WM[>Y1S[UC>XQ[ +M>YQS>[5K>[UCE)Q[>X2$A+UKA)Q[E*5[>[UKC*5[A*5[E)R$>[USC*5[G-YC +MC(R,C*U[G*6$A,YKE+U[C*V$A+5[G,YSC*V$C+5[I=YKE)24E*V,C+6$I;V$ +MI;6,C+6,E-9[G+V,C*V4C+V,E-Y[E-Y[G)R>$I;V^,I:VM +MK<:EI>4O>^4M=:EI=:EK>>>EK?>>EM=ZMK>>EO=ZMM>^EK?^^EO?^MK>>MM>^ESO>EO?^E +MO>^MO?^EQN>UM?^ESO>MQL;&QN^UM=Z]M>^UO?^MO?>UO>>]O>^]O?^USO^U +MQO>]O<[.SN?&O?>]QO^]QO^]SO?&QO^]WO?&SM;6UO_&QO_&SO_&WO_&Y__. +MSN_6SO_.UM[>WO_.WO_6UO_6WO_6Y^?GY__>WO_>Y__GWO_GY__G[__G]^_O +M[__O[_?W]__W]__W_____RP`````6P!P``<(_P#_"1Q(L*#!@P@3*ES(L*'# +MAQ`C2IQ(L:+%BQ@S:I08J:-'2"!#BAP9DI')1(12JES)LF6@ES!C^IE)L^9, +M/3ASZE2XH:?/GAB"8KA`M"B%HT@A*(7PH('3IU`;*)BJ(('5JPBR:M5ZH&N! +MKV#!$AA+8(#9LSQ__A0ZM"A1I$F7-HT:E6K5JUBW379 +MU["/(]_0V3-SN:,=/_^&[/MW];YCL6>/C;S[7]U66H(+\-6C;;?&%)R%>U)U7 +M@$(C*(CA=H9YQR&$$4;76XA]D5BBB?NAJ-Q[<.76HHL@9I5`!M.9=Z",,]*H +M77LW(I9CA]#Q:)4-9#22R2:*".+"9'P1.4*1-&;8X'?./4?78PM$T8@K=4A1 +M@P4U,*$($WN-F!`)6]9I9'9>!E7;BBQZJ(`5I6RBA@B1I7"(!7LI1`*==7+9 +MI8UZJKADGXV]4`HQ9U@@759()+'5`8HNVJBC)N89J9*3BMG``W8\H\@,]('_ +MB$,3>H6Z**-VWKG@9AJ"&:!3+\CBRA/B.6F5!F_4FM`)MXK:J*Z[$M8K@`%: +M$4TC)M05JU6'*(O0"$>01)`(*@1NNN,_J +M:NJ>H+VP##'$RL=;!F/D5:^]]SJ;ZYW[2GH4$>2$,!DDTBD`)G&&W,\*K0-NV4'.89XH-C)8RKP!*%XL8QPPAV3 +M6RZ#21HB#AO4JDJ:&!D0F(#//[O\,L.0ZHG*-EB@FZK2#5R`!7T*L?!SU%)[ +M_#'1&,BRC1(X/DAQ`Q'H@(48+@Q85=ABC]WRK5-3_[U=*MP4P>^DO]90BSWW +MW*.+!G;CS<(*>MM;]I8P(W=U$>?ZVI0&M'S#3C_WL+/*`DY-Y3@+>4?.<="5 +MJW6U$?V%[."#7[3B2S7L?).-+STCL$@#;Q81_K@P<`&RB,=QV!@.Q3X"S2` +M8%IP:8(02C`$(0AA""5(P/\0(L`2IDYUXH*!';"A0/4UL('O4$<$7SC!=8`# +M$!>47?0\4`49J."'*G!`5/_J94(!@@^%BWJ#-<[1PG:\,![Q>$<^@@'%>-!0 +M@>C`QAF^!!B`$(>XK!.LH(@!)"`)@'"*;IP#'2U\(3SBD0YYS*** +M5KSB.LX!CD_D`'IPT4(*(L`DC9T1C2?\612@X48FQI&&\CC&._`HQQJB@X_4 +M4`(@*:"#)$CO5SY#9"+!E09&@N.-"EQ@`]NA#F2L@Y*53.4YSM$-:H3A2V(0 +MP=8:`+5#HA%A>8"&-;HQ#E0^$A[M(`@P0;_X$`*?&(* +MU,PH2E(F`QK:(*8QT^=$>&Q#&N!H9B6?&8H99O@$F47 +M@*'_#&J$LYCC5!\WEH$-==*0G;.4IC:@`0Q-L@4'1T!57/`)+E\*T`FX``8T +M_-F-4XYS'=R0QC#*6<55KB^5T$QH-Q::C%/H('9DT(##D*(H`EJ4!3$812TT +MRE%P`!2.ZPB'-&SAQ'C0XQ[XN$<]Z(%,E&)QEN-0*#24`8Q+A(`[.V#"821* +M@5`1\`0"Y`,M=KI1:X33HV\4QS-F<0XGU@,?_A#(/N[!5*=>4J7:>"=5:3&& +MGIS!`YF#BZV^BKH=T&*L_!1F.#LZRW,TPQ8L;`<]\%&0?-0#'JE$QUWYV(V5 +MZA48M/`$"(QPA!1)RE:+LBD@8G'8Q%+#K)T]93.:P0LX_\*C'OTP"#[B`LEQB'>\U@BN<`^KBDY<05KGTM)R$=:"3IC"%*I@K72_6=9BA((: +ML06'=N-QCWV8>!]UY:,TQTM>:$`C&<`X;RQ,P0E$\"I)6AK!A,'E!$YXXL+0 +MC2XP.$R,69Q"&TB.+1/+*<$]KGBE>4VPBV$,6O326!(^N+%R7C.J'<>!$YRP +M<'V%#(Q>%.,0U'BM61?KT_!"=?_%2(ZR@I5!Y<.N0A6>X,0CFH`V/6%G7,L] +M!)C#C&'61G<8R`"$,EQ<5FNL.JZ:?+MX*! +M)"9Q"3#_.,.TR$4Q*)$+?BZ:T1M-% +M0%T$21A;$J8N]"YF<0E<2U<9K[:UM%T,;6?7XM*9SC,G)%$(.,PO0\/NLA*. +M;>QDLZ(8=3CL3IT-#&%`&]KN?KNNM\2M?5A=WYG7^O;U'[%YG_J`1]5V%HB-?"%\#PA#T97:%W&'QN\!Y7M0>++]3M^5/_?RE=?YA3W!]8P_0O!T<`,8JN`"(Y7= +M(OQ(O>I7S_K6N_[UL(?]1A32>GW$_O:XSST_9D][U=M>]ZLO"/!?S_O>\^/W +MPU_(\%5?_(',0Q\$\?WR$<(/=\SC^0))OD4&P8#N>]_[CG!'__2/O_S=%\08 +M/!```-8?`!08XQ_`OT@;UD__^FO"_`,A__3S3X7Z&Z#^2X!\L8<1Y@`*!FB` +MFA```,``YF`0YB=[^<=\V<<#Z\+!^CE`0 +MW@`*FG"!U]=Z_U"`FJ`)&)AZ`U$&ZX<'$9AZMZ"`K]!ZT_""H`!]PG=]/9B" +M0.@0\Z"`/#`/`^$.*%!_`!`$WA"#J><.%%A_/.`-0.@."K@%XZ=Z\V!]7F@, +M'%!_`4`%^*'MQ"#YI"'=^B' +MM[![(R@`^'=[\_`*"FB'>+A^%0!]_/]P@G7(`#S``.O'`&V($-.P?EOP@)3( +M`=XP$--0`0#``<9@>Z*(`MZ@>MXPAA6`@11(!=D'?_`7@O"G#].@?CQ@#JIW +MBT_8@L:`B^(G$(Z`BPPABA7PB0*A"788C,ZG?GC@#LIHB06A#_\W"/PPAGC@ +MA>ZPC=RXC>;PA?V'`LRW>[>P?FG8?QP0@OPP@@#0@`GA""1($$$``#SP@O;X +M@C0``#3@#?,8!/=HC_G(`_S0A&6@>J_@A/77!N9`B5OPCYK@")0X"+=`B53@ +M"/\(D0`P"`FA#[BHA`,QA@A9?Q5@#B`9DA7(#V](`[]7CB;Y"MY@DO6W!0<) +MD^M7!@G1?W'_6!!-6`$HL`14\)-`N01M,)`+&`1+<)1(>91#J8P!D(K'UXWN +MD(;J]X7KQP-'"914<)6O,)-!@)59J968J(FTN`7T.`W,*'QDF8NL%WT<28\" +MZ'OSZ('\0(GW1Q#Z,`_F-PW_IPG19PZ7:!!CR`%G*1`L"0K?.!#ZD)6ZF(D` +M``K/9WZ)N00QJ(Q/B)>KIP]O*`"ZN(X+Z)'_<)>OX),"\8;2^)E1J0FB>1"@ +M```!L(=0.0W3,(\!H)"I9PRG.!"RV08Q.)$`((ZJ-W]P6`:O8`Z:4`9Y&(BI +M1XT+N(/\8`YXH(`V^)G_QP"EJ`_>T`;0B1#`&9*TF8^L:0"1Z)GZ_["&`0"> +ME6B9JN<(B>B$##`-P=>'ZR<`!I"(2T`0=!B?\[E^L(@0$/E]WF<`=?D/@Y"' +M<+@%E\@/;5"'!?J6J3$H-">LSB`.$VQB%([B'7QB" +M:!A]*)I_M_""@3@/AQF%U@<*GYB!MNB#[O^H$.7H"%@H$-9'$*"@H`!0`;>@ +MA.Q(?V19?PP0`"[*`Z"ZA`!@D_P`"@2ZG!#Z#WJ)`I28C1JXAO07!']IEP'@ +MB<^G#VU(@T'0HO-P"Z+H"'YYA*TX#R?8?V4`"N6HD0*A#PH("@,!CZDX"*.8 +MJ8?XJ=VWI?^WCW>9@P!Z?>XP@J6)$(@8`$N@@AY9CEP8?:)Z"]`G`.DX$,HH +MA_]0`31@?DP9!-E'`PRPI?K(IZ+8K[\8`-"G#^[0?2[Z#R.XGPGAH)0H`&5@ +M#@U(`TUI$,[*`^('K_AWD/3ZG,'(`Q6P!83X#UC:!OK@H7]YD&+ZBS0@J=1* +MKP3A#A40`&=Z$*__0(F]J@^C.)@"$00&\'X&4`$;"P"O(*D92;(!@`=<^G[* +M:`[N(`#Z:A#F$*;_<`L"4)];ZJ%."Y7A"@!ENJ)`.`]CZ`B9&`0\^P_S5Z5! +M6X0'6;0"P0$""8_B%P";"+(D"P!MA#*.)SF5XXVR0$,F+#_((D"80`,(+G.A[WQ^N\CCA_#-M]^%>.D_L/ +M\&B^`T&!4BH0%%@&OS<-3=A^4PH`Z7N^'2A^U:<)#&``LAM]\\BI!3%_Y9F? +MFS@0;4H0Y0BS=UO`#>RB*/F=_R<`J&J^+VF3!(&(=FB>*'#!!G&"FL"X2]@& +M61FQ!?$*7SL//_C#;:@/_SN'6_"3@P!]M_"EB.J`@_"36T#"5EK%5GS%6)S% +36KS%7-S%7OS%8!S&8IP1`<$`.P`` +` +end + diff --git a/packages/node-mimimi/test/mimetools-testmsgs/x-gzip64-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/x-gzip64-expected.json new file mode 100644 index 00000000000..71b5d47a156 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/x-gzip64-expected.json @@ -0,0 +1,7 @@ +{ + "exception": { + "clazz": "java.io.IOException", + "message": "Unknown encoding: x-gzip64" + }, + "result": null +} diff --git a/packages/node-mimimi/test/mimetools-testmsgs/x-gzip64.msg b/packages/node-mimimi/test/mimetools-testmsgs/x-gzip64.msg new file mode 100644 index 00000000000..3fa249eb9b6 --- /dev/null +++ b/packages/node-mimimi/test/mimetools-testmsgs/x-gzip64.msg @@ -0,0 +1,13 @@ +Content-Type: text/plain; name=".signature" +Content-Disposition: inline; filename=".signature" +Content-Transfer-Encoding: x-gzip64 +Mime-Version: 1.0 +X-Mailer: MIME-tools 3.204 (ME 3.204 ) +Subject: Testing! +Content-Length: 281 + +H4sIAJ+A5jIAA0VPTWvDMAy9+1e8nbpCsS877bRS1vayXdJDDwURbJEEEqez +VdKC6W+fnQ0iwdN7ktAHQEQAzV7irAv9DI8fvHLGD/bCobai7TisFUyuXxJW +lDB70aucxfHWtBxRnc4bfG+rrTmMztXBobrWlrHvu6YV7LwErVLZZP4n0IJA +K3J9N2aaJj3YqD2LeZYzFC75tlTaCtsg/SGRwmJZklnI1wOxa3wtt8Dgu2V2 +EdIyAudnBvaOHd7Qd57ji/oFWju6Pg4BAAA= diff --git a/packages/node-mimimi/test/sample.eml b/packages/node-mimimi/test/sample.eml new file mode 100644 index 00000000000..ec31b7d2802 --- /dev/null +++ b/packages/node-mimimi/test/sample.eml @@ -0,0 +1,39 @@ +From: Art Vandelay (Vandelay Industries) +To: "Colleagues": "James Smythe" ; Friends: + jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?= ; +Date: Sat, 20 Nov 2021 14:22:01 -0800 +Subject: Why not both importing AND exporting? =?utf-8?b?4pi6?= +Content-Type: multipart/mixed; boundary="festivus"; + +--festivus +Content-Type: text/html; charset="us-ascii" +Content-Transfer-Encoding: base64 + +PGh0bWw+PHA+SSB3YXMgdGhpbmtpbmcgYWJvdXQgcXVpdHRpbmcgdGhlICZsZHF1bztle +HBvcnRpbmcmcmRxdW87IHRvIGZvY3VzIGp1c3Qgb24gdGhlICZsZHF1bztpbXBvcnRpbm +cmcmRxdW87LDwvcD48cD5idXQgdGhlbiBJIHRob3VnaHQsIHdoeSBub3QgZG8gYm90aD8 +gJiN4MjYzQTs8L3A+PC9odG1sPg== +--festivus +Content-Type: message/rfc822 + +From: "Cosmo Kramer" +Subject: Exporting my book about coffee tables +Content-Type: multipart/mixed; boundary="giddyup"; + +--giddyup +Content-Type: text/plain; charset="utf-16" +Content-Transfer-Encoding: quoted-printable + +=FF=FE=0C!5=D8"=DD5=D8)=DD5=D8-=DD =005=D8*=DD5=D8"=DD =005=D8"= +=DD5=D85=DD5=D8-=DD5=D8,=DD5=D8/=DD5=D81=DD =005=D8*=DD5=D86=DD = +=005=D8=1F=DD5=D8,=DD5=D8,=DD5=D8(=DD =005=D8-=DD5=D8)=DD5=D8"= +=DD5=D8=1E=DD5=D80=DD5=D8"=DD!=00 +--giddyup +Content-Type: image/gif; name*1="about "; name*0="Book "; + name*2*=utf-8''%e2%98%95 tables.gif +Content-Transfer-Encoding: Base64 +Content-Disposition: attachment + +R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7 +--giddyup-- +--festivus-- \ No newline at end of file diff --git a/packages/node-mimimi/test/tsconfig.json b/packages/node-mimimi/test/tsconfig.json new file mode 100644 index 00000000000..e3f61415118 --- /dev/null +++ b/packages/node-mimimi/test/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig_common.json", + "include": ["../../../types/*.d.ts"], + "files": ["Suite.ts"], + "compilerOptions": { + "outDir": "../build", + "declaration": false, + "noImplicitAny": false + }, + "references": [ + { + "path": "../../tutanota-test-utils/tsconfig.json" + } + ] +} diff --git a/packages/node-mimimi/tsconfig.json b/packages/node-mimimi/tsconfig.json new file mode 100644 index 00000000000..18c2b4c7c64 --- /dev/null +++ b/packages/node-mimimi/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "composite": true + } +} diff --git a/src/calendar-app/calendarLocator.ts b/src/calendar-app/calendarLocator.ts index 6719e330247..36cfcce772d 100644 --- a/src/calendar-app/calendarLocator.ts +++ b/src/calendar-app/calendarLocator.ts @@ -112,6 +112,7 @@ import { DbError } from "../common/api/common/error/DbError.js" import { WorkerRandomizer } from "../common/api/worker/workerInterfaces.js" import { lang } from "../common/misc/LanguageViewModel.js" import type { CalendarContactPreviewViewModel } from "./calendar/gui/eventpopup/CalendarContactPreviewViewModel.js" +import { MailImportFacade } from "../common/native/common/generatedipc/MailImportFacade" assertMainOrNode() @@ -152,6 +153,7 @@ class CalendarLocator { searchTextFacade!: SearchTextInAppFacade desktopSettingsFacade!: SettingsFacade desktopSystemFacade!: DesktopSystemFacade + mailImportFacade!: MailImportFacade webMobileFacade!: WebMobileFacade systemPermissionHandler!: SystemPermissionHandler interWindowEventSender!: InterWindowEventFacadeSendDispatcher diff --git a/src/common/api/main/CommonLocator.ts b/src/common/api/main/CommonLocator.ts index d5a5060271f..9e5f2816f6a 100644 --- a/src/common/api/main/CommonLocator.ts +++ b/src/common/api/main/CommonLocator.ts @@ -69,6 +69,7 @@ import { WorkerRandomizer } from "../worker/workerInterfaces.js" import { CommonSearchModel } from "../../search/CommonSearchModel.js" import { DeviceConfig } from "../../misc/DeviceConfig.js" import type { CalendarContactPreviewViewModel } from "../../../calendar-app/calendar/gui/eventpopup/CalendarContactPreviewViewModel.js" +import { MailImportFacade } from "../../native/common/generatedipc/MailImportFacade.js" export interface CommonLocator { worker: WorkerClient @@ -85,6 +86,7 @@ export interface CommonLocator { infoMessageHandler: InfoMessageHandler desktopSettingsFacade: SettingsFacade desktopSystemFacade: DesktopSystemFacade + mailImportFacade: MailImportFacade themeController: ThemeController entityClient: EntityClient diff --git a/src/common/api/worker/rest/DefaultEntityRestCache.ts b/src/common/api/worker/rest/DefaultEntityRestCache.ts index 4000d6a8f39..6a5f9be97a3 100644 --- a/src/common/api/worker/rest/DefaultEntityRestCache.ts +++ b/src/common/api/worker/rest/DefaultEntityRestCache.ts @@ -893,7 +893,14 @@ export class DefaultEntityRestCache implements EntityRestCache { if (oldFolder != null && oldFolder.isMailSet) return const updatedFolder = await this.entityRestClient.load(MailFolderTypeRef, [update.instanceListId, update.instanceId]) if (!updatedFolder.isMailSet) return + let mailsInOffline = await this.storage.getIdsInRange(MailTypeRef, updatedFolder.mails) await this.storage.deleteWholeList(MailTypeRef, updatedFolder.mails) + + await this.storage.lockRangesDbAccess(updatedFolder.mails) + console.log("Loading mails: ", mailsInOffline) + await this.entityRestClient.loadMultiple(MailTypeRef, updatedFolder.mails, mailsInOffline) + await this.storage.unlockRangesDbAccess(updatedFolder.mails) + await this.storage.put(updatedFolder) } } diff --git a/src/common/desktop/DesktopMain.ts b/src/common/desktop/DesktopMain.ts index 916007217eb..6c823cd45a5 100644 --- a/src/common/desktop/DesktopMain.ts +++ b/src/common/desktop/DesktopMain.ts @@ -71,6 +71,9 @@ import { DelayedImpls, exposeLocalDelayed } from "../api/common/WorkerProxy.js" import { DefaultDateProvider } from "../calendar/date/CalendarUtils.js" import { AlarmScheduler } from "../calendar/date/AlarmScheduler.js" import { DesktopExternalCalendarFacade } from "./ipc/DesktopExternalCalendarFacade.js" +import { DesktopMailImportFacade } from "./mailimport/DesktopMailImportFacade.js" + +mp() /** * Should be injected during build time. @@ -86,7 +89,6 @@ setupAssetProtocol(electron) const TAG = "[DesktopMain]" -mp() type Components = { readonly wm: WindowManager readonly tfs: TempFs @@ -271,6 +273,7 @@ async function createComponents(): Promise { new DesktopExternalCalendarFacade(), new DesktopFileFacade(window, conf, dateProvider, desktopNet, electron, tfs, fs), new DesktopInterWindowEventFacade(window, wm), + new DesktopMailImportFacade(window), nativeCredentialsFacade, desktopCrypto, pushFacade, @@ -317,7 +320,7 @@ async function startupInstance(components: Components) { const { wm, sse, tfs } = components if (!(await desktopUtils.cleanupOldInstance())) return sse.connect().catch((e) => log.warn("unable to start sse client", e)) - // The second-instance event fires when we call app.requestSingleInstanceLock inside of DesktopUtils.makeSingleInstance + // The second-instance event fires when we call app.requestSingleInstanceLock inside DesktopUtils.makeSingleInstance app.on("second-instance", async (_ev, args) => desktopUtils.handleSecondInstance(wm, args)) app.on("open-url", (e, url) => { // MacOS mailto handling diff --git a/src/common/desktop/mailimport/DesktopMailImportFacade.ts b/src/common/desktop/mailimport/DesktopMailImportFacade.ts new file mode 100644 index 00000000000..3389e299512 --- /dev/null +++ b/src/common/desktop/mailimport/DesktopMailImportFacade.ts @@ -0,0 +1,63 @@ +import { ImporterApi, TutaCredentials, TutaCredentialType } from "../../../../packages/node-mimimi/dist/binding.cjs" +import { UnencryptedCredentials } from "../../native/common/generatedipc/UnencryptedCredentials.js" +import { CredentialType } from "../../misc/credentials/CredentialType.js" +import { ApplicationWindow } from "../ApplicationWindow.js" +import { MailImportFacade } from "../../native/common/generatedipc/MailImportFacade" + +export class DesktopMailImportFacade implements MailImportFacade { + constructor(private readonly win: ApplicationWindow) {} + + async setupImapImport(apiUrl: string, unencryptedTutaCredentials: UnencryptedCredentials): Promise { + try { + const tutaCredentials: TutaCredentials = { + accessToken: unencryptedTutaCredentials?.accessToken, + credentialType: + unencryptedTutaCredentials.credentialInfo.type == CredentialType.Internal ? TutaCredentialType.Internal : TutaCredentialType.External, + encryptedPassphraseKey: unencryptedTutaCredentials.encryptedPassphraseKey ? Array.from(unencryptedTutaCredentials.encryptedPassphraseKey) : [], + login: unencryptedTutaCredentials.credentialInfo.login, + userId: unencryptedTutaCredentials.credentialInfo.userId, + apiUrl: apiUrl, + clientVersion: env.versionNumber, + } + + //const importCredentials = ImportCredentials.setup(tutaCredentials, imapCredentials) + //const importerObj = await importCredentials.login() + + //console.log(importerObj) + } catch (e) { + console.log(e) + } + } + + startImapImport(): Promise { + throw new Error("Method not implemented.") + } + + stopImapImport(): Promise { + throw new Error("Method not implemented.") + } + + async importFromFiles( + apiUrl: string, + unencryptedTutaCredentials: UnencryptedCredentials, + targetOwnerGroup: string, + targetFolderId: IdTuple, + filePaths: Array, + ): Promise { + const tutaCredentials: TutaCredentials = { + accessToken: unencryptedTutaCredentials?.accessToken, + credentialType: + unencryptedTutaCredentials.credentialInfo.type == CredentialType.Internal ? TutaCredentialType.Internal : TutaCredentialType.External, + encryptedPassphraseKey: unencryptedTutaCredentials.encryptedPassphraseKey ? Array.from(unencryptedTutaCredentials.encryptedPassphraseKey) : [], + login: unencryptedTutaCredentials.credentialInfo.login, + userId: unencryptedTutaCredentials.credentialInfo.userId, + apiUrl: apiUrl, + clientVersion: env.versionNumber, + } + + const targetFolderIdTuple: [string, string] = [targetFolderId[0], targetFolderId[1]] + const fileImporter = await ImporterApi.createFileImporter(tutaCredentials, targetOwnerGroup, targetFolderIdTuple, filePaths) + const importStatus = await fileImporter.continueImport() + return importStatus ? "importSuccessful" : "importFailure" + } +} diff --git a/src/common/desktop/mailimport/MailImporter.ts b/src/common/desktop/mailimport/MailImporter.ts new file mode 100644 index 00000000000..81a14813c3e --- /dev/null +++ b/src/common/desktop/mailimport/MailImporter.ts @@ -0,0 +1,61 @@ +import { Dialog, DialogType } from "../../gui/base/Dialog.js" +import { lang } from "../../misc/LanguageViewModel.js" +import { DialogHeaderBar } from "../../gui/base/DialogHeaderBar.js" +import { ButtonType } from "../../gui/base/Button.js" +import m from "mithril" +import { DropDownSelector, DropDownSelectorAttrs } from "../../gui/base/DropDownSelector.js" +import { BootIcons } from "../../gui/base/icons/BootIcons.js" +import { MailFolder } from "../../api/entities/tutanota/TypeRefs" +import { IndentedFolder } from "../../api/common/mail/FolderSystem" +import { repeat } from "@tutao/tutanota-utils" + +/** + * Shows a dialog with the users folders that are able to import mails. + * @param indentedFolders List of user's folders + * @param okAction + */ +export function folderSelectionDialog(indentedFolders: IndentedFolder[], okAction: (dialog: Dialog, selectedMailFolder: MailFolder) => unknown) { + let selectedIndentedFolder = indentedFolders[0] + + const dialog = new Dialog(DialogType.EditSmall, { + view: () => [ + m(DialogHeaderBar, { + left: [ + { + type: ButtonType.Secondary, + label: "cancel_action", + click: () => { + dialog.close() + }, + }, + ], + middle: () => lang.getMaybeLazy("mailFolder_label"), + right: [ + { + type: ButtonType.Primary, + label: "pricing.select_action", + click: () => { + okAction(dialog, selectedIndentedFolder.folder) + }, + }, + ], + }), + + m(".dialog-max-height.plr-l.pt.pb.text-break.scroll", [ + m(".text-break.selectable", lang.get("mailImportSelection_label")), + m(DropDownSelector, { + label: "mailFolder_label", + items: indentedFolders.map((mailFolder) => { + return { + name: repeat(".", mailFolder.level) + mailFolder.folder.name, + value: mailFolder.folder, + } + }), + selectedValue: selectedIndentedFolder.folder, + selectionChangedHandler: (v) => (selectedIndentedFolder.folder = v), + icon: BootIcons.Expand, + } satisfies DropDownSelectorAttrs), + ]), + ], + }).show() +} diff --git a/src/common/desktop/preload.js b/src/common/desktop/preload.js index 53cb619a0fb..2ee61e475f7 100644 --- a/src/common/desktop/preload.js +++ b/src/common/desktop/preload.js @@ -7,7 +7,7 @@ * Note: we can't import any other desktop code here because it is in the web (render) process. */ -const { ipcRenderer, contextBridge } = require("electron") +const { ipcRenderer, contextBridge, webUtils } = require("electron") contextBridge.exposeInMainWorld("nativeApp", { invoke: (msg) => ipcRenderer.invoke("to-main", msg), @@ -16,4 +16,5 @@ contextBridge.exposeInMainWorld("nativeApp", { ipcRenderer.on("to-renderer", (ev, msg) => handler(msg)) return undefined }, + getPathForFile: (file) => webUtils.getPathForFile(file), }) diff --git a/src/common/desktop/sse/SseClient.ts b/src/common/desktop/sse/SseClient.ts index 93d84c51134..01f8de4089a 100644 --- a/src/common/desktop/sse/SseClient.ts +++ b/src/common/desktop/sse/SseClient.ts @@ -79,7 +79,7 @@ export class SseClient { constructor(private readonly net: DesktopNetworkClient, private readonly delay: SseDelay, private readonly scheduler: Scheduler) {} async connect(options: SseConnectOptions) { - log.debug("connect") + log.debug("connect", options) switch (this.state.state) { case ConnectionState.delayedReconnect: this.scheduler.unscheduleTimeout(this.state.timeout) diff --git a/src/common/file/FileController.ts b/src/common/file/FileController.ts index b69d9bb385f..581c883b5f4 100644 --- a/src/common/file/FileController.ts +++ b/src/common/file/FileController.ts @@ -31,6 +31,10 @@ export enum VCARD_MIME_TYPES { X_VCARD = "text/x-vcard", VCARD = "text/vcard", } +export enum MAIL_MIME_TYPES { + EML = "message/rfc822", + MBOX = "application/mbox", +} const enum DownloadPostProcessing { Open, @@ -144,7 +148,7 @@ export function handleDownloadErrors(e: Error, errorAction: (msg: Translation } } -export function readLocalFiles(fileList: FileList): Promise> { +export function fileListToArray(fileList: FileList): Array { // create an array of files form the FileList because we can not iterate the FileList directly let nativeFiles: File[] = [] @@ -152,6 +156,10 @@ export function readLocalFiles(fileList: FileList): Promise> { nativeFiles.push(fileList[i]) } + return nativeFiles +} + +export function readLocalFiles(nativeFiles: Array): Promise> { return promiseMap( nativeFiles, (nativeFile) => { diff --git a/src/common/gui/AttachmentBubble.ts b/src/common/gui/AttachmentBubble.ts index 9e4e3031156..c11420621e2 100644 --- a/src/common/gui/AttachmentBubble.ts +++ b/src/common/gui/AttachmentBubble.ts @@ -18,12 +18,13 @@ import { getSafeAreaInsetBottom } from "./HtmlUtils.js" import { hasError } from "../api/common/utils/ErrorUtils.js" import { BubbleButton, bubbleButtonHeight, bubbleButtonPadding } from "./base/buttons/BubbleButton.js" import { BootIcons } from "./base/icons/BootIcons.js" -import { CALENDAR_MIME_TYPE, VCARD_MIME_TYPES } from "../file/FileController.js" +import { CALENDAR_MIME_TYPE, MAIL_MIME_TYPES, VCARD_MIME_TYPES } from "../file/FileController.js" export enum AttachmentType { GENERIC, CONTACT, CALENDAR, + MAIL, } export type AttachmentBubbleAttrs = { @@ -93,6 +94,8 @@ export function getAttachmentType(mimeType: string) { return AttachmentType.CONTACT } else if (mimeType === CALENDAR_MIME_TYPE) { return AttachmentType.CALENDAR + } else if (Object.values(MAIL_MIME_TYPES).includes(mimeType)) { + return AttachmentType.MAIL } return AttachmentType.GENERIC diff --git a/src/common/gui/base/GuiUtils.ts b/src/common/gui/base/GuiUtils.ts index 823b0613f29..20a6634bb27 100644 --- a/src/common/gui/base/GuiUtils.ts +++ b/src/common/gui/base/GuiUtils.ts @@ -17,7 +17,23 @@ import { LoginController } from "../../api/main/LoginController.js" import { client } from "../../misc/ClientDetector.js" import type { Contact } from "../../api/entities/tutanota/TypeRefs.js" -export type dropHandler = (dragData: string) => void +export const enum DropType { + ExternalFile = "ExternalFile", + Mail = "Mail", +} + +export type MailDropData = { + dropType: DropType.Mail + mailId: string +} +export type FileDropData = { + dropType: DropType.ExternalFile + files: Array +} + +export type DropData = FileDropData | MailDropData + +export type DropHandler = (dropData: DropData) => void // not all browsers have the actual button as e.currentTarget, but all of them send it as a second argument (see https://github.com/tutao/tutanota/issues/1110) export type ClickHandler = (event: MouseEvent, dom: HTMLElement) => void diff --git a/src/common/gui/base/NavButton.ts b/src/common/gui/base/NavButton.ts index 13a6a904f0f..ec165b8ab7c 100644 --- a/src/common/gui/base/NavButton.ts +++ b/src/common/gui/base/NavButton.ts @@ -11,9 +11,10 @@ import type { TranslationKey } from "../../misc/LanguageViewModel" import { lang } from "../../misc/LanguageViewModel" import { Keys } from "../../api/common/TutanotaConstants" import { isKeyPressed } from "../../misc/KeyManager" -import type { dropHandler } from "./GuiUtils" -import { assertMainOrNode } from "../../api/common/Env" +import { DropData, DropHandler, DropType } from "./GuiUtils" +import { assertMainOrNode, isDesktop } from "../../api/common/Env" import { stateBgHover } from "../builtinThemes.js" +import { fileListToArray } from "../../file/FileController" assertMainOrNode() export type NavButtonAttrs = { @@ -23,7 +24,7 @@ export type NavButtonAttrs = { isSelectedPrefix?: string | boolean click?: (event: Event, dom: HTMLElement) => unknown colors?: NavButtonColor - dropHandler?: dropHandler + dropHandler?: DropHandler hideLabel?: boolean vertical?: boolean fontSize?: number @@ -163,9 +164,21 @@ export class NavButton implements Component { this._dropCounter = 0 this._draggedOver = false ev.preventDefault() - - if (ev.dataTransfer?.getData("text")) { - neverNull(a.dropHandler)(ev.dataTransfer.getData("text")) + ev.stopPropagation() + + if (ev.dataTransfer?.getData(DropType.Mail)) { + let dropData: DropData = { + dropType: DropType.Mail, + mailId: ev.dataTransfer.getData(DropType.Mail), + } + neverNull(a.dropHandler)(dropData) + } else if (isDesktop() && ev.dataTransfer?.files && ev.dataTransfer.files.length > 0) { + neverNull(a.dropHandler)({ + dropType: DropType.ExternalFile, + files: fileListToArray(ev.dataTransfer.files), + }) + } else { + console.error("received onDrop DragEvent has invalid DropType or is unsupported on this platform!") } } } diff --git a/src/common/login/PostLoginActions.ts b/src/common/login/PostLoginActions.ts index acd8a874b87..1a2f7eedc71 100644 --- a/src/common/login/PostLoginActions.ts +++ b/src/common/login/PostLoginActions.ts @@ -198,6 +198,27 @@ export class PostLoginActions implements PostLoginAction { // Needs to be called after UsageTestModel.init() if the UsageOptInNews is live! (its isShown() requires an initialized UsageTestModel) await locator.newsModel.loadNewsIds() + // FIXME + // initialize imap import + /*if (isDesktop()) { + const userId = locator.logins.getUserController().userId + const unencryptedCredentials = await locator.credentialsProvider.getDecryptedCredentialsByUserId(userId) + + if (unencryptedCredentials) { + const imapCredentials: ImapCredentials = { + password: "imap-password", + username: "imap-user", + host: "mail.gmail.com", + port: 123, + } + + const apiUrl = getApiBaseUrl(locator.domainConfigProvider().getCurrentDomainConfig()) + await locator.mailImportFacade.setupImapImport(apiUrl, unencryptedCredentials, imapCredentials) + } else { + console.error(`could not load credentials for user with userId ${userId}`) + } + }*/ + // Redraw to render usage tests and news, among other things that may have changed. m.redraw() } diff --git a/src/common/misc/TranslationKey.ts b/src/common/misc/TranslationKey.ts index b399e8ff9cb..0fc1ac757a6 100644 --- a/src/common/misc/TranslationKey.ts +++ b/src/common/misc/TranslationKey.ts @@ -1799,3 +1799,5 @@ export type TranslationKeyType = | "yourMessage_label" | "you_label" | "emptyString_msg" + | "emlOrMboxInSharingFiles_msg" + | "mailImportSelection_label" diff --git a/src/common/native/common/generatedipc/DesktopGlobalDispatcher.ts b/src/common/native/common/generatedipc/DesktopGlobalDispatcher.ts index c6473c215ea..6e116e1cd3c 100644 --- a/src/common/native/common/generatedipc/DesktopGlobalDispatcher.ts +++ b/src/common/native/common/generatedipc/DesktopGlobalDispatcher.ts @@ -12,6 +12,8 @@ import { FileFacade } from "./FileFacade.js" import { FileFacadeReceiveDispatcher } from "./FileFacadeReceiveDispatcher.js" import { InterWindowEventFacade } from "./InterWindowEventFacade.js" import { InterWindowEventFacadeReceiveDispatcher } from "./InterWindowEventFacadeReceiveDispatcher.js" +import { MailImportFacade } from "./MailImportFacade.js" +import { MailImportFacadeReceiveDispatcher } from "./MailImportFacadeReceiveDispatcher.js" import { NativeCredentialsFacade } from "./NativeCredentialsFacade.js" import { NativeCredentialsFacadeReceiveDispatcher } from "./NativeCredentialsFacadeReceiveDispatcher.js" import { NativeCryptoFacade } from "./NativeCryptoFacade.js" @@ -36,6 +38,7 @@ export class DesktopGlobalDispatcher { private readonly externalCalendarFacade: ExternalCalendarFacadeReceiveDispatcher private readonly fileFacade: FileFacadeReceiveDispatcher private readonly interWindowEventFacade: InterWindowEventFacadeReceiveDispatcher + private readonly mailImportFacade: MailImportFacadeReceiveDispatcher private readonly nativeCredentialsFacade: NativeCredentialsFacadeReceiveDispatcher private readonly nativeCryptoFacade: NativeCryptoFacadeReceiveDispatcher private readonly nativePushFacade: NativePushFacadeReceiveDispatcher @@ -51,6 +54,7 @@ export class DesktopGlobalDispatcher { externalCalendarFacade: ExternalCalendarFacade, fileFacade: FileFacade, interWindowEventFacade: InterWindowEventFacade, + mailImportFacade: MailImportFacade, nativeCredentialsFacade: NativeCredentialsFacade, nativeCryptoFacade: NativeCryptoFacade, nativePushFacade: NativePushFacade, @@ -66,6 +70,7 @@ export class DesktopGlobalDispatcher { this.externalCalendarFacade = new ExternalCalendarFacadeReceiveDispatcher(externalCalendarFacade) this.fileFacade = new FileFacadeReceiveDispatcher(fileFacade) this.interWindowEventFacade = new InterWindowEventFacadeReceiveDispatcher(interWindowEventFacade) + this.mailImportFacade = new MailImportFacadeReceiveDispatcher(mailImportFacade) this.nativeCredentialsFacade = new NativeCredentialsFacadeReceiveDispatcher(nativeCredentialsFacade) this.nativeCryptoFacade = new NativeCryptoFacadeReceiveDispatcher(nativeCryptoFacade) this.nativePushFacade = new NativePushFacadeReceiveDispatcher(nativePushFacade) @@ -90,6 +95,8 @@ export class DesktopGlobalDispatcher { return this.fileFacade.dispatch(methodName, args) case "InterWindowEventFacade": return this.interWindowEventFacade.dispatch(methodName, args) + case "MailImportFacade": + return this.mailImportFacade.dispatch(methodName, args) case "NativeCredentialsFacade": return this.nativeCredentialsFacade.dispatch(methodName, args) case "NativeCryptoFacade": diff --git a/src/common/native/common/generatedipc/MailImportFacade.ts b/src/common/native/common/generatedipc/MailImportFacade.ts new file mode 100644 index 00000000000..cebcc4b596f --- /dev/null +++ b/src/common/native/common/generatedipc/MailImportFacade.ts @@ -0,0 +1,33 @@ +/* generated file, don't edit. */ + +import { UnencryptedCredentials } from "./UnencryptedCredentials.js" +/** + * Facade implemented by the native desktop client enabling mail imports, both from files, and via IMAP. + */ +export interface MailImportFacade { + /** + * Initializing an IMAP import. + */ + setupImapImport(apiUrl: string, unencryptedTutaCredentials: UnencryptedCredentials): Promise + + /** + * Start an IMAP import. + */ + startImapImport(): Promise + + /** + * Stop a running IMAP import. + */ + stopImapImport(): Promise + + /** + * Import multiple mails from .eml or .mbox files. + */ + importFromFiles( + apiUrl: string, + unencryptedTutaCredentials: UnencryptedCredentials, + targetOwnerGroup: string, + targetFolder: ReadonlyArray, + filePaths: ReadonlyArray, + ): Promise +} diff --git a/src/common/native/common/generatedipc/MailImportFacadeReceiveDispatcher.ts b/src/common/native/common/generatedipc/MailImportFacadeReceiveDispatcher.ts new file mode 100644 index 00000000000..56aaa05598e --- /dev/null +++ b/src/common/native/common/generatedipc/MailImportFacadeReceiveDispatcher.ts @@ -0,0 +1,31 @@ +/* generated file, don't edit. */ + +import { UnencryptedCredentials } from "./UnencryptedCredentials.js" +import { MailImportFacade } from "./MailImportFacade.js" + +export class MailImportFacadeReceiveDispatcher { + constructor(private readonly facade: MailImportFacade) {} + async dispatch(method: string, arg: Array): Promise { + switch (method) { + case "setupImapImport": { + const apiUrl: string = arg[0] + const unencryptedTutaCredentials: UnencryptedCredentials = arg[1] + return this.facade.setupImapImport(apiUrl, unencryptedTutaCredentials) + } + case "startImapImport": { + return this.facade.startImapImport() + } + case "stopImapImport": { + return this.facade.stopImapImport() + } + case "importFromFiles": { + const apiUrl: string = arg[0] + const unencryptedTutaCredentials: UnencryptedCredentials = arg[1] + const targetOwnerGroup: string = arg[2] + const targetFolder: ReadonlyArray = arg[3] + const filePaths: ReadonlyArray = arg[4] + return this.facade.importFromFiles(apiUrl, unencryptedTutaCredentials, targetOwnerGroup, targetFolder, filePaths) + } + } + } +} diff --git a/src/common/native/common/generatedipc/MailImportFacadeSendDispatcher.ts b/src/common/native/common/generatedipc/MailImportFacadeSendDispatcher.ts new file mode 100644 index 00000000000..fb3889decb2 --- /dev/null +++ b/src/common/native/common/generatedipc/MailImportFacadeSendDispatcher.ts @@ -0,0 +1,22 @@ +/* generated file, don't edit. */ + +import { MailImportFacade } from "./MailImportFacade.js" + +interface NativeInterface { + invokeNative(requestType: string, args: unknown[]): Promise +} +export class MailImportFacadeSendDispatcher implements MailImportFacade { + constructor(private readonly transport: NativeInterface) {} + async setupImapImport(...args: Parameters) { + return this.transport.invokeNative("ipc", ["MailImportFacade", "setupImapImport", ...args]) + } + async startImapImport(...args: Parameters) { + return this.transport.invokeNative("ipc", ["MailImportFacade", "startImapImport", ...args]) + } + async stopImapImport(...args: Parameters) { + return this.transport.invokeNative("ipc", ["MailImportFacade", "stopImapImport", ...args]) + } + async importFromFiles(...args: Parameters) { + return this.transport.invokeNative("ipc", ["MailImportFacade", "importFromFiles", ...args]) + } +} diff --git a/src/common/native/common/generatedipc/TutaCredentials.ts b/src/common/native/common/generatedipc/TutaCredentials.ts new file mode 100644 index 00000000000..b741bb6c7b8 --- /dev/null +++ b/src/common/native/common/generatedipc/TutaCredentials.ts @@ -0,0 +1,3 @@ +/* generated file, don't edit. */ + +export { TutaCredentials } from "../../../../../packages/node-mimimi/dist/binding.cjs" diff --git a/src/common/native/main/NativeInterfaceFactory.ts b/src/common/native/main/NativeInterfaceFactory.ts index 976b876428c..879cf7a938e 100644 --- a/src/common/native/main/NativeInterfaceFactory.ts +++ b/src/common/native/main/NativeInterfaceFactory.ts @@ -38,6 +38,8 @@ import { MobilePaymentsFacadeSendDispatcher } from "../common/generatedipc/Mobil import { AppType } from "../../misc/ClientConstants.js" import { ExternalCalendarFacade } from "../common/generatedipc/ExternalCalendarFacade.js" import { ExternalCalendarFacadeSendDispatcher } from "../common/generatedipc/ExternalCalendarFacadeSendDispatcher.js" +import { MailImportFacadeSendDispatcher } from "../common/generatedipc/MailImportFacadeSendDispatcher" +import { MailImportFacade } from "../common/generatedipc/MailImportFacade" export type NativeInterfaces = { native: NativeInterfaceMain @@ -56,6 +58,7 @@ export type DesktopInterfaces = { searchTextFacade: SearchTextInAppFacade desktopSettingsFacade: SettingsFacadeSendDispatcher desktopSystemFacade: DesktopSystemFacade + mailImportFacade: MailImportFacade interWindowEventSender: InterWindowEventFacadeSendDispatcher } @@ -113,6 +116,7 @@ export function createDesktopInterfaces(native: NativeInterfaceMain): DesktopInt searchTextFacade: new SearchTextInAppFacadeSendDispatcher(native), desktopSettingsFacade: new SettingsFacadeSendDispatcher(native), desktopSystemFacade: new DesktopSystemFacadeSendDispatcher(native), + mailImportFacade: new MailImportFacadeSendDispatcher(native), interWindowEventSender: new InterWindowEventFacadeSendDispatcher(native), } } diff --git a/src/common/native/main/WebCommonNativeFacade.ts b/src/common/native/main/WebCommonNativeFacade.ts index fa5c8b1b01f..2573b9e5762 100644 --- a/src/common/native/main/WebCommonNativeFacade.ts +++ b/src/common/native/main/WebCommonNativeFacade.ts @@ -15,6 +15,7 @@ import { NativePushServiceApp } from "./NativePushServiceApp.js" import { locator } from "../../api/main/CommonLocator.js" import { AppType } from "../../misc/ClientConstants.js" import { ContactTypeRef } from "../../api/entities/tutanota/TypeRefs.js" +import { isDesktop } from "../../api/common/Env" export class WebCommonNativeFacade implements CommonNativeFacade { constructor( @@ -79,6 +80,7 @@ export class WebCommonNativeFacade implements CommonNativeFacade { const files = await fileApp.getFilesMetaData(filesUris) const allFilesAreVCards = files.length > 0 && files.every((file) => getAttachmentType(file.mimeType) === AttachmentType.CONTACT) const allFilesAreICS = files.length > 0 && files.every((file) => getAttachmentType(file.mimeType) === AttachmentType.CALENDAR) + const allFilesAreMail = files.length > 0 && files.every((file) => getAttachmentType(file.mimeType) === AttachmentType.MAIL) if (this.appType === AppType.Calendar) { if (!allFilesAreICS) { @@ -105,6 +107,14 @@ export class WebCommonNativeFacade implements CommonNativeFacade { }, { text: "attachFiles_action", value: false }, ]) + } else if (isDesktop() && allFilesAreMail) { + willImport = await Dialog.choice("emlOrMboxInSharingFiles_msg", [ + { + text: "import_action", + value: true, + }, + { text: "attachFiles_action", value: false }, + ]) } if (willImport) { @@ -164,7 +174,10 @@ export class WebCommonNativeFacade implements CommonNativeFacade { */ async promptForNewPassword(title: string, oldPassword: string | null): Promise { const [{ Dialog }, { PasswordForm, PasswordModel }] = await Promise.all([import("../../gui/base/Dialog.js"), import("../../settings/PasswordForm.js")]) - const model = new PasswordModel(this.usageTestController, this.logins, { checkOldPassword: false, enforceStrength: false }) + const model = new PasswordModel(this.usageTestController, this.logins, { + checkOldPassword: false, + enforceStrength: false, + }) return new Promise((resolve, reject) => { const changePasswordOkAction = async (dialog: Dialog) => { diff --git a/src/global.d.ts b/src/global.d.ts index 66243dbc1b4..211537f3aa3 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -13,12 +13,14 @@ import { WorkerLocatorType } from "./api/worker/WorkerLocator" import { TopLevelView } from "./TopLevelView.js" interface NativeApp { - // In desktop we can pass whole objects + // In desktop, we can pass whole objects // In app, we can only pass strings invoke(message: any) attach(handler: (JsMessage) => unknown) + getPathForFile(file: File): string + startWebMessageChannel() // Available in android } diff --git a/src/mail-app/app.ts b/src/mail-app/app.ts index 1c919aaddb9..f1a44f81515 100644 --- a/src/mail-app/app.ts +++ b/src/mail-app/app.ts @@ -308,6 +308,7 @@ import("./translations/en.js") cache, header, desktopSystemFacade: mailLocator.desktopSystemFacade, + mailImportFacade: mailLocator.mailImportFacade, mailViewModel, }), }, diff --git a/src/mail-app/mail/editor/MailEditor.ts b/src/mail-app/mail/editor/MailEditor.ts index cdfaaf67d17..65f19dce336 100644 --- a/src/mail-app/mail/editor/MailEditor.ts +++ b/src/mail-app/mail/editor/MailEditor.ts @@ -67,7 +67,7 @@ import { getContactDisplayName } from "../../../common/contactsFunctionality/Con import { ResolvableRecipient } from "../../../common/api/main/RecipientsModel" import { animateToolbar, RichTextToolbar } from "../../../common/gui/base/RichTextToolbar.js" -import { readLocalFiles } from "../../../common/file/FileController" +import { fileListToArray, readLocalFiles } from "../../../common/file/FileController" import { IconButton, IconButtonAttrs } from "../../../common/gui/base/IconButton.js" import { ToggleButton, ToggleButtonAttrs } from "../../../common/gui/base/buttons/ToggleButton.js" import { BootIcons } from "../../../common/gui/base/icons/BootIcons.js" @@ -407,7 +407,8 @@ export class MailEditor implements Component { }, ondrop: (ev: DragEvent) => { if (ev.dataTransfer?.files && ev.dataTransfer.files.length > 0) { - readLocalFiles(ev.dataTransfer.files) + let nativeFiles = fileListToArray(ev.dataTransfer.files) + readLocalFiles(nativeFiles) .then((dataFiles) => { model.attachFiles(dataFiles as any) m.redraw() diff --git a/src/mail-app/mail/view/MailFoldersView.ts b/src/mail-app/mail/view/MailFoldersView.ts index 2b53a7e4d36..d42ebba6f08 100644 --- a/src/mail-app/mail/view/MailFoldersView.ts +++ b/src/mail-app/mail/view/MailFoldersView.ts @@ -21,13 +21,14 @@ import { MailModel } from "../model/MailModel.js" import { getFolderName, MAX_FOLDER_INDENT_LEVEL } from "../model/MailUtils.js" import { getFolderIcon } from "./MailGuiUtils.js" import { isSpamOrTrashFolder } from "../model/MailChecks.js" +import { DropData } from "../../../common/gui/base/GuiUtils" export interface MailFolderViewAttrs { mailModel: MailModel mailboxDetail: MailboxDetail mailFolderElementIdToSelectedMailId: ReadonlyMap onFolderClick: (folder: MailFolder) => unknown - onFolderDrop: (mailId: string, folder: MailFolder) => unknown + onFolderDrop: (dropData: DropData, folder: MailFolder) => unknown expandedFolders: ReadonlySet onFolderExpanded: (folder: MailFolder, state: boolean) => unknown onShowFolderAddEditDialog: (mailGroupId: Id, folder: MailFolder | null, parentFolder: MailFolder | null) => unknown @@ -111,7 +112,7 @@ export class MailFoldersView implements Component { isSelectedPrefix: attrs.inEditMode ? false : MAIL_PREFIX + "/" + getElementId(system.folder), colors: NavButtonColor.Nav, click: () => attrs.onFolderClick(system.folder), - dropHandler: (droppedMailId) => attrs.onFolderDrop(droppedMailId, system.folder), + dropHandler: (dropData) => attrs.onFolderDrop(dropData, system.folder), disableHoverBackground: true, disabled: attrs.inEditMode, } diff --git a/src/mail-app/mail/view/MailListView.ts b/src/mail-app/mail/view/MailListView.ts index 897e5e40193..79f458b1e00 100644 --- a/src/mail-app/mail/view/MailListView.ts +++ b/src/mail-app/mail/view/MailListView.ts @@ -34,6 +34,7 @@ import { mailLocator } from "../../mailLocator.js" import { assertSystemFolderOfType } from "../model/MailUtils.js" import { canDoDragAndDropExport } from "./MailViewerUtils.js" import { isOfTypeOrSubfolderOf } from "../model/MailChecks.js" +import { DropType } from "../../../common/gui/base/GuiUtils" assertMainOrNode() @@ -140,7 +141,7 @@ export class MailListView implements Component { this._doExportDrag(draggedMails) } else if (styles.isDesktopLayout()) { // Desktop layout only because it doesn't make sense to drag mails to folders when the folder list and mail list aren't visible at the same time - neverNull(event.dataTransfer).setData("text", getLetId(neverNull(mailUnderCursor))[1]) + neverNull(event.dataTransfer).setData(DropType.Mail, getLetId(neverNull(mailUnderCursor))[1]) } else { event.preventDefault() } @@ -166,7 +167,7 @@ export class MailListView implements Component { this._doExportDrag(draggedMails) } else if (styles.isDesktopLayout()) { // Desktop layout only because it doesn't make sense to drag mails to folders when the folder list and mail list aren't visible at the same time - neverNull(event.dataTransfer).setData("text", getLetId(neverNull(mailUnderCursor))[1]) + neverNull(event.dataTransfer).setData(DropType.Mail, getLetId(neverNull(mailUnderCursor))[1]) } else { event.preventDefault() } diff --git a/src/mail-app/mail/view/MailView.ts b/src/mail-app/mail/view/MailView.ts index 83edcf57ce6..2cffbe42f06 100644 --- a/src/mail-app/mail/view/MailView.ts +++ b/src/mail-app/mail/view/MailView.ts @@ -8,7 +8,7 @@ import { AppHeaderAttrs, Header } from "../../../common/gui/Header.js" import type { Mail, MailFolder } from "../../../common/api/entities/tutanota/TypeRefs.js" import { noOp, ofClass } from "@tutao/tutanota-utils" import { MailListView } from "./MailListView" -import { assertMainOrNode, isApp } from "../../../common/api/common/Env" +import { assertMainOrNode, getApiBaseUrl, isApp } from "../../../common/api/common/Env" import type { Shortcut } from "../../../common/misc/KeyManager" import { keyManager } from "../../../common/misc/KeyManager" import { getMailSelectionMessage, MultiItemViewer } from "./MultiItemViewer.js" @@ -19,14 +19,12 @@ import { locator } from "../../../common/api/main/CommonLocator" import { PermissionError } from "../../../common/api/common/error/PermissionError" import { styles } from "../../../common/gui/styles" import { px, size } from "../../../common/gui/size" -import { UserError } from "../../../common/api/main/UserError" -import { showUserError } from "../../../common/misc/ErrorHandlerImpl" import { archiveMails, getConversationTitle, getMoveMailBounds, moveMails, moveToInbox, promptAndDeleteMails, showMoveMailsDropdown } from "./MailGuiUtils" import { getElementId, isSameId } from "../../../common/api/common/utils/EntityUtils" import { isNewMailActionAvailable } from "../../../common/gui/nav/NavFunctions" import { CancelledError } from "../../../common/api/common/error/CancelledError" import Stream from "mithril/stream" -import { readLocalFiles } from "../../../common/file/FileController.js" +import { fileListToArray, readLocalFiles } from "../../../common/file/FileController.js" import { MobileMailActionBar } from "./MobileMailActionBar.js" import { deviceConfig } from "../../../common/misc/DeviceConfig.js" import { DrawerMenuAttrs } from "../../../common/gui/nav/DrawerMenu.js" @@ -39,7 +37,6 @@ import { EditFoldersDialog } from "./EditFoldersDialog.js" import { TopLevelAttrs, TopLevelView } from "../../../TopLevelView.js" import { ConversationViewModel } from "./ConversationViewModel.js" import { conversationCardMargin, ConversationViewer } from "./ConversationViewer.js" -import type { DesktopSystemFacade } from "../../../common/native/common/generatedipc/DesktopSystemFacade.js" import { IconButton } from "../../../common/gui/base/IconButton.js" import { BackgroundColumnLayout } from "../../../common/gui/BackgroundColumnLayout.js" import { MailViewerActions } from "./MailViewerToolbar.js" @@ -63,6 +60,8 @@ import { showSnackBar } from "../../../common/gui/base/SnackBar.js" import { getFolderName } from "../model/MailUtils.js" import { canDoDragAndDropExport } from "./MailViewerUtils.js" import { isSpamOrTrashFolder } from "../model/MailChecks.js" +import { DropType, FileDropData, MailDropData } from "../../../common/gui/base/GuiUtils" +import { MailImportFacade } from "../../../common/native/common/generatedipc/MailImportFacade" assertMainOrNode() @@ -76,7 +75,7 @@ export interface MailViewAttrs extends TopLevelAttrs { drawerAttrs: DrawerMenuAttrs cache: MailViewCache header: AppHeaderAttrs - desktopSystemFacade: DesktopSystemFacade | null + mailImportFacade: MailImportFacade | null mailViewModel: MailViewModel } @@ -88,7 +87,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView) { super() - this.desktopSystemFacade = vnode.attrs.desktopSystemFacade + this.mailImportFacade = vnode.attrs.mailImportFacade this.expandedState = new Set(deviceConfig.getExpandedFolders(locator.logins.getUserController().userId)) this.cache = vnode.attrs.cache this.folderColumn = this.createFolderColumn(null, vnode.attrs.drawerAttrs) @@ -247,7 +246,10 @@ export class MailView extends BaseTopLevelView implements TopLevelView this.mailViewModel.setFilter(filter) }) + return m(MailFilterButton, { + filter: this.mailViewModel.filterType, + setFilter: (filter) => this.mailViewModel.setFilter(filter), + }) } private mailViewerSingleActions(viewModel: ConversationViewModel) { @@ -334,24 +336,10 @@ export class MailView extends BaseTopLevelView implements TopLevelView { if (isNewMailActionAvailable() && ev.dataTransfer?.files && ev.dataTransfer.files.length > 0) { - Promise.all([ - this.mailViewModel.getMailboxDetails(), - readLocalFiles(ev.dataTransfer.files), - import("../signature/Signature"), - import("../editor/MailEditor"), - ]) - .then(([mailbox, dataFiles, { appendEmailSignature }, { newMailEditorFromTemplate }]) => { - mailbox && - newMailEditorFromTemplate( - mailbox, - {}, - "", - appendEmailSignature("", locator.logins.getUserController().props), - dataFiles, - ).then((dialog) => dialog.show()) - }) - .catch(ofClass(PermissionError, noOp)) - .catch(ofClass(UserError, showUserError)) + this.handleFileDrop({ + dropType: DropType.ExternalFile, + files: fileListToArray(ev.dataTransfer.files), + }) } // prevent in any case because firefox tries to open @@ -619,7 +607,13 @@ export class MailView extends BaseTopLevelView implements TopLevelView this.setExpandedState(folder, state), onShowFolderAddEditDialog: (...args) => this.showFolderAddEditDialog(...args), onDeleteCustomMailFolder: (folder) => this.deleteCustomMailFolder(mailboxDetail, folder), - onFolderDrop: (mailId, folder) => this.handleFolderDrop(mailId, folder), + onFolderDrop: (dropData, folder) => { + if (dropData.dropType == DropType.Mail) { + this.handleFolderMailDrop(dropData, folder) + } else if (dropData.dropType == DropType.ExternalFile) { + this.handeFolderFileDrop(dropData, folder) + } + }, inEditMode, onEditMailbox, }) @@ -683,24 +677,86 @@ export class MailView extends BaseTopLevelView implements TopLevelView getElementId(item) === droppedMailId) + const entity = this.mailViewModel.listModel.state.items.find((item) => getElementId(item) === mailId) if (entity) { mailsToMove.push(entity) } } - moveMails({ mailboxModel: locator.mailboxModel, mailModel: mailLocator.mailModel, mails: mailsToMove, targetMailFolder: folder }) + moveMails({ + mailboxModel: locator.mailboxModel, + mailModel: mailLocator.mailModel, + mails: mailsToMove, + targetMailFolder: folder, + }) + } + + private async handeFolderFileDrop(dropData: FileDropData, mailFolder: MailFolder) { + function droppedOnlyMailFiles(files: Array): boolean { + // there's similar logic on the AttachmentBubble, but for natively shared files. + return files.every((f) => f.name.endsWith(".eml") || f.name.endsWith(".mbox")) + } + + const willImport = + droppedOnlyMailFiles(dropData.files) && + (await Dialog.choice("emlOrMboxInSharingFiles_msg", [ + { + text: "import_action", + value: true, + }, + { + text: "attachFiles_action", + value: false, + }, + ])) + + if (!willImport) { + await this.handleFileDrop(dropData) + } else { + const userId = locator.logins.getUserController().userId + const unencryptedCredentials = await locator.credentialsProvider.getDecryptedCredentialsByUserId(userId) + + if (unencryptedCredentials && mailFolder._ownerGroup) { + const apiUrl = getApiBaseUrl(locator.domainConfigProvider().getCurrentDomainConfig()) + this.mailImportFacade?.importFromFiles( + apiUrl, + unencryptedCredentials, + mailFolder._ownerGroup, + mailFolder._id, + dropData.files.map((f) => window.nativeApp.getPathForFile(f)), + ) + } + } } private async showNewMailDialog(): Promise { diff --git a/src/mail-app/mailLocator.ts b/src/mail-app/mailLocator.ts index ea1df577ba1..1cecdbf5828 100644 --- a/src/mail-app/mailLocator.ts +++ b/src/mail-app/mailLocator.ts @@ -1,4 +1,4 @@ -import { assertMainOrNode, isAndroidApp, isApp, isBrowser, isDesktop, isElectronClient, isIOSApp, isTest } from "../common/api/common/Env.js" +import { assertMainOrNode, getApiBaseUrl, isAndroidApp, isApp, isBrowser, isDesktop, isElectronClient, isIOSApp, isTest } from "../common/api/common/Env.js" import { EventController } from "../common/api/main/EventController.js" import { SearchModel } from "./search/model/SearchModel.js" import { type MailboxDetail, MailboxModel } from "../common/mailFunctionality/MailboxModel.js" @@ -8,7 +8,7 @@ import { EntityClient } from "../common/api/common/EntityClient.js" import { ProgressTracker } from "../common/api/main/ProgressTracker.js" import { CredentialsProvider } from "../common/misc/credentials/CredentialsProvider.js" import { bootstrapWorker, WorkerClient } from "../common/api/main/WorkerClient.js" -import { CALENDAR_MIME_TYPE, FileController, guiDownload, VCARD_MIME_TYPES } from "../common/file/FileController.js" +import { CALENDAR_MIME_TYPE, FileController, guiDownload, MAIL_MIME_TYPES, VCARD_MIME_TYPES } from "../common/file/FileController.js" import { SecondFactorHandler } from "../common/misc/2fa/SecondFactorHandler.js" import { WebauthnClient } from "../common/misc/2fa/webauthn/WebauthnClient.js" import { LoginFacade } from "../common/api/worker/facades/LoginFacade.js" @@ -138,6 +138,8 @@ import { ParsedEvent } from "../common/calendar/import/CalendarImporter.js" import { lang } from "../common/misc/LanguageViewModel.js" import type { CalendarContactPreviewViewModel } from "../calendar-app/calendar/gui/eventpopup/CalendarContactPreviewViewModel.js" import { KeyLoaderFacade } from "../common/api/worker/facades/KeyLoaderFacade.js" +import { MailImportFacade } from "../common/native/common/generatedipc/MailImportFacade" +import { folderSelectionDialog } from "../common/desktop/mailimport/MailImporter" assertMainOrNode() @@ -183,6 +185,7 @@ class MailLocator { searchTextFacade!: SearchTextInAppFacade desktopSettingsFacade!: SettingsFacade desktopSystemFacade!: DesktopSystemFacade + mailImportFacade!: MailImportFacade webMobileFacade!: WebMobileFacade systemPermissionHandler!: SystemPermissionHandler interWindowEventSender!: InterWindowEventFacadeSendDispatcher @@ -834,6 +837,7 @@ class MailLocator { if (isDesktop()) { this.desktopSettingsFacade = desktopInterfaces.desktopSettingsFacade this.desktopSystemFacade = desktopInterfaces.desktopSystemFacade + this.mailImportFacade = desktopInterfaces.mailImportFacade } } else if (isAndroidApp() || isIOSApp()) { const { SystemPermissionHandler } = await import("../common/native/main/SystemPermissionHandler.js") @@ -972,6 +976,7 @@ class MailLocator { const files = await this.fileApp.getFilesMetaData(filesUris) const areAllFilesVCard = files.every((file) => file.mimeType === VCARD_MIME_TYPES.X_VCARD || file.mimeType === VCARD_MIME_TYPES.VCARD) const areAllFilesICS = files.every((file) => file.mimeType === CALENDAR_MIME_TYPE) + const areAllFilesMail = files.every((file) => file.mimeType === MAIL_MIME_TYPES.EML || file.mimeType === MAIL_MIME_TYPES.MBOX) if (areAllFilesVCard) { const importer = await this.contactImporter() @@ -1008,6 +1013,23 @@ class MailLocator { dialog.close() handleCalendarImport(selectedCalendar.groupRoot, parsedEvents) }) + } else if (areAllFilesMail) { + // Mail import of .eml or .mbox files is only supported on desktop currently. + // The WebCommonNativeFacade performs a platform check before calling this function. + const folderSystems = Array.from(this.mailModel.folders().values()) + const indentedMailFolders = folderSystems.flatMap((folderSystem) => folderSystem.getIndentedList()) + + folderSelectionDialog(indentedMailFolders, async (dialog, selectedMailFolder) => { + dialog.close() + + const userId = locator.logins.getUserController().userId + const unencryptedCredentials = await locator.credentialsProvider.getDecryptedCredentialsByUserId(userId) + + if (unencryptedCredentials && selectedMailFolder._ownerGroup) { + const apiUrl = getApiBaseUrl(locator.domainConfigProvider().getCurrentDomainConfig()) + this.mailImportFacade?.importFromFiles(apiUrl, unencryptedCredentials, selectedMailFolder._ownerGroup, selectedMailFolder._id, filesUris) + } + }) } } @@ -1128,7 +1150,11 @@ class MailLocator { for (const [id, name] of CLIENT_ONLY_CALENDARS.entries()) { const calendarId = `${this.logins.getUserController().userId}#${id}` const config = configs.get(calendarId) - if (!config) deviceConfig.updateClientOnlyCalendars(calendarId, { name: lang.get(name), color: DEFAULT_CLIENT_ONLY_CALENDAR_COLORS.get(id)! }) + if (!config) + deviceConfig.updateClientOnlyCalendars(calendarId, { + name: lang.get(name), + color: DEFAULT_CLIENT_ONLY_CALENDAR_COLORS.get(id)!, + }) } } diff --git a/src/mail-app/translations/en.ts b/src/mail-app/translations/en.ts index 7ee12825a6e..7a279a9b5db 100644 --- a/src/mail-app/translations/en.ts +++ b/src/mail-app/translations/en.ts @@ -1814,6 +1814,8 @@ export default { "yourCalendars_label": "Your calendars", "yourFolders_action": "YOUR FOLDERS", "yourMessage_label": "Your message", - "you_label": "You" + "you_label": "You", + "emlOrMboxInSharingFiles_msg": "One or more mail files were detected. Would you like to import or attach them?", + "mailImportSelection_label": "Import or Attach?" } } diff --git a/test/tests/api/worker/crypto/InstanceMapperTest.ts b/test/tests/api/worker/crypto/InstanceMapperTest.ts index dc4ec9e7e4a..bd78c7bdf13 100644 --- a/test/tests/api/worker/crypto/InstanceMapperTest.ts +++ b/test/tests/api/worker/crypto/InstanceMapperTest.ts @@ -295,6 +295,7 @@ o.spec("InstanceMapper", function () { o(encryptValue("_id", vt, null, null)).equals(null) o(encryptValue("_permissions", vt, null, null)).equals(null) }) + o("throw error on ONE null values (enc String)", makeTestForErrorOnNull(ValueType.String)) o("throw error on ONE null values (enc Date)", makeTestForErrorOnNull(ValueType.Date)) o("throw error on ONE null values (enc Bytes)", makeTestForErrorOnNull(ValueType.Bytes)) diff --git a/test/types/test.d.ts b/test/types/test.d.ts index eb92ac06b6f..0a153385478 100644 --- a/test/types/test.d.ts +++ b/test/types/test.d.ts @@ -26,4 +26,5 @@ declare function node(f: F): F */ declare const buildOptions: { readonly sqliteNativePath: string + readonly mimimiNativePath: string } diff --git a/tsconfig.json b/tsconfig.json index 91bb648f77f..ca3efda3090 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,6 +25,9 @@ }, { "path": "./packages/tuta-wasm-loader" + }, + { + "path": "./packages/node-mimimi" } ] } diff --git a/tuta-sdk/rust/Cargo.lock b/tuta-sdk/rust/Cargo.lock index 4a72d57f176..583f097aa60 100644 --- a/tuta-sdk/rust/Cargo.lock +++ b/tuta-sdk/rust/Cargo.lock @@ -317,12 +317,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "bumpalo" -version = "3.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" - [[package]] name = "byteorder" version = "1.5.0" @@ -582,17 +576,6 @@ dependencies = [ "parking_lot_core", ] -[[package]] -name = "demo" -version = "0.1.0" -dependencies = [ - "async-trait", - "base64", - "reqwest", - "tokio", - "tuta-sdk", -] - [[package]] name = "der" version = "0.7.9" @@ -625,17 +608,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "downcast" version = "0.11.0" @@ -654,15 +626,6 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -679,12 +642,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "fastrand" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" - [[package]] name = "fiat-crypto" version = "0.2.9" @@ -697,21 +654,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1040,22 +982,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.10" @@ -1075,145 +1001,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "idna" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - [[package]] name = "indexmap" version = "2.6.0" @@ -1234,12 +1021,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "ipnet" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" - [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1270,15 +1051,6 @@ dependencies = [ "libc", ] -[[package]] -name = "js-sys" -version = "0.3.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" -dependencies = [ - "wasm-bindgen", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -1322,12 +1094,6 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" -[[package]] -name = "litemap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" - [[package]] name = "lock_api" version = "0.4.12" @@ -1472,23 +1238,6 @@ dependencies = [ "syn", ] -[[package]] -name = "native-tls" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" -dependencies = [ - "libc", - "log 0.4.22", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nom" version = "7.1.3" @@ -1597,50 +1346,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" -[[package]] -name = "openssl" -version = "0.10.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" -[[package]] -name = "openssl-sys" -version = "0.9.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "oslog" version = "0.2.0" @@ -1652,16 +1363,6 @@ dependencies = [ "log 0.4.22", ] -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - [[package]] name = "parking_lot_core" version = "0.9.10" @@ -1740,12 +1441,6 @@ dependencies = [ "spki", ] -[[package]] -name = "pkg-config" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" - [[package]] name = "plain" version = "0.2.3" @@ -1954,49 +1649,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" -[[package]] -name = "reqwest" -version = "0.12.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-tls", - "hyper-util", - "ipnet", - "js-sys", - "log 0.4.22", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "system-configuration", - "tokio", - "tokio-native-tls", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows-registry", -] - [[package]] name = "ring" version = "0.17.8" @@ -2236,18 +1888,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "sha2" version = "0.10.8" @@ -2265,15 +1905,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - [[package]] name = "signature" version = "2.2.0" @@ -2349,12 +1980,6 @@ dependencies = [ "der", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "static_assertions" version = "1.1.0" @@ -2384,60 +2009,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tempfile" -version = "3.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" -dependencies = [ - "cfg-if", - "fastrand", - "once_cell", - "rustix", - "windows-sys 0.59.0", -] - [[package]] name = "termtree" version = "0.4.1" @@ -2506,16 +2077,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "tokio" version = "1.41.0" @@ -2526,9 +2087,7 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -2545,16 +2104,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.0" @@ -2841,41 +2390,12 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "url" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.5" @@ -2897,83 +2417,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "wasm-bindgen" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" -dependencies = [ - "cfg-if", - "once_cell", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" -dependencies = [ - "bumpalo", - "log 0.4.22", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" - -[[package]] -name = "web-sys" -version = "0.3.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "weedle2" version = "5.0.0" @@ -2994,36 +2437,6 @@ dependencies = [ "rustix", ] -[[package]] -name = "windows-registry" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" -dependencies = [ - "windows-result", - "windows-strings", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result", - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -3181,42 +2594,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - -[[package]] -name = "yoke" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "zerocopy" version = "0.7.35" @@ -3238,27 +2615,6 @@ dependencies = [ "syn", ] -[[package]] -name = "zerofrom" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "zeroize" version = "1.8.1" @@ -3278,25 +2634,3 @@ dependencies = [ "quote", "syn", ] - -[[package]] -name = "zerovec" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/tuta-sdk/rust/Cargo.toml b/tuta-sdk/rust/Cargo.toml index 3c027e5a930..c14ace174bd 100644 --- a/tuta-sdk/rust/Cargo.toml +++ b/tuta-sdk/rust/Cargo.toml @@ -5,8 +5,4 @@ resolver = "2" members = [ "sdk", "uniffi-bindgen", - "demo", -] - -# Exclude demo from default members to prevent cross-compiling openssl -default-members = ["sdk", "uniffi-bindgen"] \ No newline at end of file +] \ No newline at end of file diff --git a/tuta-sdk/rust/demo/Cargo.toml b/tuta-sdk/rust/demo/Cargo.toml deleted file mode 100644 index 5ea59e6b92d..00000000000 --- a/tuta-sdk/rust/demo/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "demo" -version = "0.1.0" -edition = "2021" - -[dependencies] -tuta-sdk = { path = "../sdk" } -reqwest = "0.12.7" -tokio = { version = "1.40.0", features = ["full"] } -async-trait = "0.1.80" -base64 = "0.22.1" \ No newline at end of file diff --git a/tuta-sdk/rust/demo/src/main.rs b/tuta-sdk/rust/demo/src/main.rs deleted file mode 100644 index 4f76743047e..00000000000 --- a/tuta-sdk/rust/demo/src/main.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::collections::HashMap; -use std::error::Error; -use std::sync::Arc; - -use async_trait::async_trait; - -use tutasdk::folder_system::MailSetKind; -use tutasdk::login::{CredentialType, Credentials}; -use tutasdk::rest_client::{ - HttpMethod, RestClient, RestClientError, RestClientOptions, RestResponse, -}; -use tutasdk::GeneratedId; -use tutasdk::Sdk; - -struct ReqwestHttpClient { - client: reqwest::Client, -} - -#[async_trait] -impl RestClient for ReqwestHttpClient { - async fn request_binary( - &self, - url: String, - method: HttpMethod, - options: RestClientOptions, - ) -> Result { - self.request_inner(url, method, options).await.map_err(|e| { - eprintln!("Network request failed! {:?}", e); - RestClientError::NetworkError - }) - } -} - -impl ReqwestHttpClient { - fn new() -> Self { - ReqwestHttpClient { - client: reqwest::Client::new(), - } - } - async fn request_inner( - &self, - url: String, - method: HttpMethod, - options: RestClientOptions, - ) -> Result> { - use reqwest::header::{HeaderMap, HeaderName}; - let mut req = self.client.request(http_method(method), url); - if let Some(body) = options.body { - req = req.body(body); - } - let mut headers: HeaderMap = HeaderMap::with_capacity(options.headers.len()); - for (key, value) in options.headers { - headers.insert(HeaderName::from_bytes(key.as_bytes())?, value.try_into()?); - } - let res = req.headers(headers).send().await?; - - let mut ret_headers = HashMap::with_capacity(res.headers().len()); - // for some reason collect() does not work - for (key, value) in res.headers() { - ret_headers.insert(key.to_string(), value.to_str()?.to_owned()); - } - Ok(RestResponse { - status: res.status().as_u16() as u32, - headers: ret_headers, - body: Some( - res.bytes() - .await - .expect("assuming response has a body") - .into(), - ), - }) - } -} - -fn http_method(http_method: HttpMethod) -> reqwest::Method { - use reqwest::Method; - match http_method { - HttpMethod::GET => Method::GET, - HttpMethod::POST => Method::POST, - HttpMethod::PUT => Method::PUT, - HttpMethod::DELETE => Method::DELETE, - } -} - -#[tokio::main] -async fn main() -> Result<(), Box> { - use base64::prelude::*; - - // replace with real values - let host = "http://localhost:9000"; - let credentials = Credentials { - login: "bed-free@tutanota.de".to_owned(), - access_token: "access_token".to_owned(), - credential_type: CredentialType::Internal, - user_id: GeneratedId("user_id".to_owned()), - encrypted_passphrase_key: BASE64_STANDARD.decode("encrypted_passphrase_key").unwrap(), - }; - - let rest_client = ReqwestHttpClient::new(); - let sdk = Sdk::new(host.to_owned(), Arc::new(rest_client)); - let session = sdk.login(credentials).await?; - let mail_facade = session.mail_facade(); - - let mailbox = mail_facade.load_user_mailbox().await?; - - let folders = mail_facade.load_folders_for_mailbox(&mailbox).await?; - let inbox = folders - .system_folder_by_type(MailSetKind::Inbox) - .expect("inbox exists"); - let inbox_mails = mail_facade.load_mails_in_folder(inbox).await?; - - println!("Inbox:"); - for mail in inbox_mails { - let sender_arg = if mail.sender.name.is_empty() { - format!("<{}>", mail.sender.address) - } else { - format!("{} <{}>", mail.sender.name, mail.sender.address) - }; - println!("{0: <40}\t{1: <40}", sender_arg, mail.subject) - } - Ok(()) -} diff --git a/tuta-sdk/rust/sdk/Cargo.toml b/tuta-sdk/rust/sdk/Cargo.toml index 575b9583058..487e2309928 100644 --- a/tuta-sdk/rust/sdk/Cargo.toml +++ b/tuta-sdk/rust/sdk/Cargo.toml @@ -86,4 +86,14 @@ explicit_iter_loop = "warn" [[example]] name = "create_session" path = "examples/create_session.rs" +required-features = ["net"] + +[[example]] +name = "get_default_folders" +path = "examples/get_default_folders.rs" +required-features = ["net"] + +[[example]] +name = "http_request" +path = "examples/http_request.rs" required-features = ["net"] \ No newline at end of file diff --git a/tuta-sdk/rust/sdk/examples/get_default_folders.rs b/tuta-sdk/rust/sdk/examples/get_default_folders.rs new file mode 100644 index 00000000000..b2de812fc38 --- /dev/null +++ b/tuta-sdk/rust/sdk/examples/get_default_folders.rs @@ -0,0 +1,46 @@ +use std::error::Error; +use std::sync::Arc; +use tutasdk::folder_system::MailSetKind; +use tutasdk::login::{CredentialType, Credentials}; +use tutasdk::net::native_rest_client::NativeRestClient; +use tutasdk::GeneratedId; +use tutasdk::Sdk; + +#[tokio::main] +async fn main() -> Result<(), Box> { + use base64::prelude::*; + + // replace with real values + let host = "http://localhost:9000"; + let credentials = Credentials { + login: "bed-free@tutanota.de".to_owned(), + access_token: "access_token".to_owned(), + credential_type: CredentialType::Internal, + user_id: GeneratedId("user_id".to_owned()), + encrypted_passphrase_key: BASE64_STANDARD.decode("encrypted_passphrase_key").unwrap(), + }; + + let rest_client = NativeRestClient::try_new().unwrap(); + let sdk = Sdk::new(host.to_owned(), Arc::new(rest_client)); + let session = sdk.login(credentials).await?; + let mail_facade = session.mail_facade(); + + let mailbox = mail_facade.load_user_mailbox().await?; + + let folders = mail_facade.load_folders_for_mailbox(&mailbox).await?; + let inbox = folders + .system_folder_by_type(MailSetKind::Inbox) + .expect("inbox exists"); + let inbox_mails = mail_facade.load_mails_in_folder(inbox).await?; + + println!("Inbox:"); + for mail in inbox_mails { + let sender_arg = if mail.sender.name.is_empty() { + format!("<{}>", mail.sender.address) + } else { + format!("{} <{}>", mail.sender.name, mail.sender.address) + }; + println!("{0: <40}\t{1: <40}", sender_arg, mail.subject) + } + Ok(()) +} diff --git a/tuta-sdk/rust/sdk/examples/http_request.rs b/tuta-sdk/rust/sdk/examples/http_request.rs new file mode 100644 index 00000000000..0a652830baa --- /dev/null +++ b/tuta-sdk/rust/sdk/examples/http_request.rs @@ -0,0 +1,27 @@ +use tutasdk::net::native_rest_client::NativeRestClient; +use tutasdk::rest_client::RestResponse; +use tutasdk::rest_client::{HttpMethod, RestClient, RestClientOptions}; + +#[tokio::main] +async fn main() { + const URL: &str = "https://echo.free.beeceptor.com"; + let rest_client: NativeRestClient = + NativeRestClient::try_new().expect("failed to get rest client"); + let response_pending = rest_client + .request_binary( + URL.to_string(), + HttpMethod::GET, + RestClientOptions { + headers: Default::default(), + body: Default::default(), + }, + ) + .await; + + let RestResponse { + status, + headers: _, + body: _, + } = response_pending.expect("Failed to get response."); + assert_eq!(200, status); +} diff --git a/tuta-sdk/rust/sdk/src/crypto/crypto_facade.rs b/tuta-sdk/rust/sdk/src/crypto/crypto_facade.rs index 1f5c08956ca..b2d291a7662 100644 --- a/tuta-sdk/rust/sdk/src/crypto/crypto_facade.rs +++ b/tuta-sdk/rust/sdk/src/crypto/crypto_facade.rs @@ -61,6 +61,11 @@ impl CryptoFacade { } } + #[must_use] + pub fn get_key_loader_facade(&self) -> &Arc { + &self.key_loader_facade + } + /// Returns the session key from `entity` and resolves the bucket key fields contained inside /// if present pub async fn resolve_session_key( @@ -376,7 +381,7 @@ mod test { use crate::crypto::aes::{Aes256Key, Iv}; use crate::crypto::crypto_facade::{CryptoFacade, CryptoProtocolVersion}; use crate::crypto::ecc::EccKeyPair; - use crate::crypto::key::{AsymmetricKeyPair, GenericAesKey}; + use crate::crypto::key::{AsymmetricKeyPair, GenericAesKey, VersionedAesKey}; use crate::crypto::randomizer_facade::test_util::make_thread_rng_facade; use crate::crypto::randomizer_facade::RandomizerFacade; use crate::crypto::rsa::{RSAEccKeyPair, RSAKeyPair}; @@ -386,7 +391,7 @@ mod test { use crate::entities::generated::tutanota::Mail; use crate::entities::Entity; use crate::instance_mapper::InstanceMapper; - use crate::key_loader_facade::{MockKeyLoaderFacade, VersionedAesKey}; + use crate::key_loader_facade::MockKeyLoaderFacade; use crate::metamodel::TypeModel; use crate::type_model_provider::init_type_model_provider; use crate::util::test_utils::{create_test_entity, typed_entity_to_parsed_entity}; diff --git a/tuta-sdk/rust/sdk/src/crypto/key.rs b/tuta-sdk/rust/sdk/src/crypto/key.rs index 17ca03a9381..39c3b086af9 100644 --- a/tuta-sdk/rust/sdk/src/crypto/key.rs +++ b/tuta-sdk/rust/sdk/src/crypto/key.rs @@ -1,7 +1,7 @@ use super::aes::*; use super::rsa::*; use super::tuta_crypt::*; -use crate::util::ArrayCastingError; +use crate::util::{ArrayCastingError, Versioned}; use crate::ApiCallError; use zeroize::Zeroizing; @@ -159,6 +159,16 @@ impl KeyLoadErrorSubtype for RSAKeyError {} /// Used to handle errors from the entity client impl KeyLoadErrorSubtype for ApiCallError {} +pub type VersionedAesKey = Versioned; + +impl Versioned { + pub fn encrypt_key(&self, key_to_encrypt: &GenericAesKey, iv: Iv) -> Versioned> { + let encrypted_key = self.object.encrypt_key(key_to_encrypt, iv); + // todo: this looks like the vec has the version which is not true + Versioned::new(encrypted_key, self.version) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/tuta-sdk/rust/sdk/src/crypto_entity_client.rs b/tuta-sdk/rust/sdk/src/crypto_entity_client.rs index 1541e0e9020..47624178a82 100644 --- a/tuta-sdk/rust/sdk/src/crypto_entity_client.rs +++ b/tuta-sdk/rust/sdk/src/crypto_entity_client.rs @@ -37,6 +37,10 @@ impl CryptoEntityClient { } } + pub fn get_crypto_facade(&self) -> &Arc { + &self.crypto_facade + } + pub async fn load, ID: IdType>( &self, id: &ID, diff --git a/tuta-sdk/rust/sdk/src/entities.rs b/tuta-sdk/rust/sdk/src/entities.rs index 2a8dbe6cd99..1ae117dd459 100644 --- a/tuta-sdk/rust/sdk/src/entities.rs +++ b/tuta-sdk/rust/sdk/src/entities.rs @@ -10,6 +10,7 @@ use crate::TypeRef; pub mod entity_facade; pub mod generated; +pub mod size_estimator; /// `'static` on trait bound is fine here because Entity does not contain any non-static references. /// See https://doc.rust-lang.org/rust-by-example/scope/lifetime/static_lifetime.html#trait-bound diff --git a/tuta-sdk/rust/sdk/src/entities/size_estimator.rs b/tuta-sdk/rust/sdk/src/entities/size_estimator.rs new file mode 100644 index 00000000000..646f514eeb6 --- /dev/null +++ b/tuta-sdk/rust/sdk/src/entities/size_estimator.rs @@ -0,0 +1,687 @@ +use crate::entities::Entity; +use serde::ser::{ + SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple, + SerializeTupleStruct, SerializeTupleVariant, StdError, +}; +use serde::{ser, Serialize, Serializer}; +use std::fmt::{Debug, Display, Formatter}; + +/// this is the length of an empty encrypted byte slice. +const MINIMUM_ENCRYPTED_SLICE_SIZE: usize = 65; + +/// estimate the size of the given serializable instance when encrypted, mapped and +/// serialized to json. +/// needed for limiting the request size, therefore should prefer to overestimate. +pub fn estimate_json_size(value: &T) -> usize +where + T: Serialize + Entity, +{ + value.serialize(&mut SizeEstimatingSerializer).unwrap() +} + +struct SizeEstimatingSerializer; + +#[derive(Debug)] +struct SizeEstimationError(String); + +impl StdError for SizeEstimationError {} + +impl Display for SizeEstimationError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl ser::Error for SizeEstimationError { + fn custom(msg: T) -> Self { + Self(msg.to_string()) + } +} + +impl<'a> Serializer for &'a mut SizeEstimatingSerializer { + type Ok = usize; + type Error = SizeEstimationError; + type SerializeSeq = SizeEstimatingCompoundSerializer; + type SerializeTuple = ser::Impossible; + type SerializeTupleStruct = ser::Impossible; + type SerializeTupleVariant = ser::Impossible; + type SerializeMap = SizeEstimatingCompoundSerializer; + type SerializeStruct = SizeEstimatingCompoundSerializer; + type SerializeStructVariant = ser::Impossible; + + fn serialize_bool(self, v: bool) -> Result { + Ok(if v { 4 } else { 5 }) + } + + fn serialize_i8(self, _v: i8) -> Result { + unimplemented!("serialize_i8"); + } + + fn serialize_i16(self, _v: i16) -> Result { + unimplemented!("serialize_i16"); + } + + fn serialize_i32(self, _v: i32) -> Result { + unimplemented!("serialize_i32"); + } + + fn serialize_i64(self, _v: i64) -> Result { + unimplemented!("serialize_i64"); + } + + fn serialize_u8(self, _v: u8) -> Result { + unimplemented!("serialize u8"); + } + + fn serialize_u16(self, _v: u16) -> Result { + unimplemented!("serialize_u16") + } + + fn serialize_u32(self, v: u32) -> Result { + Ok((v + 1).ilog10() as usize + 1) + } + + fn serialize_u64(self, v: u64) -> Result { + Ok((v + 1).ilog10() as usize + 1) + } + + fn serialize_f32(self, _v: f32) -> Result { + unimplemented!("serialize_f32") + } + + fn serialize_f64(self, _v: f64) -> Result { + unimplemented!("serialize_f64") + } + + fn serialize_char(self, _v: char) -> Result { + unimplemented!("serialize_char") + } + + fn serialize_str(self, v: &str) -> Result { + self.serialize_bytes(v.as_bytes()) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + // return the byte length of the resulting utf-8 string when b64-encoding the given + // byte slice with padding, taking into account that we're probably going to encrypt the value. + // +2 for the quotes + Ok(enc_base64_size_with_pad(v) + 2) + } + + fn serialize_none(self) -> Result { + Ok("null".len()) + } + + fn serialize_some(self, value: &T) -> Result + where + T: ?Sized + Serialize, + { + value.serialize(self) + } + + fn serialize_unit(self) -> Result { + Ok("null".len()) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + unimplemented!("serialize_unit_struct") + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result { + unimplemented!("serialize_unit_variant") + } + + fn serialize_newtype_struct( + self, + name: &'static str, + value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + use crate::date::DATETIME_STRUCT_NAME; + use crate::id::custom_id::CUSTOM_ID_STRUCT_NAME; + use crate::id::generated_id::GENERATED_ID_STRUCT_NAME; + + match name { + DATETIME_STRUCT_NAME | GENERATED_ID_STRUCT_NAME | CUSTOM_ID_STRUCT_NAME => { + value.serialize(self) + }, + _ => unimplemented!("serialize_newtype_struct"), + } + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + unimplemented!("serialize_newtype_variant") + } + + fn serialize_seq(self, len: Option) -> Result { + let Some(len) = len else { + return Err(SizeEstimationError("serialize_map".into())); + }; + // starting with the brackets + commas + Ok(SizeEstimatingCompoundSerializer( + CompoundType::Seq, + 2 + len.saturating_sub(1), + )) + } + + fn serialize_tuple(self, _len: usize) -> Result { + unreachable!() + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + unreachable!() + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + unreachable!() + } + + /// maps are only used for the _finalIvs field which is not encrypted. + fn serialize_map(self, len: Option) -> Result { + let Some(len) = len else { + return Err(SizeEstimationError("serialize_map".into())); + }; + // starting with the braces + colons + one comma for each field after the first + Ok(SizeEstimatingCompoundSerializer( + CompoundType::Map, + 2 + (len + len).saturating_sub(1), + )) + } + + fn serialize_struct( + self, + name: &'static str, + len: usize, + ) -> Result { + use crate::id::id_tuple::{ID_TUPLE_CUSTOM_NAME, ID_TUPLE_GENERATED_NAME}; + use CompoundType::*; + match name { + // braces + one comma + ID_TUPLE_GENERATED_NAME | ID_TUPLE_CUSTOM_NAME => { + Ok(SizeEstimatingCompoundSerializer(IdTuple, 3)) + }, + // braces + colons + one comma for each field after the first + _ => Ok(SizeEstimatingCompoundSerializer( + Struct, + 2 + (len + len).saturating_sub(1), + )), + } + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + unreachable!() + } +} + +struct SizeEstimatingPlaintextSerializer; + +impl<'a> Serializer for &'a mut SizeEstimatingPlaintextSerializer { + type Ok = usize; + type Error = SizeEstimationError; + type SerializeSeq = ser::Impossible; + type SerializeTuple = ser::Impossible; + type SerializeTupleStruct = ser::Impossible; + type SerializeTupleVariant = ser::Impossible; + type SerializeMap = ser::Impossible; + type SerializeStruct = ser::Impossible; + type SerializeStructVariant = ser::Impossible; + + fn serialize_bool(self, v: bool) -> Result { + unimplemented!("serialize_bool") + } + + fn serialize_i8(self, v: i8) -> Result { + unimplemented!("serialize_i8") + } + + fn serialize_i16(self, v: i16) -> Result { + unimplemented!("serialize_i16") + } + + fn serialize_i32(self, v: i32) -> Result { + unimplemented!("serialize_i32") + } + + fn serialize_i64(self, v: i64) -> Result { + unimplemented!("serialize_i64") + } + + fn serialize_u8(self, v: u8) -> Result { + unimplemented!("serialize_u8") + } + + fn serialize_u16(self, v: u16) -> Result { + unimplemented!("serialize_u16") + } + + fn serialize_u32(self, v: u32) -> Result { + unimplemented!("serialize_iu32") + } + + fn serialize_u64(self, v: u64) -> Result { + unimplemented!("serialize_u64") + } + + fn serialize_f32(self, v: f32) -> Result { + unimplemented!("serialize_f32") + } + + fn serialize_f64(self, v: f64) -> Result { + unimplemented!("serialize_f64") + } + + fn serialize_char(self, v: char) -> Result { + unimplemented!("serialize_char") + } + + fn serialize_str(self, v: &str) -> Result { + Ok(v.len() + 2) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + Ok(plain_base64_size_with_pad(v) + 2) + } + + fn serialize_none(self) -> Result { + unimplemented!("serialize_none") + } + + fn serialize_some(self, value: &T) -> Result + where + T: ?Sized + Serialize, + { + unimplemented!("serialize_some") + } + + fn serialize_unit(self) -> Result { + unimplemented!("serialize_unit") + } + + fn serialize_unit_struct(self, name: &'static str) -> Result { + unimplemented!("serialize_unit_struct") + } + + fn serialize_unit_variant( + self, + name: &'static str, + variant_index: u32, + variant: &'static str, + ) -> Result { + unimplemented!("serialize_unit_variant") + } + + fn serialize_newtype_struct( + self, + name: &'static str, + value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + name: &'static str, + variant_index: u32, + variant: &'static str, + value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + unimplemented!("serialize_newtype_variant") + } + + fn serialize_seq(self, len: Option) -> Result { + unimplemented!("serialize_seq") + } + + fn serialize_tuple(self, len: usize) -> Result { + unimplemented!("serialize_tuple") + } + + fn serialize_tuple_struct( + self, + name: &'static str, + len: usize, + ) -> Result { + unimplemented!("serialize_tuple_struct") + } + + fn serialize_tuple_variant( + self, + name: &'static str, + variant_index: u32, + variant: &'static str, + len: usize, + ) -> Result { + unimplemented!("serialize_tuple_variant") + } + + fn serialize_map(self, len: Option) -> Result { + unimplemented!("serialize_map") + } + + fn serialize_struct( + self, + name: &'static str, + len: usize, + ) -> Result { + unimplemented!("serialize_struct") + } + + fn serialize_struct_variant( + self, + name: &'static str, + variant_index: u32, + variant: &'static str, + len: usize, + ) -> Result { + unimplemented!("serialize_struct_variant") + } +} + +impl<'a> SerializeSeq for SizeEstimatingCompoundSerializer { + type Ok = usize; + type Error = SizeEstimationError; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + self.1 += value.serialize(&mut SizeEstimatingSerializer)?; + Ok(()) + } + + fn end(self) -> Result { + Ok(self.1) + } +} + +// maps are only used for the _finalIvs fields which are not encrypted. +impl<'a> SerializeMap for SizeEstimatingCompoundSerializer { + type Ok = usize; + type Error = SizeEstimationError; + + fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + self.1 += key.serialize(&mut SizeEstimatingPlaintextSerializer)?; + Ok(()) + } + + fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + self.1 += value.serialize(&mut SizeEstimatingPlaintextSerializer)?; + Ok(()) + } + + fn end(self) -> Result { + Ok(self.1) + } +} + +enum CompoundType { + IdTuple, + Struct, + Map, + Seq, +} +struct SizeEstimatingCompoundSerializer(CompoundType, usize); + +impl<'a> SerializeStruct for SizeEstimatingCompoundSerializer { + type Ok = usize; + type Error = SizeEstimationError; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + match self.0 { + CompoundType::IdTuple => { + match key { + "list_id" | "element_id" => self.1 += value.serialize(&mut SizeEstimatingPlaintextSerializer)?, + _ => unreachable!("should not serialize unknown field with CompoundType::IdTuple ") + } + } + CompoundType::Struct => self.1 += key.serialize(&mut SizeEstimatingPlaintextSerializer)? + value.serialize(&mut SizeEstimatingSerializer)?, + _ => unreachable!("shouldn't call SerializeStruct::serialize_field while not serializing an IdTuple or struct") + } + + Ok(()) + } + + fn end(self) -> Result { + Ok(self.1) + } +} + +fn enc_base64_size_with_pad(bytes: &[u8]) -> usize { + // b64 encodes 3 bytes with 4 ascii chars, rounded up. + // since we're encrypting and padding to the block size, + // we also need to add more overhead for that. + (bytes.len() + MINIMUM_ENCRYPTED_SLICE_SIZE) + .div_ceil(3) + .saturating_mul(4) +} + +fn plain_base64_size_with_pad(bytes: &[u8]) -> usize { + // b64 encodes 3 bytes with 4 ascii chars, rounded up. + // since we're padding to the block size, we also need to add more overhead for that. + bytes.len().div_ceil(3).saturating_mul(4) +} + +#[cfg(test)] +mod tests { + use super::{enc_base64_size_with_pad, estimate_json_size, SizeEstimatingSerializer}; + use crate::date::DateTime; + use crate::entities::FinalIv; + use crate::{CustomId, GeneratedId, IdTupleCustom, IdTupleGenerated, TypeRef}; + use serde::Serialize; + use std::collections::HashMap; + + #[derive(Serialize)] + struct FooBarBaz { + pub field_a: A, + pub field_b: B, + pub field_c: C, + } + + impl crate::entities::Entity for FooBarBaz { + fn type_ref() -> TypeRef { + unreachable!() + } + } + + #[test] + fn estimate_struct_size() { + let foo: FooBarBaz = FooBarBaz { + field_a: 0, + field_b: 234, + field_c: true, + }; + assert_eq!( + r#"{"field_a":0,"field_b":234,"field_c":true}"#.len(), + estimate_json_size(&foo) + ); + + let foo2: FooBarBaz = FooBarBaz { + field_a: IdTupleGenerated { + list_id: GeneratedId("moo".to_string()), + element_id: GeneratedId("wuff".to_string()), + }, + field_b: IdTupleCustom { + list_id: GeneratedId("meow".to_string()), + element_id: CustomId("123".to_string()), + }, + field_c: DateTime::from_millis(1753355555555), + }; + + assert_eq!( + r#"{"field_a":["moo","wuff"],"field_b":["meow","123"],"field_c":1753355555555}"#.len(), + estimate_json_size(&foo2) + ); + + let foo3: FooBarBaz, Option<()>, ()> = FooBarBaz { + field_a: None, + field_b: Some(()), + field_c: (), + }; + assert_eq!( + r#"{"field_a":null,"field_b":null,"field_c":null}"#.len(), + estimate_json_size(&foo3) + ); + } + + #[test] + fn estimate_map_size() { + let value = HashMap::from([ + ("some", FinalIv(Vec::from(b"0"))), + ("other", FinalIv(Vec::from(b"234"))), + ]); + assert_eq!( + r#"{"some":"MAo=","other":"===="}"#.len(), + // maps are only used for the _finalIvs fields which are not encrypted. + value.serialize(&mut SizeEstimatingSerializer).unwrap() + ); + } + + #[test] + fn estimate_bool_size() { + assert_eq!(4, true.serialize(&mut SizeEstimatingSerializer).unwrap()); + assert_eq!(5, false.serialize(&mut SizeEstimatingSerializer).unwrap()); + } + + #[test] + fn estimate_str_size() { + assert_eq!( + enc_base64_size_with_pad(b"foo") + 2, + "foo".serialize(&mut SizeEstimatingSerializer).unwrap() + ); + assert_eq!( + enc_base64_size_with_pad(b"") + 2, + "".serialize(&mut SizeEstimatingSerializer).unwrap() + ); + } + + #[test] + fn estimate_num_size() { + assert_eq!(1, 0_u32.serialize(&mut SizeEstimatingSerializer).unwrap()); + assert_eq!(2, 10_u32.serialize(&mut SizeEstimatingSerializer).unwrap()); + } + + #[test] + fn estimate_vec_size() { + assert_eq!( + 2, + Vec::::new() + .serialize(&mut SizeEstimatingSerializer) + .unwrap() + ); + assert_eq!( + "[0,10]".len(), + vec![0_u32, 10_u32] + .serialize(&mut SizeEstimatingSerializer) + .unwrap() + ); + assert_eq!( + "[true,false]".len(), + vec![true, false] + .serialize(&mut SizeEstimatingSerializer) + .unwrap() + ); + assert_eq!( + "[true]".len(), + vec![true].serialize(&mut SizeEstimatingSerializer).unwrap() + ); + assert_eq!( + r#"["",""]"#.len() + enc_base64_size_with_pad(b"0") + enc_base64_size_with_pad(b"10"), + vec!["0", "10"] + .serialize(&mut SizeEstimatingSerializer) + .unwrap() + ); + } + + #[test] + fn estimate_bytes_size() { + // using FinalIv because it's annotated to use serde_bytes for the byte vector serialization. + // serde serializes a bare &[u8] as a sequence or tuple by default + assert_eq!( + enc_base64_size_with_pad(b"") + 2, + FinalIv(b"".as_slice().to_owned()) + .serialize(&mut SizeEstimatingSerializer) + .unwrap() + ); + assert_eq!( + enc_base64_size_with_pad(b"0") + 2, + FinalIv(b"0".as_slice().to_owned()) + .serialize(&mut SizeEstimatingSerializer) + .unwrap() + ); + assert_eq!( + enc_base64_size_with_pad(b"hello") + 2, + FinalIv(b"hello".as_slice().to_owned()) + .serialize(&mut SizeEstimatingSerializer) + .unwrap() + ); + } + + #[test] + fn estimate_date_size() { + assert_eq!( + "123456".len(), + DateTime::from_millis(123456) + .serialize(&mut SizeEstimatingSerializer) + .unwrap() + ); + } + + #[test] + fn estimate_id_tuple() { + let id = IdTupleGenerated::new( + GeneratedId("abc".to_string()), + GeneratedId("defg".to_string()), + ); + assert_eq!( + r#"["abc","defg"]"#.len(), + id.serialize(&mut SizeEstimatingSerializer).unwrap() + ); + } +} diff --git a/tuta-sdk/rust/sdk/src/id/id_tuple.rs b/tuta-sdk/rust/sdk/src/id/id_tuple.rs index 9d54a3d2ebc..9c33bdaa042 100644 --- a/tuta-sdk/rust/sdk/src/id/id_tuple.rs +++ b/tuta-sdk/rust/sdk/src/id/id_tuple.rs @@ -3,6 +3,9 @@ use crate::GeneratedId; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; +pub const ID_TUPLE_GENERATED_NAME: &str = "IdTupleGenerated"; +pub const ID_TUPLE_CUSTOM_NAME: &str = "IdTupleCustom"; + /// Denotes an ID that can be serialised into a string and used to access resources pub trait IdType: Display + 'static {} diff --git a/tuta-sdk/rust/sdk/src/instance_mapper.rs b/tuta-sdk/rust/sdk/src/instance_mapper.rs index 52c89419730..8b58b1955f4 100644 --- a/tuta-sdk/rust/sdk/src/instance_mapper.rs +++ b/tuta-sdk/rust/sdk/src/instance_mapper.rs @@ -364,7 +364,7 @@ impl<'de, 's> Deserializer<'de> for ElementValueDeserializer<'s> { } } } - if name == "IdTupleGenerated" { + if name == crate::id::id_tuple::ID_TUPLE_GENERATED_NAME { return if let ElementValue::IdTupleGeneratedElementId(IdTupleGenerated { list_id: GeneratedId(list_id_str), element_id: GeneratedId(element_id_str), @@ -375,10 +375,10 @@ impl<'de, 's> Deserializer<'de> for ElementValueDeserializer<'s> { value: None, }) } else { - Err(self.wrong_type_err("IdTupleGenerated")) + Err(self.wrong_type_err(crate::id::id_tuple::ID_TUPLE_GENERATED_NAME)) }; } - if name == "IdTupleCustom" { + if name == crate::id::id_tuple::ID_TUPLE_CUSTOM_NAME { return if let ElementValue::IdTupleCustomElementId(IdTupleCustom { list_id: GeneratedId(list_id_str), element_id: CustomId(element_id_str), @@ -389,7 +389,7 @@ impl<'de, 's> Deserializer<'de> for ElementValueDeserializer<'s> { value: None, }) } else { - Err(self.wrong_type_err("IdTupleCustom")) + Err(self.wrong_type_err(crate::id::id_tuple::ID_TUPLE_CUSTOM_NAME)) }; } if let ElementValue::Dict(dict) = self.value { @@ -751,12 +751,12 @@ impl Serializer for ElementValueSerializer { name: &'static str, len: usize, ) -> Result { - if name == "IdTupleGenerated" { + if name == crate::id::id_tuple::ID_TUPLE_GENERATED_NAME { Ok(ElementValueStructSerializer::IdTupleGenerated { list_id: None, element_id: None, }) - } else if name == "IdTupleCustom" { + } else if name == crate::id::id_tuple::ID_TUPLE_CUSTOM_NAME { Ok(ElementValueStructSerializer::IdTupleCustom { list_id: None, element_id: None, @@ -929,10 +929,14 @@ impl ElementValue { ElementValue::Bool(v) => Unexpected::Bool(*v), ElementValue::IdGeneratedId(_) => Unexpected::Other("GeneratedId"), ElementValue::IdCustomId(_) => Unexpected::Other("CustomId"), - ElementValue::IdTupleGeneratedElementId(_) => Unexpected::Other("IdTupleGenerated"), + ElementValue::IdTupleGeneratedElementId(_) => { + Unexpected::Other(crate::id::id_tuple::ID_TUPLE_GENERATED_NAME) + }, ElementValue::Dict(_) => Unexpected::Map, ElementValue::Array(_) => Unexpected::Seq, - ElementValue::IdTupleCustomElementId(_) => Unexpected::Other("IdTupleCustom"), + ElementValue::IdTupleCustomElementId(_) => { + Unexpected::Other(crate::id::id_tuple::ID_TUPLE_CUSTOM_NAME) + }, } } } diff --git a/tuta-sdk/rust/sdk/src/key_cache.rs b/tuta-sdk/rust/sdk/src/key_cache.rs index 0721325f3e1..727f5a38ebf 100644 --- a/tuta-sdk/rust/sdk/src/key_cache.rs +++ b/tuta-sdk/rust/sdk/src/key_cache.rs @@ -1,6 +1,6 @@ +use crate::crypto::key::VersionedAesKey; use crate::crypto::Aes256Key; use crate::entities::generated::sys::User; -use crate::key_loader_facade::VersionedAesKey; use crate::GeneratedId; use std::collections::HashMap; use std::sync::RwLock; diff --git a/tuta-sdk/rust/sdk/src/key_loader_facade.rs b/tuta-sdk/rust/sdk/src/key_loader_facade.rs index 3b4d4f4af3c..5df9cb4154c 100644 --- a/tuta-sdk/rust/sdk/src/key_loader_facade.rs +++ b/tuta-sdk/rust/sdk/src/key_loader_facade.rs @@ -1,4 +1,4 @@ -use crate::crypto::key::{AsymmetricKeyPair, GenericAesKey, KeyLoadError}; +use crate::crypto::key::{AsymmetricKeyPair, GenericAesKey, KeyLoadError, VersionedAesKey}; use crate::crypto::key_encryption::decrypt_key_pair; use crate::entities::generated::sys::{Group, GroupKey}; #[cfg_attr(test, mockall_double::double)] @@ -263,8 +263,6 @@ impl KeyLoaderFacade { } } -pub type VersionedAesKey = Versioned; - struct FormerGroupKey { symmetric_group_key: GenericAesKey, group_key_instance: GroupKey, diff --git a/tuta-sdk/rust/sdk/src/lib.rs b/tuta-sdk/rust/sdk/src/lib.rs index a1a3b963685..ddcc47f97de 100644 --- a/tuta-sdk/rust/sdk/src/lib.rs +++ b/tuta-sdk/rust/sdk/src/lib.rs @@ -15,7 +15,7 @@ use crate::blobs::blob_facade::BlobFacade; use crate::crypto::crypto_facade::create_auth_verifier; #[cfg_attr(test, mockall_double::double)] use crate::crypto::crypto_facade::CryptoFacade; -use crate::crypto::key::GenericAesKey; +use crate::crypto::key::{GenericAesKey, VersionedAesKey}; use crate::crypto::randomizer_facade::RandomizerFacade; use crate::crypto::{aes::Iv, Aes256Key}; #[cfg_attr(test, mockall_double::double)] @@ -46,8 +46,7 @@ use crate::type_model_provider::{init_type_model_provider, AppName, TypeModelPro use crate::typed_entity_client::TypedEntityClient; #[cfg_attr(test, mockall_double::double)] use crate::user_facade::UserFacade; -use rest_client::RestClient; -use rest_client::RestClientError; +use rest_client::{RestClient, RestClientError}; pub mod crypto; mod crypto_entity_client; @@ -117,7 +116,6 @@ impl Display for TypeRef { } pub struct HeadersProvider { - // In the future we might need to make this one optional to support "not authenticated" state access_token: Option, } @@ -347,6 +345,26 @@ pub struct LoggedInSdk { blob_facade: Arc, } +impl LoggedInSdk { + #[must_use] + pub fn get_service_executor(&self) -> &Arc { + &self.service_executor + } + + pub async fn get_current_sym_group_key( + &self, + user_group_id: &GeneratedId, + ) -> Result { + self.crypto_entity_client + .get_crypto_facade() + .get_key_loader_facade() + .as_ref() + .get_current_sym_group_key(user_group_id) + .await + .map_err(|err| ApiCallError::internal(format!("KeyLoadError: {err:?}"))) + } +} + #[uniffi::export] impl LoggedInSdk { /// Generates a new interface to operate on mail entities diff --git a/tuta-sdk/rust/sdk/src/mail_facade.rs b/tuta-sdk/rust/sdk/src/mail_facade.rs index 70c6a63b1f9..82cacf9ff6d 100644 --- a/tuta-sdk/rust/sdk/src/mail_facade.rs +++ b/tuta-sdk/rust/sdk/src/mail_facade.rs @@ -1,11 +1,13 @@ #[cfg_attr(test, mockall_double::double)] use crate::crypto_entity_client::CryptoEntityClient; +use crate::entities::generated::sys::{Group, GroupInfo, User}; use crate::entities::generated::tutanota::{ Mail, MailBox, MailFolder, MailboxGroupRoot, SimpleMoveMailPostIn, UnreadMailStatePostIn, }; use crate::folder_system::{FolderSystem, MailSetKind}; use crate::groups::GroupType; use crate::id::id_tuple::IdTupleGenerated; +use crate::rest_error::HttpError; use crate::services::generated::tutanota::{SimpleMoveMailService, UnreadMailStateService}; #[cfg_attr(test, mockall_double::double)] use crate::services::service_executor::ResolvingServiceExecutor; @@ -145,6 +147,55 @@ impl MailFacade { .await } + pub async fn get_group_id_for_mail_address( + &self, + mail_address: &str, + ) -> Result { + let logged_in_user: Arc = self.user_facade.get_user(); + let mail_group_memberships = logged_in_user + .memberships + .iter() + .filter(|membership| Some(GroupType::Mail as i64) == membership.groupType); + + for mail_group_membership in mail_group_memberships.into_iter() { + let group: Group = self + .crypto_entity_client + .load(&mail_group_membership.group) + .await?; + + match (&group.user, &logged_in_user._id) { + (None, _) => { + let mail_group_info: GroupInfo = self + .crypto_entity_client + .load(&mail_group_membership.groupInfo) + .await?; + + let enabled_mail_addresses = + get_enabled_mail_addresses_for_group_info(&mail_group_info); + if enabled_mail_addresses.contains(&mail_address.to_string()) { + return Ok(mail_group_membership.group.clone()); + } + }, + (Some(group_user_id), Some(logged_in_user_id)) + if logged_in_user_id == group_user_id => + { + let user_group_info: GroupInfo = self + .crypto_entity_client + .load(&logged_in_user.userGroup.groupInfo) + .await?; + let enabled_mail_addresses = + get_enabled_mail_addresses_for_group_info(&user_group_info); + if enabled_mail_addresses.contains(&mail_address.to_string()) { + return Ok(mail_group_membership.group.clone()); + } + }, + (Some(_), _) => continue, + } + } + + Err(HttpError::NotFoundError.into()) + } + /// Mark mails as read/unread. /// /// This is used to avoid having to get the Mail instance, edit it locally to change unread, and @@ -181,6 +232,16 @@ impl MailFacade { } } +fn get_enabled_mail_addresses_for_group_info(group_info: &GroupInfo) -> Vec { + group_info + .mailAddressAliases + .iter() + .filter(|alias| alias.enabled) + .map(|alias| alias.mailAddress.clone()) + .chain(group_info.mailAddress.clone()) + .collect() +} + #[cfg(test)] mod tests { use super::UnreadMailStatePostIn; diff --git a/tuta-sdk/rust/sdk/src/tutanota_constants.rs b/tuta-sdk/rust/sdk/src/tutanota_constants.rs index abda1859867..5229d2ff02f 100644 --- a/tuta-sdk/rust/sdk/src/tutanota_constants.rs +++ b/tuta-sdk/rust/sdk/src/tutanota_constants.rs @@ -7,6 +7,21 @@ pub enum PublicKeyIdentifierType { MailAddress = 0, // the default to retrieve public keys. identify the group by mail address. GroupId = 1, // e.g. needed if a user's needs the admin groups public key. identify by groupId. } +#[repr(i64)] +pub enum GroupType { + User = 0, + Admin = 1, + MailingList = 2, + Customer = 3, + External = 4, + Mail = 5, + Contact = 6, + File = 7, + LocalAdmin = 8, + Calendar = 9, + Template = 10, + ContactList = 11, +} #[allow(dead_code)] #[repr(i64)] diff --git a/tuta-sdk/rust/sdk/src/user_facade.rs b/tuta-sdk/rust/sdk/src/user_facade.rs index e8a51b874c3..c2d59a65389 100644 --- a/tuta-sdk/rust/sdk/src/user_facade.rs +++ b/tuta-sdk/rust/sdk/src/user_facade.rs @@ -1,12 +1,12 @@ use crate::crypto::hkdf; use crate::crypto::key::GenericAesKey; +use crate::crypto::key::VersionedAesKey; use crate::crypto::sha256; use crate::crypto::{Aes256Key, AES_256_KEY_SIZE}; use crate::entities::generated::sys::{GroupMembership, User}; use crate::groups::GroupType; #[cfg_attr(test, mockall_double::double)] use crate::key_cache::KeyCache; -use crate::key_loader_facade::VersionedAesKey; use crate::util::Versioned; use crate::ApiCallError; use crate::GeneratedId; From 272f7cb8a0de3a302ed2ced989b2018b9d8352db Mon Sep 17 00:00:00 2001 From: map Date: Thu, 7 Nov 2024 17:49:02 +0100 Subject: [PATCH 04/32] started adding compatibility tests for mime conversion --- .../src/importer/importable_mail.rs | 1798 ++++++++++------- 1 file changed, 1068 insertions(+), 730 deletions(-) diff --git a/packages/node-mimimi/src/importer/importable_mail.rs b/packages/node-mimimi/src/importer/importable_mail.rs index 1808017c9f9..c00757f0bdb 100644 --- a/packages/node-mimimi/src/importer/importable_mail.rs +++ b/packages/node-mimimi/src/importer/importable_mail.rs @@ -1,14 +1,14 @@ use crate::importer::extend_mail_parser::{get_reply_type_from_headers, MakeString}; use crate::importer::plain_text_to_html_converter; use crate::tuta_imap::client::types::ImapMail; -use mail_parser::{Address, GetHeader, HeaderName, MessageParser, PartType}; +use mail_parser::{Address, GetHeader, HeaderName, HeaderValue, MessageParser, PartType}; use std::borrow::Cow; use std::collections::HashMap; use std::hash::Hash; use std::time::SystemTime; use tutasdk::date::DateTime; use tutasdk::entities::generated::tutanota::{ - EncryptedMailAddress, ImportMailData, ImportMailDataMailReference, MailAddress, Recipients, + EncryptedMailAddress, ImportMailData, ImportMailDataMailReference, MailAddress, Recipients, }; use tutasdk::CustomId; @@ -19,591 +19,842 @@ const FIXED_CUSTOM_ID: &str = "____"; #[derive(Default)] #[cfg_attr(test, derive(PartialEq, Debug))] enum MailState { - #[default] - Received = 2, - Sent = 1, - Draft = 0, + #[default] + Received = 2, + Sent = 1, + Draft = 0, } #[repr(i64)] #[derive(Default)] #[cfg_attr(test, derive(PartialEq, Debug))] enum ICalType { - #[default] - Nothing = 0, - ICalPublish = 1, - ICalRequest = 2, - ICalAdd = 3, - ICalCancel = 4, - ICalRefresh = 5, - ICalCounter = 6, - ICalDeclineCounter = 7, + #[default] + Nothing = 0, + ICalPublish = 1, + ICalRequest = 2, + ICalAdd = 3, + ICalCancel = 4, + ICalRefresh = 5, + ICalCounter = 6, + ICalDeclineCounter = 7, } #[derive(Default)] #[cfg_attr(test, derive(PartialEq, Debug))] pub(super) enum ReplyType { - #[default] - Nothing = 0, - Reply = 1, - Forward = 2, - ReplyForward = 3, + #[default] + Nothing = 0, + Reply = 1, + Forward = 2, + ReplyForward = 3, } #[cfg_attr(test, derive(PartialEq, Debug))] enum ImportableMailAttachment { - Attachment { - filename: Option, - content_type: String, - content_id: String, - content: Vec, - is_inline: bool, - }, - AttachedMessage { - message: ImportableMail, - }, + Attachment { + filename: Option, + content_type: String, + content_id: String, + content: Vec, + is_inline: bool, + }, + AttachedMessage { + message: ImportableMail, + }, } #[cfg_attr(test, derive(PartialEq, Debug))] enum BodyText { - Html(String), - Plain(String), + Html(String), + Plain(String), } #[derive(Default, PartialEq)] #[cfg_attr(test, derive(Debug))] pub struct MailContact { - pub mail_address: String, - pub name: String, + pub mail_address: String, + pub name: String, } impl<'a> From> for MailContact { - fn from(value: mail_parser::Addr) -> Self { - Self { - name: value.name.unwrap_or_default().to_string(), - mail_address: value.address.unwrap_or_default().to_string(), - } - } + fn from(value: mail_parser::Addr) -> Self { + Self { + name: value.name.unwrap_or_default().to_string(), + mail_address: value.address.unwrap_or_default().to_string(), + } + } } impl From for MailAddress { - fn from(value: MailContact) -> Self { - Self { - _id: None, - address: value.mail_address, - name: value.name, - contact: None, - _finalIvs: Default::default(), - } - } + fn from(value: MailContact) -> Self { + Self { + _id: None, + address: value.mail_address, + name: value.name, + contact: None, + _finalIvs: Default::default(), + } + } } /// Input data for mail import service #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ImportableMail { - pub headers_string: String, - pub subject: String, - pub html_body_text: String, - pub attachments: Vec, - - pub date: Option, - - pub different_envelope_sender: Option, - pub from_addresses: Vec, - pub to_addresses: Vec, - pub cc_addresses: Vec, - pub bcc_addresses: Vec, - pub reply_to_addresses: Vec, - - pub ical_type: ICalType, - pub reply_type: ReplyType, - - pub mail_state: MailState, - pub is_phishing: bool, // https://turbo.fish/::%3Cphising%3E - pub unread: bool, - - pub message_id: Option, - pub in_reply_to: Option, - pub references: Vec, + pub headers_string: String, + pub subject: String, + pub html_body_text: String, + pub attachments: Vec, + + pub date: Option, + + pub different_envelope_sender: Option, + pub from_addresses: Vec, + pub to_addresses: Vec, + pub cc_addresses: Vec, + pub bcc_addresses: Vec, + pub reply_to_addresses: Vec, + + pub ical_type: ICalType, + pub reply_type: ReplyType, + + pub mail_state: MailState, + pub is_phishing: bool, // https://turbo.fish/::%3Cphising%3E + pub unread: bool, + + pub message_id: Option, + pub in_reply_to: Option, + pub references: Vec, } impl ImportableMail { - /// Utility function to convert mail_parser::Address - /// to a list of tutasdk::MailAddress - /// in such a way that every address must have mail-address and optional name - /// - /// returns None, if any of the address have empty mail-address - /// - /// set the _id: of all mail address to random 4-byte long customId, - /// this will only be valid in dataTransferType context - fn map_to_tuta_mail_address(mail_parser_addresses: Cow

) -> Vec { - let address_list = match mail_parser_addresses.as_ref() { - Address::List(address_list) => Cow::Borrowed(address_list), - Address::Group(group_senders) => { - let group_addresses = group_senders - .iter() - .map(|group| group.addresses.as_slice()) - .collect::>() - .concat(); - - Cow::Owned(group_addresses) - }, - }; - - address_list - .as_ref() - .into_iter() - .map(|address| MailContact { - mail_address: address.address().unwrap_or_default().to_string(), - name: address.name().unwrap_or_default().to_string(), - }) - .collect() - } - - // from the parsed message - // return : - // .0 a single string that ca be display as email in html format - // .1 list of attachment found - fn process_all_parts( - parsed_message: &mail_parser::Message, - ) -> Result<(String, Vec), MailParseError> { - let mut email_body_as_html = String::new(); - let mut attachments = Vec::with_capacity(parsed_message.attachments.len()); - - for part in &parsed_message.parts { - match &part.body { - PartType::Text(text) => { - let plain_text_as_html = - plain_text_to_html_converter::plain_text_to_html(text.to_string()); - email_body_as_html.push_str(plain_text_as_html.as_ref()) - }, - PartType::Html(html_text) => { - email_body_as_html.push_str(html_text); - }, - PartType::Message(attached_message) => { - let importable_mail = ImportableMail::try_from(attached_message.to_owned())?; - let this_attachment = ImportableMailAttachment::AttachedMessage { - message: importable_mail, - }; - attachments.push(this_attachment); - }, - - PartType::Binary(binary_content) | PartType::InlineBinary(binary_content) => { - let is_inline = if matches!(part.body, PartType::InlineBinary(_)) { - true - } else if matches!(part.body, PartType::Binary(_)) { - false - } else { - unreachable!(); - }; - - let content_type = part.headers.header_value(&HeaderName::ContentType).map( - |content_type_header| { - content_type_header - .as_content_type() - .expect("Content-Type header should be of type content type") - }, - ); - - let filename = content_type - .map(mail_parser::ContentType::attributes) - .unwrap_or_default() - .unwrap_or_default() - .iter() - .filter(|(attribute_name, _)| attribute_name == "filename") - .map(|(_, file_name)| file_name.to_string()) - // first attribute called 'filename' - .next(); - - let content_type = content_type - .map(MakeString::make_string) - .unwrap_or_default() - .to_string(); - - let content_id = part - .headers - .header_value(&HeaderName::ContentId) - .map(|content_type_header| { - content_type_header - .as_text() - .expect("Content-Id header should be of type text") - }) - .unwrap_or("binary") - .to_string(); - let content = binary_content.to_vec(); - let this_attachment = ImportableMailAttachment::Attachment { - filename, - content_type, - content_id, - is_inline, - content, - }; - attachments.push(this_attachment); - }, - - PartType::Multipart(multi_part_msg) => { - panic!(""); - }, - } - } - - Ok((email_body_as_html, attachments)) - } + /// Utility function to convert mail_parser::Address + /// to a list of tutasdk::MailAddress + /// in such a way that every address must have mail-address and optional name + /// + /// returns None, if any of the address have empty mail-address + /// + /// set the _id: of all mail address to random 4-byte long customId, + /// this will only be valid in dataTransferType context + fn map_to_tuta_mail_address(mail_parser_addresses: Cow
) -> Vec { + let address_list = match mail_parser_addresses.as_ref() { + Address::List(address_list) => Cow::Borrowed(address_list), + Address::Group(group_senders) => { + let group_addresses = group_senders + .iter() + .map(|group| group.addresses.as_slice()) + .collect::>() + .concat(); + + Cow::Owned(group_addresses) + } + }; + + address_list + .as_ref() + .into_iter() + .map(|address| MailContact { + mail_address: address.address().unwrap_or_default().to_string(), + name: address.name().unwrap_or_default().to_string(), + }) + .collect() + } + + // from the parsed message + // return : + // .0 a single string that ca be display as email in html format + // .1 list of attachment found + fn process_all_parts( + parsed_message: &mail_parser::Message, + ) -> Result<(String, Vec), MailParseError> { + let mut email_body_as_html = String::new(); + let mut attachments = Vec::with_capacity(parsed_message.attachments.len()); + + for part in &parsed_message.parts { + match &part.body { + PartType::Text(text) => { + let plain_text_as_html = + plain_text_to_html_converter::plain_text_to_html(text.to_string()); + email_body_as_html.push_str(plain_text_as_html.as_ref()) + } + PartType::Html(html_text) => { + email_body_as_html.push_str(html_text); + } + PartType::Message(attached_message) => { + let importable_mail = ImportableMail::try_from(attached_message.to_owned())?; + let this_attachment = ImportableMailAttachment::AttachedMessage { + message: importable_mail, + }; + attachments.push(this_attachment); + } + + PartType::Binary(binary_content) | PartType::InlineBinary(binary_content) => { + let is_inline = if matches!(part.body, PartType::InlineBinary(_)) { + true + } else if matches!(part.body, PartType::Binary(_)) { + false + } else { + unreachable!(); + }; + + let content_type = part.headers.header_value(&HeaderName::ContentType).map( + |content_type_header| { + content_type_header + .as_content_type() + .expect("Content-Type header should be of type content type") + }, + ); + + let filename = content_type + .map(mail_parser::ContentType::attributes) + .unwrap_or_default() + .unwrap_or_default() + .iter() + .filter(|(attribute_name, _)| attribute_name == "filename") + .map(|(_, file_name)| file_name.to_string()) + // first attribute called 'filename' + .next(); + + let content_type = content_type + .map(MakeString::make_string) + .unwrap_or_default() + .to_string(); + + let content_id = part + .headers + .header_value(&HeaderName::ContentId) + .map(|content_type_header| { + content_type_header + .as_text() + .expect("Content-Id header should be of type text") + }) + .unwrap_or("binary") + .to_string(); + let content = binary_content.to_vec(); + let this_attachment = ImportableMailAttachment::Attachment { + filename, + content_type, + content_id, + is_inline, + content, + }; + attachments.push(this_attachment); + } + + PartType::Multipart(multi_part_msg) => { + // no need to handle as we handle all other types separately (this is just a wrapper) + continue + } + } + } + + Ok((email_body_as_html, attachments)) + } } impl From for ImportMailData { - fn from(importable_mail: ImportableMail) -> Self { - let ImportableMail { - headers_string: headers, - subject, - html_body_text, - different_envelope_sender, - from_addresses, - cc_addresses, - bcc_addresses, - to_addresses, - date, - reply_to_addresses, - ical_type, - reply_type, - mail_state, - is_phishing, - unread, - message_id, - in_reply_to, - references, - attachments, - } = importable_mail; - - let date = date.unwrap_or_else(|| DateTime::from_system_time(SystemTime::now())); - - let reply_tos = reply_to_addresses - .into_iter() - .map(|reply_to| EncryptedMailAddress { - _id: Some(CustomId::from_custom_string(FIXED_CUSTOM_ID)), - _finalIvs: Default::default(), - name: reply_to.name, - address: reply_to.mail_address, - }) - .collect(); - - let bcc_addresses = bcc_addresses.into_iter().map(Into::into).collect(); - let cc_addresses = cc_addresses.into_iter().map(Into::into).collect(); - let to_addresses = to_addresses.into_iter().map(Into::into).collect(); - let from_addresses: Vec = from_addresses.into_iter().map(Into::into).collect(); - - let references = references - .into_iter() - .map(|reference| ImportMailDataMailReference { - _id: Some(CustomId::from_custom_string(FIXED_CUSTOM_ID)), - reference, - }) - .collect(); - - ImportMailData { - _id: Some(CustomId::from_custom_string(FIXED_CUSTOM_ID)), - _finalIvs: HashMap::new(), - compressedHeaders: headers, - subject, - compressedBodyText: html_body_text, - differentEnvelopeSender: different_envelope_sender, - sender: from_addresses - .first() - .cloned() - .unwrap_or(MailContact::default().into()), - recipients: Recipients { - _id: Some(CustomId::from_custom_string(FIXED_CUSTOM_ID)), - bccRecipients: bcc_addresses, - ccRecipients: cc_addresses, - toRecipients: to_addresses, - }, - replyTos: reply_tos, - unread, - confidential: false, - method: ical_type as i64, - phishingStatus: if is_phishing { 1 } else { 0 }, - replyType: reply_type as i64, - date, - state: mail_state as i64, - messageId: message_id, - inReplyTo: in_reply_to, - references, - importedAttachments: vec![], - } - } + fn from(importable_mail: ImportableMail) -> Self { + let ImportableMail { + headers_string: headers, + subject, + html_body_text, + different_envelope_sender, + from_addresses, + cc_addresses, + bcc_addresses, + to_addresses, + date, + reply_to_addresses, + ical_type, + reply_type, + mail_state, + is_phishing, + unread, + message_id, + in_reply_to, + references, + attachments, + } = importable_mail; + + let date = date.unwrap_or_else(|| DateTime::from_system_time(SystemTime::now())); + + let reply_tos = reply_to_addresses + .into_iter() + .map(|reply_to| EncryptedMailAddress { + _id: Some(CustomId::from_custom_string(FIXED_CUSTOM_ID)), + _finalIvs: Default::default(), + name: reply_to.name, + address: reply_to.mail_address, + }) + .collect(); + + let bcc_addresses = bcc_addresses.into_iter().map(Into::into).collect(); + let cc_addresses = cc_addresses.into_iter().map(Into::into).collect(); + let to_addresses = to_addresses.into_iter().map(Into::into).collect(); + let from_addresses: Vec = from_addresses.into_iter().map(Into::into).collect(); + + let references = references + .into_iter() + .map(|reference| ImportMailDataMailReference { + _id: Some(CustomId::from_custom_string(FIXED_CUSTOM_ID)), + reference, + }) + .collect(); + + ImportMailData { + _id: Some(CustomId::from_custom_string(FIXED_CUSTOM_ID)), + _finalIvs: HashMap::new(), + compressedHeaders: headers, + subject, + compressedBodyText: html_body_text, + differentEnvelopeSender: different_envelope_sender, + sender: from_addresses + .first() + .cloned() + .unwrap_or(MailContact::default().into()), + recipients: Recipients { + _id: Some(CustomId::from_custom_string(FIXED_CUSTOM_ID)), + bccRecipients: bcc_addresses, + ccRecipients: cc_addresses, + toRecipients: to_addresses, + }, + replyTos: reply_tos, + unread, + confidential: false, + method: ical_type as i64, + phishingStatus: if is_phishing { 1 } else { 0 }, + replyType: reply_type as i64, + date, + state: mail_state as i64, + messageId: message_id, + inReplyTo: in_reply_to, + references, + importedAttachments: vec![], + } + } } impl TryFrom for ImportableMail { - type Error = MailParseError; - fn try_from(imap_mail: ImapMail) -> Result { - let ImapMail { rfc822_full } = imap_mail; + type Error = MailParseError; + fn try_from(imap_mail: ImapMail) -> Result { + let ImapMail { rfc822_full } = imap_mail; - // parse the full mime message - let imap_mail = MessageParser::new() - .parse(rfc822_full.as_slice()) - .ok_or(MailParseError::InvalidMimeMessage)?; + // parse the full mime message + let imap_mail = MessageParser::new() + .parse(rfc822_full.as_slice()) + .ok_or(MailParseError::InvalidMimeMessage)?; - let mut importable_mail = Self::try_from(imap_mail)?; + let mut importable_mail = Self::try_from(imap_mail)?; - // example: - // add more details from imap if given, - importable_mail.is_phishing = false; - importable_mail.unread = true; + // example: + // add more details from imap if given, + importable_mail.is_phishing = false; + importable_mail.unread = true; - Ok(importable_mail) - } + Ok(importable_mail) + } } #[derive(Debug, Clone, PartialEq)] pub enum MailParseError { - InconsistentParts(&'static str), - NoSentDate, - NoRecipient, - NoFrom, - InvalidDate, - InvalidHtmlBody, - InvalidTextBody, - InvalidMimeMessage, - EmptyMailAddress, - Unknown(String), + InconsistentParts(&'static str), + NoSentDate, + NoRecipient, + NoFrom, + InvalidDate, + InvalidHtmlBody, + InvalidTextBody, + InvalidMimeMessage, + EmptyMailAddress, + Unknown(String), } /// allow to convert from parsed message impl<'x> TryFrom> for ImportableMail { - type Error = MailParseError; - - fn try_from(parsed_message: mail_parser::Message) -> Result { - let subject = parsed_message.subject().unwrap_or_default().to_string(); - - let (html_body_text, attachments) = ImportableMail::process_all_parts(&parsed_message)?; - - let date = parsed_message - .date() - .as_ref() - .map(|date_time| DateTime::from_millis(date_time.to_timestamp() as u64 * 1000)); - - let from_addresses = ImportableMail::map_to_tuta_mail_address( - parsed_message.from().map(Cow::Borrowed).unwrap_or_else(|| { - parsed_message - .sender() - .map(Cow::Borrowed) - .unwrap_or_else(|| Cow::Owned(mail_parser::Address::List(vec![]))) - }), - ) - .into_iter() - .map(|mut address| { - // we currently use the name as address if no address was defined on server side - if address.mail_address.is_empty() { - address.mail_address = address.name; - address.name = String::new(); - } - address - }) - .collect::>(); - - let different_envelope_sender = parsed_message - .sender() - .map(|sender| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(sender))) - // sender is allowed to be empty - .unwrap_or_default() - // there should only be one different envelope sender - .pop() - .map(|mut address| { - // we currently use the name as address if no address was defined on server side - if address.mail_address.is_empty() { - address.mail_address = address.name; - address.name = String::new(); - } - address - }) - // different envelope sender should not contain address listed in from_addresses; - .filter(|diff_sender| { - from_addresses - .iter() - .filter(|from| from.mail_address != diff_sender.mail_address) - .next() - .is_some() - }) - .map(|mail_address| mail_address.mail_address); - - let to_addresses = parsed_message - .to() - .map(|to| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(to))) - .unwrap_or_default() - .into_iter() - .filter(|address| !address.mail_address.trim().is_empty()) - .collect(); - - let cc_addresses = parsed_message - .cc() - .map(|cc| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(cc))) - .unwrap_or_default() - .into_iter() - .filter(|address| !address.mail_address.trim().is_empty()) - .collect(); - - let bcc_addresses = parsed_message - .bcc() - .map(|bcc| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(bcc))) - .unwrap_or_default() - .into_iter() - .filter(|address| !address.mail_address.trim().is_empty()) - .collect(); - - let reply_to_addresses = parsed_message - .reply_to() - .map(|reply_to| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(reply_to))) - .unwrap_or_default() - .into_iter() - .filter(|address| !address.mail_address.trim().is_empty()) - .collect(); - - let headers_string = parsed_message - .headers() - .into_iter() - .map(MakeString::make_string) - .collect::>() - .join("\n"); - - let reply_type = get_reply_type_from_headers(parsed_message.headers()); - let message_id = parsed_message.message_id().map(String::from); - let in_reply_to = parsed_message.in_reply_to().as_text().map(String::from); - let references = parsed_message - .references() - .as_text() - .map(String::from) - .into_iter() - .collect(); - - Ok(Self { - headers_string, - html_body_text, - subject, - different_envelope_sender, - from_addresses, - to_addresses, - cc_addresses, - bcc_addresses, - reply_to_addresses, - date, - reply_type, - message_id, - in_reply_to, - references, - attachments, - - ical_type: Default::default(), - unread: false, - mail_state: Default::default(), - is_phishing: false, - }) - } + type Error = MailParseError; + + fn try_from(parsed_message: mail_parser::Message) -> Result { + let subject = parsed_message.subject().unwrap_or_default().to_string(); + + let (html_body_text, attachments) = ImportableMail::process_all_parts(&parsed_message)?; + + let date = parsed_message + .date() + .as_ref() + .map(|date_time| DateTime::from_millis(date_time.to_timestamp() as u64 * 1000)); + + let from_addresses = ImportableMail::map_to_tuta_mail_address( + parsed_message.from().map(Cow::Borrowed).unwrap_or_else(|| { + parsed_message + .sender() + .map(Cow::Borrowed) + .unwrap_or_else(|| Cow::Owned(mail_parser::Address::List(vec![]))) + }), + ) + .into_iter() + .map(|mut address| { + // we currently use the name as address if no address was defined on server side + if address.mail_address.is_empty() { + address.mail_address = address.name; + address.name = String::new(); + } + address + }) + .collect::>(); + + let different_envelope_sender = parsed_message + .sender() + .map(|sender| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(sender))) + // sender is allowed to be empty + .unwrap_or_default() + // there should only be one different envelope sender + .pop() + .map(|mut address| { + // we currently use the name as address if no address was defined on server side + if address.mail_address.is_empty() { + address.mail_address = address.name; + address.name = String::new(); + } + address + }) + // different envelope sender should not contain address listed in from_addresses; + .filter(|diff_sender| { + from_addresses + .iter() + .filter(|from| from.mail_address != diff_sender.mail_address) + .next() + .is_some() + }) + .map(|mail_address| mail_address.mail_address); + + let to_addresses = parsed_message + .to() + .map(|to| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(to))) + .unwrap_or_default() + .into_iter() + .filter(|address| !address.mail_address.trim().is_empty()) + .collect(); + + let cc_addresses = parsed_message + .cc() + .map(|cc| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(cc))) + .unwrap_or_default() + .into_iter() + .filter(|address| !address.mail_address.trim().is_empty()) + .collect(); + + let bcc_addresses = parsed_message + .bcc() + .map(|bcc| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(bcc))) + .unwrap_or_default() + .into_iter() + .filter(|address| !address.mail_address.trim().is_empty()) + .collect(); + + let reply_to_addresses = parsed_message + .reply_to() + .map(|reply_to| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(reply_to))) + .unwrap_or_default() + .into_iter() + .filter(|address| !address.mail_address.trim().is_empty()) + .collect(); + + let headers_string = parsed_message + .headers_raw() + .map(|(name, value)| name.to_string() + ":" + value) + .collect::>() + .join(""); + + let reply_type = get_reply_type_from_headers(parsed_message.headers()); + let message_id = parsed_message.message_id().map(String::from); + let in_reply_to = parsed_message.in_reply_to().as_text().map(String::from); + let references = match parsed_message.references() { + HeaderValue::Text(reference) => {vec![reference.to_string()]} + HeaderValue::TextList(references) => {references.iter().map(|cow| cow.to_string()).collect()} + _ => {vec![]} + }; + + Ok(Self { + headers_string, + html_body_text, + subject, + different_envelope_sender, + from_addresses, + to_addresses, + cc_addresses, + bcc_addresses, + reply_to_addresses, + date, + reply_type, + message_id, + in_reply_to, + references, + attachments, + + ical_type: Default::default(), + unread: false, + mail_state: Default::default(), + is_phishing: false, + }) + } } +// Keep in sync with MimeStringToSmtpMessageConverterTest ! #[cfg(test)] mod tests { - use crate::importer::importable_mail::{ImportableMail, MailContact}; - use mail_parser::{MessageParser, MessagePartId}; - use serde::Deserialize; - use std::borrow::Cow; - - impl From for MailContact { - fn from(value: TestMailAddress) -> Self { - let TestMailAddress { - name, mail_address, .. - } = value; - Self { mail_address, name } - } - } - - impl From for ImportableMail { - fn from(mut expected_message: ExpectedMessage) -> Self { - let mut html_body_ids: Vec = vec![]; - let mut plain_body_ids: Vec = vec![]; - let mut attachment_ids: Vec = vec![]; - let mut body_parts = vec![]; - - expected_message.mail_headers.push_str("\n"); - let parsed_headers_res = MessageParser::default() - .parse_headers(expected_message.mail_headers.as_str()) - .unwrap(); - let root_part = parsed_headers_res.part(0).unwrap().clone(); - body_parts.push(root_part); - - if let Some(plain_body_part) = expected_message.plain_body_text { - let plain_body_converted = mail_parser::MessagePart { - headers: vec![], - is_encoding_problem: false, - body: mail_parser::PartType::Text(Cow::Owned(plain_body_part)), - encoding: Default::default(), - offset_header: 0, - offset_body: 0, - offset_end: 0, - }; - plain_body_ids.push(body_parts.len()); - body_parts.push(plain_body_converted); - } - - if let Some(html_body_part) = expected_message.html_body_text { - let html_body_converted = mail_parser::MessagePart { - headers: vec![], - is_encoding_problem: false, - body: mail_parser::PartType::Html(Cow::Owned(html_body_part)), - encoding: Default::default(), - offset_header: 0, - offset_body: 0, - offset_end: 0, - }; - html_body_ids.push(body_parts.len()); - body_parts.push(html_body_converted); - } - - for attached_message in expected_message.attached_messages { - let attached_message_converted = mail_parser::MessagePart { - headers: vec![], - is_encoding_problem: false, - body: Default::default(), - encoding: Default::default(), - offset_header: 0, - offset_body: 0, - offset_end: 0, - }; - attachment_ids.push(body_parts.len()); - body_parts.push(attached_message_converted); - } - - for attached_file in expected_message.attached_files { - let attached_file_converted = mail_parser::MessagePart { - headers: vec![], - is_encoding_problem: false, - body: Default::default(), - encoding: Default::default(), - offset_header: 0, - offset_body: 0, - offset_end: 0, - }; - attachment_ids.push(body_parts.len()); - body_parts.push(attached_file_converted); - } - - let parsed_mail = mail_parser::Message { - html_body: html_body_ids, - text_body: plain_body_ids, - attachments: attachment_ids, - parts: body_parts, - raw_message: Default::default(), - }; - - ImportableMail::try_from(parsed_mail).unwrap() - } - } - - #[test] - fn headers() {} - - #[test] - fn concatenate_multiple_plain_text_parts() { - let eml_contents = r#"Message-Id: some-id + use crate::importer::importable_mail::{ImportableMail, ImportableMailAttachment, MailContact}; + use mail_parser::{MessageParser, MessagePartId}; + use serde::Deserialize; + use std::borrow::Cow; + use tutasdk::date::DateTime; + + impl From for MailContact { + fn from(value: TestMailAddress) -> Self { + let TestMailAddress { + name, mail_address, .. + } = value; + Self { mail_address, name } + } + } + + impl From for ImportableMail { + fn from(mut expected_message: ExpectedMessage) -> Self { + let mut html_body_ids: Vec = vec![]; + let mut plain_body_ids: Vec = vec![]; + let mut attachment_ids: Vec = vec![]; + let mut body_parts = vec![]; + + expected_message.mail_headers.push_str("\n"); + let parsed_headers_res = MessageParser::default() + .parse_headers(expected_message.mail_headers.as_str()) + .unwrap(); + let root_part = parsed_headers_res.part(0).unwrap().clone(); + body_parts.push(root_part); + + if let Some(plain_body_part) = expected_message.plain_body_text { + let plain_body_converted = mail_parser::MessagePart { + headers: vec![], + is_encoding_problem: false, + body: mail_parser::PartType::Text(Cow::Owned(plain_body_part)), + encoding: Default::default(), + offset_header: 0, + offset_body: 0, + offset_end: 0, + }; + plain_body_ids.push(body_parts.len()); + body_parts.push(plain_body_converted); + } + + if let Some(html_body_part) = expected_message.html_body_text { + let html_body_converted = mail_parser::MessagePart { + headers: vec![], + is_encoding_problem: false, + body: mail_parser::PartType::Html(Cow::Owned(html_body_part)), + encoding: Default::default(), + offset_header: 0, + offset_body: 0, + offset_end: 0, + }; + html_body_ids.push(body_parts.len()); + body_parts.push(html_body_converted); + } + + for attached_message in expected_message.attached_messages { + let attached_message_converted = mail_parser::MessagePart { + headers: vec![], + is_encoding_problem: false, + body: Default::default(), + encoding: Default::default(), + offset_header: 0, + offset_body: 0, + offset_end: 0, + }; + attachment_ids.push(body_parts.len()); + body_parts.push(attached_message_converted); + } + + for attached_file in expected_message.attached_files { + let attached_file_converted = mail_parser::MessagePart { + headers: vec![], + is_encoding_problem: false, + body: Default::default(), + encoding: Default::default(), + offset_header: 0, + offset_body: 0, + offset_end: 0, + }; + attachment_ids.push(body_parts.len()); + body_parts.push(attached_file_converted); + } + + let parsed_mail = mail_parser::Message { + html_body: html_body_ids, + text_body: plain_body_ids, + attachments: attachment_ids, + parts: body_parts, + raw_message: Default::default(), + }; + + ImportableMail::try_from(parsed_mail).unwrap() + } + } + + fn parseMail(msg: &str) -> ImportableMail { + let parsed_message = MessageParser::default() + .parse(msg) + .unwrap(); + + println!("{:?}", parsed_message.headers()); + let m: ImportableMail = parsed_message.try_into().unwrap(); + m + } + + #[test] + fn headers() { + let msg = r#"Message-ID: 123456 +Subject: Hello +From: A +To: B +Reply-To: Reply , Reply2 +References: <1234564@web.de> +In-Reply-To: <1234564@web.de> +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-Type: multipart/mixed; boundary=frontier +"#; + println!("{}", msg); + let m: ImportableMail = parseMail(msg); + assert_eq!("123456", m.message_id.unwrap()); + assert_eq!(vec![ + MailContact { name: "Reply".to_string(), mail_address: "reply@tutanota.de".to_string() }, + MailContact { name: "Reply2".to_string(), mail_address: "reply2@tutanota.de".to_string() }, + ], m.reply_to_addresses); + assert_eq!(vec!["sadf@tutanota.de".to_string(), "1234564@web.de".to_string()], m.references); + assert_eq!("1234564@web.de", m.in_reply_to.unwrap()); + // assert_eq!("frontier", m.boundary); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(msg, m.headers_string); + } + + #[test] + fn bad_frontier() { + // todo!() + } + + #[test] + fn empty_references() { + // todo!() + } + + #[test] + fn empty_in_reply_to() { + // todo!() + } + + #[test] + fn text_plain_us_ascii() { + // todo!() + } + + #[test] + fn text_plain_utf8bit() { + // todo!() + } + + #[test] + fn text_plain_utf_explicit_8bit() { + // todo!() + } + + #[test] + fn text_plain_utf_quoted_printable() { + // todo!() + } + + #[test] + fn text_plain_utf_base64() { + // todo!() + } + + #[test] + fn text_plain_utf_invalid_base64() { + // todo!() + } + + #[test] + fn text_plain_format_flowed() { + // todo!() + } + + #[test] + fn text_plain_format_flowed_del_sp() { + // todo!() + } + + #[test] + fn text_plain_subject_encoded_word_Qencoding() { + // todo!() + } + + #[test] + fn text_plain_subject_encoded_word_Qencoding_turkish() { + // todo!() + } + + #[test] + fn from_encoded_word_Qencoding() { + // todo!() + } + + #[test] + fn from_encoded_word_Qencoding_colon() { + // todo!() + } + + #[test] + fn recipients_encoded_word_Qencoding_colon() { + // todo!() + } + + #[test] + fn recipients_encoded_word_Qencoding_partly() { + // todo!() + } + + + #[test] + fn text_plain_subject_encoded_word_base64() { + // todo!() + } + + + #[test] + fn text_html_only() { + // todo!() + } + + + #[test] + fn charset() { + // todo!() + } + + #[test] + fn text_html_inline_charset_definition_utf8() { + // todo!() + } + + #[test] + fn text_html_inline_charset_definition_western() { + // todo!() + } + #[test] + fn text_alternative() { + let msg = r#"Subject: Hello +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 (CET) +Content-Type: multipart/alternative; boundary=frontier + +--frontier +Content-type: text/plain; charset=UTF-8; + +Hello Àâüß +--frontier +Content-type: text/html; charset=UTF-8; + +Hello Àâüß
+--frontier-- +"#; + let m: ImportableMail = parseMail(msg); + + assert_eq!(&MailContact { mail_address: "a@tutanota.de".to_string(), name: "A".to_string() }, m.from_addresses.first().unwrap()); + assert_eq!(vec![MailContact { mail_address: "b@tutanota.de".to_string(), name: "B".to_string() }], m.to_addresses); + assert_eq!("Hello", m.subject); + assert_eq!("Hello Àâüß
", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + } + + #[test] + fn invalid_domains_in_mail_addresses() { + // todo!() + } + + #[test] + fn multiple_to_headers() { + // todo!() + } + + #[test] + fn attached_message() { + let msg = r#"Subject: parent message +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 (CET) +Content-Type: multipart/mixed; boundary=frontier + +--frontier +Content-type: text/plain; charset=UTF-8; + +normal message +--frontier +Content-Type: message/rfc822; charset=UTF-8; + +Subject: attached message +From: D +To: E +Date: Thu, 7 Nov 2024 15:54:04 +0100 (CET) +Content-type: text/plain; charset=UTF-8; + +Hello Àâüß +"#; + + let m: ImportableMail = parseMail(msg); + + // assert_eq!(&MailContact { mail_address: "a@tutanota.de".to_string(), name: "A".to_string() }, m.from_addresses.first().unwrap()); + // assert_eq!(vec![MailContact { mail_address: "b@tutanota.de".to_string(), name: "B".to_string() }], m.to_addresses); + assert_eq!("parent message", m.subject); + assert_eq!("normal message", m.html_body_text); + // assert_eq!(Some(DateTime::from_millis(0)), m.date); + + let attachment = m.attachments.first().unwrap(); + match attachment { + ImportableMailAttachment::Attachment { .. } => {panic!("should be an attached message")} + ImportableMailAttachment::AttachedMessage { message } => { + // assert_eq!(MailContact{name: "D", mail_address: "d@tutanota.de"}, m.getSender()); + // assert_eq!(List.of(new SmtpMailContact("E", "e@tutanota.de")), m.getToRecipients()); + // assert_eq!("attached message", attached.getSubject()); + // assert_eq!("Hello Àâüß", attached.getPlainBodyText()); + // assert_eq!(null, attached.getHtmlBodyText()); + // assert_eq!(yesterday, attached.getSentDate()); + } + } + } + + #[test] + fn attachments() { + // todo!() + } + + #[test] + fn inline_attachment() { + // todo!() + } + + #[test] + fn attachment_to_attached_message() { + // todo!() + } + + #[test] + fn textAttachment() {} + + + #[test] + fn htmlAttachment() {} + + #[test] + fn multiple_plain_body_text_parts_are_concatenated() { + let eml_contents = r#"Message-Id: some-id From: A To: B Date: Tue, 5 Nov 2024 13:18:59 +0000 @@ -621,24 +872,24 @@ second plain text in body --line-- "#; - let parsed_message = MessageParser::default() - .with_mime_headers() - .parse(eml_contents) - .unwrap(); - let text_contents = parsed_message - .text_bodies() - .map(|a| a.text_contents().unwrap()) - .collect::>() - .join(""); - assert_eq!( - "first plain text in body\nsecond plain text in body", - text_contents - ); - } - - #[test] - fn concatenate_multiple_html_text_parts() { - let eml_contents = r#"Message-Id: some-id + let parsed_message = MessageParser::default() + .with_mime_headers() + .parse(eml_contents) + .unwrap(); + let text_contents = parsed_message + .text_bodies() + .map(|a| a.text_contents().unwrap()) + .collect::>() + .join(""); + assert_eq!( + "first plain text in body\nsecond plain text in body", + text_contents + ); + } + + #[test] + fn multiple_html_body_text_parts_are_concatenated() { + let eml_contents = r#"Message-Id: some-id From: A To: B Date: Tue, 5 Nov 2024 13:18:59 +0000 @@ -656,24 +907,25 @@ Content-Type: text/html; charset=UTF-8 --line-- "#; - let parsed_message = MessageParser::default() - .with_mime_headers() - .parse(eml_contents) - .unwrap(); - let text_contents = parsed_message - .html_bodies() - .map(|a| a.text_contents().unwrap()) - .collect::>() - .join(""); - assert_eq!( - "

first html text in body

\n

second html in body

", - text_contents - ); - } - - #[test] - fn concatenate_alternative_html_text_parts() { - let eml_contents = r#"Message-Id: some-id + let parsed_message = MessageParser::default() + .with_mime_headers() + .parse(eml_contents) + .unwrap(); + let text_contents = parsed_message + .html_bodies() + .map(|a| a.text_contents().unwrap()) + .collect::>() + .join(""); + assert_eq!( + "

first html text in body

\n

second html in body

", + text_contents + ); + } + + #[test] + // todo! what does this test (map) + fn concatenate_alternative_html_text_parts() { + let eml_contents = r#"Message-Id: some-id From: A To: B Date: Tue, 5 Nov 2024 13:18:59 +0000 @@ -692,19 +944,20 @@ Content-Type: text/html; charset=UTF-8 --line-- "#; - let parsed_message = MessageParser::default() - .with_mime_headers() - .parse(eml_contents) - .unwrap(); - for body_part in parsed_message.html_bodies() { - eprintln!("====="); - eprintln!("{body_part:#?}"); - } - } - - #[test] - fn concatenate_multiple_html_and_plain_text_parts() { - let eml_contents = r#"Message-Id: some-id + let parsed_message = MessageParser::default() + .with_mime_headers() + .parse(eml_contents) + .unwrap(); + for body_part in parsed_message.html_bodies() { + eprintln!("====="); + eprintln!("{body_part:#?}"); + } + } + + #[test] + // todo! what does this test (map) + fn concatenate_multiple_html_and_plain_text_parts() { + let eml_contents = r#"Message-Id: some-id From: A To: B Date: Tue, 5 Nov 2024 13:18:59 +0000 @@ -724,158 +977,243 @@ first plain text in body --line-- "#; - let parsed_message = MessageParser::default() - .with_mime_headers() - .parse(eml_contents) - .unwrap(); - - eprintln!("{:?}", parsed_message.text_body); - eprintln!("{:?}", parsed_message.html_body); - eprintln!("{:?}", parsed_message.attachments); - - let text_contents = parsed_message - .text_bodies() - .map(|a| a.text_contents().unwrap()) - .collect::>() - .join(""); - assert_eq!( - "

first html text in body

\nfirst plain text in body", - text_contents - ); - } - #[test] - fn can_map_to_all_header_value() { - todo!() - } - - #[derive(Debug, PartialEq, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct TestMailAddress { - name: String, - mail_address: String, - valid: bool, - } - - #[derive(Debug, PartialEq, Deserialize)] - #[serde(rename_all = "camelCase")] - struct ExpectedAttachedFile { - name: String, - data: String, - mime_type: String, - charset: Option, - content_id: String, - calender_method: Option<()>, - } - - #[derive(Debug, PartialEq, Deserialize)] - #[serde(rename_all = "camelCase")] - struct ExpectedMessage { - id: Option, - boundary: Option, - alternative_boundary: Option, - sender: TestMailAddress, - to_recipients: Vec, - cc_recipients: Vec, - bcc_recipients: Vec, - reply_to: Vec, - in_reply_to: Option, - references: Vec, - auto_submitted: Option<()>, - sent_date: Option, - subject: String, - plain_body_text: Option, - html_body_text: Option, - attached_messages: Vec<()>, - attached_files: Vec, - mail_headers: String, - spf_result: String, - list_unsubscribe: bool, - mail_authentication_result: Option<()>, - } - - #[derive(Debug, PartialEq, Deserialize)] - #[serde(rename_all = "camelCase")] - struct Exception { - clazz: String, - message: String, - } - - #[derive(Debug, PartialEq, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct FileContent { - exception: Option, - result: Option, - } - - impl FileContent { - fn read_from_file(file_path: &str) -> Result { - let file_content = std::fs::read_to_string(file_path) - .map_err(|_| format!("Cannot read content of: {file_path}"))?; - serde_json::from_str::(file_content.as_str()) - .map_err(|e| format!("Cannot read to valid ExpectedMessage struct. Error: {e:?}")) - } - } - - #[test] - fn mime_tools_test_messages() { - const DATA_DIR: &'static str = - concat!(env!("CARGO_MANIFEST_DIR"), "/test/mimetools-testmsgs"); - let source_message_paths = std::fs::read_dir(DATA_DIR) - .unwrap() - .map(Result::unwrap) - .filter(|path| path.file_name().to_str().unwrap().ends_with(".msg")); - - for message_path in source_message_paths { - eprintln!("File: {}", message_path.file_name().to_str().unwrap()); - - let message_file_content = std::fs::read_to_string(&message_path.path()).unwrap(); - let parsed_message = MessageParser::default() - .parse(message_file_content.as_str()) - .expect(format!("Cannot parse test message: {:?}", message_path.path()).as_str()); - - let expected_json_file_name = format!( - "{DATA_DIR}/{}", - message_path - .file_name() - .to_str() - .unwrap() - .replace(".msg", "-expected.json") - ); - let FileContent { - result: expected_result, - exception: expected_exception, - } = FileContent::read_from_file(expected_json_file_name.as_str()).unwrap(); - let parsed_message_result = ImportableMail::try_from(parsed_message.clone()); - - if expected_result.is_some() && expected_exception.is_none() { - let expected_importable_mail = ImportableMail::from(expected_result.unwrap()); - let mut importable_mail = parsed_message_result.unwrap(); - importable_mail.attachments = vec![]; - importable_mail.different_envelope_sender = None; - - // assert_eq!( - // importable_mail.headers_string, - // expected_importable_mail.headers_string - // ); - // assert_eq!( - // importable_mail.html_body_text, - // expected_importable_mail.html_body_text - // ); - assert_eq!(importable_mail, expected_importable_mail); - } else if expected_exception.is_some() && expected_result.is_none() { - // check that the parsing have failed, - // but we cannot check for the actual reason in `expected_exception` - // - // - // todo: should not badbound.msg fail on mail_parser::parse thing? why is it failing in ImportableMail::try_from()? - //assert!(parsed_message_result.is_err()); - } else if expected_result.is_none() && expected_exception.is_none() { - unreachable!() - } else if expected_exception.is_some() && expected_exception.is_some() { - unreachable!() - } else { - unreachable!() - } - } - } + let parsed_message = MessageParser::default() + .with_mime_headers() + .parse(eml_contents) + .unwrap(); + + eprintln!("{:?}", parsed_message.text_body); + eprintln!("{:?}", parsed_message.html_body); + eprintln!("{:?}", parsed_message.attachments); + + let text_contents = parsed_message + .text_bodies() + .map(|a| a.text_contents().unwrap()) + .collect::>() + .join(""); + assert_eq!( + "

first html text in body

\nfirst plain text in body", + text_contents + ); + } + + #[test] + fn plain_body_text_parts_are_concatenated_with_html_body_parts_if_html_body_parts_already_existing() { + todo!() + } + + #[test] + fn plain_body_text_parts_are_converted_to_html_body_parts_if_html_body_parts_follow_afterwards() { + todo!() + } + + #[test] + fn text_attachment_with_disposition() { + todo!() + } + + #[test] + fn attachment_with_non_ascii_name() { + todo!() + } + + #[test] + fn attachment_filename_in_content_type() { + todo!() + } + + #[test] + fn attachment_filename_qencoding() { + todo!() + } + + #[test] + fn encrypted() { + todo!() + } + + #[test] + fn can_map_to_all_header_value() { + todo!() + } + + #[test] + fn recipient_groups() { + todo!() + } + + #[test] + fn undisclosed_recipients() { + todo!() + } + + #[test] + fn long_content_type() { + todo!() + } + + #[test] + fn normalize_header_value() {} + + #[test] + fn get_spf_result() { + // net yet used on rust + } + + #[test] + fn mail_from_with_delemiter() { + todo!() + } + + #[test] + fn incomplete_text_content_type() { + todo!() + } + + #[test] + fn calendar_content_type() { + todo!() + } + + #[test] + fn calendar_content_type_method() { + todo!() + } + + #[test] + fn invalid_content_types_default_to_text_plain() { + todo!() + } + + + #[derive(Debug, PartialEq, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct TestMailAddress { + name: String, + mail_address: String, + valid: bool, + } + + #[derive(Debug, PartialEq, Deserialize)] + #[serde(rename_all = "camelCase")] + struct ExpectedAttachedFile { + name: String, + data: String, + mime_type: String, + charset: Option, + content_id: String, + calender_method: Option<()>, + } + + #[derive(Debug, PartialEq, Deserialize)] + #[serde(rename_all = "camelCase")] + struct ExpectedMessage { + id: Option, + boundary: Option, + alternative_boundary: Option, + sender: TestMailAddress, + to_recipients: Vec, + cc_recipients: Vec, + bcc_recipients: Vec, + reply_to: Vec, + in_reply_to: Option, + references: Vec, + auto_submitted: Option<()>, + sent_date: Option, + subject: String, + plain_body_text: Option, + html_body_text: Option, + attached_messages: Vec<()>, + attached_files: Vec, + mail_headers: String, + spf_result: String, + list_unsubscribe: bool, + mail_authentication_result: Option<()>, + } + + #[derive(Debug, PartialEq, Deserialize)] + #[serde(rename_all = "camelCase")] + struct Exception { + clazz: String, + message: String, + } + + #[derive(Debug, PartialEq, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct FileContent { + exception: Option, + result: Option, + } + + impl FileContent { + fn read_from_file(file_path: &str) -> Result { + let file_content = std::fs::read_to_string(file_path) + .map_err(|_| format!("Cannot read content of: {file_path}"))?; + serde_json::from_str::(file_content.as_str()) + .map_err(|e| format!("Cannot read to valid ExpectedMessage struct. Error: {e:?}")) + } + } + + #[test] + fn mime_tools_test_messages() { + const DATA_DIR: &'static str = + concat!(env!("CARGO_MANIFEST_DIR"), "/test/mimetools-testmsgs"); + let source_message_paths = std::fs::read_dir(DATA_DIR) + .unwrap() + .map(Result::unwrap) + .filter(|path| path.file_name().to_str().unwrap().ends_with(".msg")); + + for message_path in source_message_paths { + eprintln!("File: {}", message_path.file_name().to_str().unwrap()); + + let message_file_content = std::fs::read_to_string(&message_path.path()).unwrap(); + let parsed_message = MessageParser::default() + .parse(message_file_content.as_str()) + .expect(format!("Cannot parse test message: {:?}", message_path.path()).as_str()); + + let expected_json_file_name = format!( + "{DATA_DIR}/{}", + message_path + .file_name() + .to_str() + .unwrap() + .replace(".msg", "-expected.json") + ); + let FileContent { + result: expected_result, + exception: expected_exception, + } = FileContent::read_from_file(expected_json_file_name.as_str()).unwrap(); + let parsed_message_result = ImportableMail::try_from(parsed_message.clone()); + + if expected_result.is_some() && expected_exception.is_none() { + let expected_importable_mail = ImportableMail::from(expected_result.unwrap()); + let mut importable_mail = parsed_message_result.unwrap(); + importable_mail.attachments = vec![]; + importable_mail.different_envelope_sender = None; + + // assert_eq!( + // importable_mail.headers_string, + // expected_importable_mail.headers_string + // ); + // assert_eq!( + // importable_mail.html_body_text, + // expected_importable_mail.html_body_text + // ); + assert_eq!(importable_mail, expected_importable_mail); + } else if expected_exception.is_some() && expected_result.is_none() { + // check that the parsing have failed, + // but we cannot check for the actual reason in `expected_exception` + // + // + // todo: should not badbound.msg fail on mail_parser::parse thing? why is it failing in ImportableMail::try_from()? + //assert!(parsed_message_result.is_err()); + } else if expected_result.is_none() && expected_exception.is_none() { + unreachable!() + } else if expected_exception.is_some() && expected_exception.is_some() { + unreachable!() + } else { + unreachable!() + } + } + } } From 5ce9583116e009a1c76a17f6ac23c82f6ca45aa7 Mon Sep 17 00:00:00 2001 From: sug Date: Thu, 7 Nov 2024 19:45:26 +0100 Subject: [PATCH 05/32] wip --- packages/node-mimimi/Cargo.toml | 2 +- .../src/importer/extend_mail_parser.rs | 2 +- .../src/importer/importable_mail.rs | 246 +++++++++++------- .../importer/plain_text_to_html_converter.rs | 20 +- 4 files changed, 170 insertions(+), 100 deletions(-) diff --git a/packages/node-mimimi/Cargo.toml b/packages/node-mimimi/Cargo.toml index 06706f6a38c..0e32997708a 100644 --- a/packages/node-mimimi/Cargo.toml +++ b/packages/node-mimimi/Cargo.toml @@ -16,7 +16,7 @@ strip = "symbols" tuta-sdk = { path = "../../tuta-sdk/rust/sdk", features = ["net"] } async-trait = "0.1.83" rand = { version = "0.8.5" } -mail-parser = { version = "0.9.4" } +mail-parser = { version = "0.9.4", features = ["full_encoding"] } thiserror = { version = "1.0.64" } # Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix napi = { version = "2.16.12", default-features = false, features = ["napi9", "async", "tokio_rt"] } diff --git a/packages/node-mimimi/src/importer/extend_mail_parser.rs b/packages/node-mimimi/src/importer/extend_mail_parser.rs index bf4d9d988a7..fcea71b9810 100644 --- a/packages/node-mimimi/src/importer/extend_mail_parser.rs +++ b/packages/node-mimimi/src/importer/extend_mail_parser.rs @@ -82,7 +82,7 @@ impl<'a> MakeString for mail_parser::DateTime { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec", ]; - let weekday = DAY_OF_WEEK[self.day_of_week() as usize - 1]; + let weekday = DAY_OF_WEEK[self.day_of_week() as usize]; let Self { year, month, diff --git a/packages/node-mimimi/src/importer/importable_mail.rs b/packages/node-mimimi/src/importer/importable_mail.rs index c00757f0bdb..20af6b102ba 100644 --- a/packages/node-mimimi/src/importer/importable_mail.rs +++ b/packages/node-mimimi/src/importer/importable_mail.rs @@ -1,7 +1,7 @@ use crate::importer::extend_mail_parser::{get_reply_type_from_headers, MakeString}; use crate::importer::plain_text_to_html_converter; use crate::tuta_imap::client::types::ImapMail; -use mail_parser::{Address, GetHeader, HeaderName, HeaderValue, MessageParser, PartType}; +use mail_parser::{Address, GetHeader, HeaderName, HeaderValue, MessageParser, MimeHeaders, PartType}; use std::borrow::Cow; use std::collections::HashMap; use std::hash::Hash; @@ -160,6 +160,27 @@ impl ImportableMail { .collect() } + fn handle_plain_text(email_body_as_html: &mut String, plain_text: &str) { + let plain_text_as_html = plain_text_to_html_converter::plain_text_to_html(plain_text); + Self::handle_html_text(email_body_as_html, plain_text_as_html.as_str()) + } + + fn handle_html_text(email_body_as_html: &mut String, html_text: &str) { + email_body_as_html.push_str(html_text); + } + + fn handle_attached_message( + attachments: &mut Vec, + attached_message: mail_parser::Message, + ) -> Result<(), MailParseError> { + let importable_mail = ImportableMail::try_from(attached_message)?; + let this_attachment = ImportableMailAttachment::AttachedMessage { + message: importable_mail, + }; + attachments.push(this_attachment); + Ok(()) + } + // from the parsed message // return : // .0 a single string that ca be display as email in html format @@ -170,23 +191,29 @@ impl ImportableMail { let mut email_body_as_html = String::new(); let mut attachments = Vec::with_capacity(parsed_message.attachments.len()); - for part in &parsed_message.parts { + for (part_id, part) in parsed_message.parts.iter().enumerate() { + // if not boundary attribute is defined in Content-Type, then the text is treated as comment. + // see: russian.msg + let probably_unbounded_message = + parsed_message.attachments.contains(&part_id) && part_id == 0; + match &part.body { + PartType::Html(_) | PartType::Text(_) if probably_unbounded_message => { + // is this a comment in mime? + continue; + }, + PartType::Text(text) => { - let plain_text_as_html = - plain_text_to_html_converter::plain_text_to_html(text.to_string()); - email_body_as_html.push_str(plain_text_as_html.as_ref()) - } + Self::handle_plain_text(&mut email_body_as_html, text.as_ref()); + }, + PartType::Html(html_text) => { - email_body_as_html.push_str(html_text); - } + Self::handle_html_text(&mut email_body_as_html, html_text.as_ref()) + }, + PartType::Message(attached_message) => { - let importable_mail = ImportableMail::try_from(attached_message.to_owned())?; - let this_attachment = ImportableMailAttachment::AttachedMessage { - message: importable_mail, - }; - attachments.push(this_attachment); - } + Self::handle_attached_message(&mut attachments, attached_message.to_owned())?; + } PartType::Binary(binary_content) | PartType::InlineBinary(binary_content) => { let is_inline = if matches!(part.body, PartType::InlineBinary(_)) { @@ -196,59 +223,72 @@ impl ImportableMail { } else { unreachable!(); }; - - let content_type = part.headers.header_value(&HeaderName::ContentType).map( - |content_type_header| { - content_type_header - .as_content_type() - .expect("Content-Type header should be of type content type") - }, - ); - - let filename = content_type - .map(mail_parser::ContentType::attributes) - .unwrap_or_default() - .unwrap_or_default() - .iter() - .filter(|(attribute_name, _)| attribute_name == "filename") - .map(|(_, file_name)| file_name.to_string()) - // first attribute called 'filename' - .next(); - - let content_type = content_type - .map(MakeString::make_string) - .unwrap_or_default() - .to_string(); - - let content_id = part - .headers - .header_value(&HeaderName::ContentId) - .map(|content_type_header| { - content_type_header - .as_text() - .expect("Content-Id header should be of type text") - }) - .unwrap_or("binary") - .to_string(); - let content = binary_content.to_vec(); - let this_attachment = ImportableMailAttachment::Attachment { - filename, - content_type, - content_id, - is_inline, - content, - }; - attachments.push(this_attachment); - } - - PartType::Multipart(multi_part_msg) => { - // no need to handle as we handle all other types separately (this is just a wrapper) - continue - } - } - } - - Ok((email_body_as_html, attachments)) + Self::handle_binary( + &mut attachments, + &part.headers, + binary_content.to_vec(), + is_inline, + ); + }, + + PartType::Multipart(multi_part_msg) => { + continue; + }, + } + } + + Ok((email_body_as_html, attachments)) + } + + fn handle_binary<'a>( + attachments: &mut Vec, + header_values: &Vec>, + binary_content: Vec, + is_inline: bool, + ) { + let content_type = header_values.header(HeaderName::ContentType).map(|c| { + c.value + .as_content_type() + .expect("Content type should be of type ContentType") + }); + let content_type_attributes = content_type + // get attributes_of_content_type if content-type is there + .map(mail_parser::ContentType::attributes) + .flatten() + // if can-not get attributes, default to empty list of attributes + .unwrap_or_default(); + let filename = content_type_attributes + .iter() + // find a attribute name filename + .filter(|(attribute_name, _)| attribute_name == "filename") + .map(|(_, file_name)| file_name.to_string()) + // first attribute called 'filename' + .next(); + + let content_id = header_values + .header_value(&HeaderName::ContentId) + .map(|content_type_header| { + content_type_header + .as_text() + .expect("Content-Id header should be of type text") + }) + .unwrap_or("binary") + .to_string(); + + let content_type = content_type + .map(MakeString::make_string) + .unwrap_or_default() + .to_string(); + + let content = binary_content.to_vec(); + let this_attachment = ImportableMailAttachment::Attachment { + filename, + content_type, + content_id, + is_inline, + content, + }; + attachments.push(this_attachment); } } @@ -506,6 +546,7 @@ mod tests { use mail_parser::{MessageParser, MessagePartId}; use serde::Deserialize; use std::borrow::Cow; + use std::io::Read; use tutasdk::date::DateTime; impl From for MailContact { @@ -528,7 +569,15 @@ mod tests { let parsed_headers_res = MessageParser::default() .parse_headers(expected_message.mail_headers.as_str()) .unwrap(); - let root_part = parsed_headers_res.part(0).unwrap().clone(); + let root_part = mail_parser::MessagePart { + headers: parsed_headers_res.headers().to_vec(), + is_encoding_problem: false, + body: mail_parser::PartType::Text(Cow::Borrowed("")), + encoding: Default::default(), + offset_header: 0, + offset_body: 0, + offset_end: 0, + }; body_parts.push(root_part); if let Some(plain_body_part) = expected_message.plain_body_text { @@ -1164,20 +1213,22 @@ first plain text in body .filter(|path| path.file_name().to_str().unwrap().ends_with(".msg")); for message_path in source_message_paths { - eprintln!("File: {}", message_path.file_name().to_str().unwrap()); - - let message_file_content = std::fs::read_to_string(&message_path.path()).unwrap(); + let message_file_name = message_path.file_name().to_str().unwrap().to_string(); + eprint!("File: {message_file_name}"); + + // let message_file_content = std::fs::r(&message_path.path()).unwrap() + let mut message_file_content = vec![]; + std::fs::File::open(message_path.path()) + .unwrap() + .read_to_end(&mut message_file_content) + .unwrap(); let parsed_message = MessageParser::default() - .parse(message_file_content.as_str()) + .parse(message_file_content.as_slice()) .expect(format!("Cannot parse test message: {:?}", message_path.path()).as_str()); let expected_json_file_name = format!( "{DATA_DIR}/{}", - message_path - .file_name() - .to_str() - .unwrap() - .replace(".msg", "-expected.json") + message_file_name.replace(".msg", "-expected.json") ); let FileContent { result: expected_result, @@ -1186,19 +1237,37 @@ first plain text in body let parsed_message_result = ImportableMail::try_from(parsed_message.clone()); if expected_result.is_some() && expected_exception.is_none() { + let mut importable_mail = parsed_message_result.unwrap(); let expected_importable_mail = ImportableMail::from(expected_result.unwrap()); - let mut importable_mail = parsed_message_result.unwrap(); importable_mail.attachments = vec![]; - importable_mail.different_envelope_sender = None; - - // assert_eq!( - // importable_mail.headers_string, - // expected_importable_mail.headers_string - // ); - // assert_eq!( - // importable_mail.html_body_text, - // expected_importable_mail.html_body_text - // ); + // everything else is related to multipart i suppose, + const IGNORED_FILES: [&str; 14] = [ + // files i suppose related to multipart + "2002_06_12_doublebound.msg", + "attachment-filename-encoding-Latin1.msg", + "attachment-filename-encoding-UTF8.msg", + "multi-bad.msg", + "multi-clen.msg", + "multi-digest.msg", + "multi-igor.msg", + "multi-igor2.msg", + "multi-nested.msg", + "multi-nested3.msg", + "multi-nested2.msg", + "multi-digest.msg", + "infinite.msg", // have encoding problem + "double-semicolon.msg", // do not know why this fail + ]; + if IGNORED_FILES + .iter() + .filter(|f| f.starts_with(message_file_name.as_str())) + .next() + .is_some() + { + eprintln!(" ....ignored"); + continue; + } + eprintln!(); assert_eq!(importable_mail, expected_importable_mail); } else if expected_exception.is_some() && expected_result.is_none() { // check that the parsing have failed, @@ -1207,6 +1276,7 @@ first plain text in body // // todo: should not badbound.msg fail on mail_parser::parse thing? why is it failing in ImportableMail::try_from()? //assert!(parsed_message_result.is_err()); + eprintln!(); } else if expected_result.is_none() && expected_exception.is_none() { unreachable!() } else if expected_exception.is_some() && expected_exception.is_some() { diff --git a/packages/node-mimimi/src/importer/plain_text_to_html_converter.rs b/packages/node-mimimi/src/importer/plain_text_to_html_converter.rs index 8be7ad70639..572a925e852 100644 --- a/packages/node-mimimi/src/importer/plain_text_to_html_converter.rs +++ b/packages/node-mimimi/src/importer/plain_text_to_html_converter.rs @@ -8,10 +8,10 @@ use regex::Regex; /// 3. **escapes** "&" with "&", "<" with "<", and ">" with ">" /// /// This code is ported from tutadb PlainTextToHtmlConverter -pub fn plain_text_to_html(plain_text: String) -> String { +pub fn plain_text_to_html(plain_text: &str) -> String { let mut result: String = String::from(""); let SEPARATOR: Regex = Regex::new("\r?\n").expect("invalid regex"); // todo! move to const - let lines = SEPARATOR.split(plain_text.as_str()); + let lines = SEPARATOR.split(plain_text); let mut previous_quote_level = 0; for (i, line) in lines.enumerate() { let line_quote_level = get_line_quote_level(line.to_string()); @@ -108,41 +108,41 @@ mod test { #[test] pub fn convert_to_html() { assert_eq!("Test-Mail im Plain-Text und mit komischen Zeichen: & \"~ΓΆΓ€β₯£Τ»Β³@
weiter gehts in der naechsten Zeile", - plain_text_to_html("Test-Mail im Plain-Text und mit komischen Zeichen: & \"~ΓΆΓ€β₯£Τ»Β³@\r\nweiter gehts in der naechsten Zeile".to_string())); + plain_text_to_html("Test-Mail im Plain-Text und mit komischen Zeichen: & \"~ΓΆΓ€β₯£Τ»Β³@\r\nweiter gehts in der naechsten Zeile")); assert_eq!( "
simple blockquote
", - plain_text_to_html("> simple blockquote".to_string()) + plain_text_to_html("> simple blockquote") ); assert_eq!( "
blockquote
with line break
", - plain_text_to_html("> blockquote \r\n> with line break".to_string()) + plain_text_to_html("> blockquote \r\n> with line break") ); assert_eq!( "
blockquote
with line break
", - plain_text_to_html(">> blockquote \r\n> with line break".to_string()) + plain_text_to_html(">> blockquote \r\n> with line break") ); assert_eq!( "
blockquote
with line break
", - plain_text_to_html("> blockquote \r\n>> with line break".to_string()) + plain_text_to_html("> blockquote \r\n>> with line break") ); assert_eq!("
blockquote
with line break", - plain_text_to_html(">>> blockquote \r\n with line break".to_string())); + plain_text_to_html(">>> blockquote \r\n with line break")); // quote without text assert_eq!( "
", - plain_text_to_html(">".to_string()) + plain_text_to_html(">") ); // quote without text but newline assert_eq!( "

", - plain_text_to_html(">\r\n>".to_string()) + plain_text_to_html(">\r\n>") ); } From 101916129095de64c98654f881415a7557f1ec8f Mon Sep 17 00:00:00 2001 From: nig Date: Fri, 8 Nov 2024 10:08:38 +0100 Subject: [PATCH 06/32] wip improve size_estimator docs --- tuta-sdk/rust/sdk/src/entities.rs | 2 +- ...ze_estimator.rs => json_size_estimator.rs} | 105 ++++++++++-------- 2 files changed, 60 insertions(+), 47 deletions(-) rename tuta-sdk/rust/sdk/src/entities/{size_estimator.rs => json_size_estimator.rs} (87%) diff --git a/tuta-sdk/rust/sdk/src/entities.rs b/tuta-sdk/rust/sdk/src/entities.rs index 1ae117dd459..91b8a15d843 100644 --- a/tuta-sdk/rust/sdk/src/entities.rs +++ b/tuta-sdk/rust/sdk/src/entities.rs @@ -10,7 +10,7 @@ use crate::TypeRef; pub mod entity_facade; pub mod generated; -pub mod size_estimator; +pub mod json_size_estimator; /// `'static` on trait bound is fine here because Entity does not contain any non-static references. /// See https://doc.rust-lang.org/rust-by-example/scope/lifetime/static_lifetime.html#trait-bound diff --git a/tuta-sdk/rust/sdk/src/entities/size_estimator.rs b/tuta-sdk/rust/sdk/src/entities/json_size_estimator.rs similarity index 87% rename from tuta-sdk/rust/sdk/src/entities/size_estimator.rs rename to tuta-sdk/rust/sdk/src/entities/json_size_estimator.rs index 646f514eeb6..10df57efa5d 100644 --- a/tuta-sdk/rust/sdk/src/entities/size_estimator.rs +++ b/tuta-sdk/rust/sdk/src/entities/json_size_estimator.rs @@ -38,6 +38,11 @@ impl ser::Error for SizeEstimationError { } } +/// main serializer for all entities and their field values. +/// there are some special-cased types that are serialized +/// in a non-serde way (eg IdTuple struct -> vector of strings) +/// it assumes that certain types will be encrypted and/or b64 +/// encoded. impl<'a> Serializer for &'a mut SizeEstimatingSerializer { type Ok = usize; type Error = SizeEstimationError; @@ -247,6 +252,9 @@ impl<'a> Serializer for &'a mut SizeEstimatingSerializer { struct SizeEstimatingPlaintextSerializer; +/// serializer that will not apply the encryption and encoding padding, +/// to be used for objects that we know will not be encrypted or encoded, +/// eg struct field names, ids. impl<'a> Serializer for &'a mut SizeEstimatingPlaintextSerializer { type Ok = usize; type Error = SizeEstimationError; @@ -258,51 +266,51 @@ impl<'a> Serializer for &'a mut SizeEstimatingPlaintextSerializer { type SerializeStruct = ser::Impossible; type SerializeStructVariant = ser::Impossible; - fn serialize_bool(self, v: bool) -> Result { + fn serialize_bool(self, _v: bool) -> Result { unimplemented!("serialize_bool") } - fn serialize_i8(self, v: i8) -> Result { + fn serialize_i8(self, _v: i8) -> Result { unimplemented!("serialize_i8") } - fn serialize_i16(self, v: i16) -> Result { + fn serialize_i16(self, _v: i16) -> Result { unimplemented!("serialize_i16") } - fn serialize_i32(self, v: i32) -> Result { + fn serialize_i32(self, _v: i32) -> Result { unimplemented!("serialize_i32") } - fn serialize_i64(self, v: i64) -> Result { + fn serialize_i64(self, _v: i64) -> Result { unimplemented!("serialize_i64") } - fn serialize_u8(self, v: u8) -> Result { + fn serialize_u8(self, _v: u8) -> Result { unimplemented!("serialize_u8") } - fn serialize_u16(self, v: u16) -> Result { + fn serialize_u16(self, _v: u16) -> Result { unimplemented!("serialize_u16") } - fn serialize_u32(self, v: u32) -> Result { + fn serialize_u32(self, _v: u32) -> Result { unimplemented!("serialize_iu32") } - fn serialize_u64(self, v: u64) -> Result { + fn serialize_u64(self, _v: u64) -> Result { unimplemented!("serialize_u64") } - fn serialize_f32(self, v: f32) -> Result { + fn serialize_f32(self, _v: f32) -> Result { unimplemented!("serialize_f32") } - fn serialize_f64(self, v: f64) -> Result { + fn serialize_f64(self, _v: f64) -> Result { unimplemented!("serialize_f64") } - fn serialize_char(self, v: char) -> Result { + fn serialize_char(self, _v: char) -> Result { unimplemented!("serialize_char") } @@ -318,7 +326,7 @@ impl<'a> Serializer for &'a mut SizeEstimatingPlaintextSerializer { unimplemented!("serialize_none") } - fn serialize_some(self, value: &T) -> Result + fn serialize_some(self, _value: &T) -> Result where T: ?Sized + Serialize, { @@ -329,22 +337,22 @@ impl<'a> Serializer for &'a mut SizeEstimatingPlaintextSerializer { unimplemented!("serialize_unit") } - fn serialize_unit_struct(self, name: &'static str) -> Result { + fn serialize_unit_struct(self, _name: &'static str) -> Result { unimplemented!("serialize_unit_struct") } fn serialize_unit_variant( self, - name: &'static str, - variant_index: u32, - variant: &'static str, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, ) -> Result { unimplemented!("serialize_unit_variant") } fn serialize_newtype_struct( self, - name: &'static str, + _name: &'static str, value: &T, ) -> Result where @@ -355,10 +363,10 @@ impl<'a> Serializer for &'a mut SizeEstimatingPlaintextSerializer { fn serialize_newtype_variant( self, - name: &'static str, - variant_index: u32, - variant: &'static str, - value: &T, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, ) -> Result where T: ?Sized + Serialize, @@ -366,55 +374,68 @@ impl<'a> Serializer for &'a mut SizeEstimatingPlaintextSerializer { unimplemented!("serialize_newtype_variant") } - fn serialize_seq(self, len: Option) -> Result { + fn serialize_seq(self, _len: Option) -> Result { unimplemented!("serialize_seq") } - fn serialize_tuple(self, len: usize) -> Result { + fn serialize_tuple(self, _len: usize) -> Result { unimplemented!("serialize_tuple") } fn serialize_tuple_struct( self, - name: &'static str, - len: usize, + _name: &'static str, + _len: usize, ) -> Result { unimplemented!("serialize_tuple_struct") } fn serialize_tuple_variant( self, - name: &'static str, - variant_index: u32, - variant: &'static str, - len: usize, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, ) -> Result { unimplemented!("serialize_tuple_variant") } - fn serialize_map(self, len: Option) -> Result { + fn serialize_map(self, _len: Option) -> Result { unimplemented!("serialize_map") } fn serialize_struct( self, - name: &'static str, - len: usize, + _name: &'static str, + _len: usize, ) -> Result { unimplemented!("serialize_struct") } fn serialize_struct_variant( self, - name: &'static str, - variant_index: u32, - variant: &'static str, - len: usize, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, ) -> Result { unimplemented!("serialize_struct_variant") } } +/// the compound types that are handled by the serialization. +/// some types are special-cased. +enum CompoundType { + /// id tuples are strings, but serialize as a sequence. + IdTuple, + Struct, + Map, + Seq, +} + +/// +struct SizeEstimatingCompoundSerializer(CompoundType, usize); + impl<'a> SerializeSeq for SizeEstimatingCompoundSerializer { type Ok = usize; type Error = SizeEstimationError; @@ -432,7 +453,7 @@ impl<'a> SerializeSeq for SizeEstimatingCompoundSerializer { } } -// maps are only used for the _finalIvs fields which are not encrypted. +/// maps are only used for the _finalIvs fields which are not encrypted. impl<'a> SerializeMap for SizeEstimatingCompoundSerializer { type Ok = usize; type Error = SizeEstimationError; @@ -458,14 +479,6 @@ impl<'a> SerializeMap for SizeEstimatingCompoundSerializer { } } -enum CompoundType { - IdTuple, - Struct, - Map, - Seq, -} -struct SizeEstimatingCompoundSerializer(CompoundType, usize); - impl<'a> SerializeStruct for SizeEstimatingCompoundSerializer { type Ok = usize; type Error = SizeEstimationError; From 189881612acd888af8f7e4652a9b070c33d1a9e5 Mon Sep 17 00:00:00 2001 From: sug Date: Fri, 8 Nov 2024 13:51:49 +0100 Subject: [PATCH 07/32] wip --- packages/node-mimimi/src/importer.rs | 2 - .../src/importer/importable_mail.rs | 1657 ++++++----------- .../extend_mail_parser.rs | 4 +- .../mime_string_to_importable_mail_test.rs | 515 +++++ .../msg_file_compatibility_test.rs | 276 +++ .../plain_text_to_html_converter.rs | 11 +- 6 files changed, 1333 insertions(+), 1132 deletions(-) rename packages/node-mimimi/src/importer/{ => importable_mail}/extend_mail_parser.rs (96%) create mode 100644 packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs create mode 100644 packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs rename packages/node-mimimi/src/importer/{ => importable_mail}/plain_text_to_html_converter.rs (94%) diff --git a/packages/node-mimimi/src/importer.rs b/packages/node-mimimi/src/importer.rs index ec563b6beb2..74a3a95d8e7 100644 --- a/packages/node-mimimi/src/importer.rs +++ b/packages/node-mimimi/src/importer.rs @@ -18,11 +18,9 @@ use tutasdk::{IdTupleGenerated, LoggedInSdk, Sdk}; pub type NapiTokioMutex = napi::tokio::sync::Mutex; -pub mod extend_mail_parser; pub mod file_reader; pub mod imap_reader; mod importable_mail; -mod plain_text_to_html_converter; #[derive(Clone, PartialEq)] pub enum ImportParams { diff --git a/packages/node-mimimi/src/importer/importable_mail.rs b/packages/node-mimimi/src/importer/importable_mail.rs index 20af6b102ba..7092ad174aa 100644 --- a/packages/node-mimimi/src/importer/importable_mail.rs +++ b/packages/node-mimimi/src/importer/importable_mail.rs @@ -1,16 +1,18 @@ -use crate::importer::extend_mail_parser::{get_reply_type_from_headers, MakeString}; -use crate::importer::plain_text_to_html_converter; use crate::tuta_imap::client::types::ImapMail; -use mail_parser::{Address, GetHeader, HeaderName, HeaderValue, MessageParser, MimeHeaders, PartType}; +use extend_mail_parser::MakeString; +use mail_parser::{ + Address, GetHeader, HeaderName, HeaderValue, MessageParser, MessagePart, MessagePartId, + MimeHeaders, PartType, +}; use std::borrow::Cow; -use std::collections::HashMap; -use std::hash::Hash; +use std::collections::{HashMap, HashSet}; use std::time::SystemTime; use tutasdk::date::DateTime; use tutasdk::entities::generated::tutanota::{ - EncryptedMailAddress, ImportMailData, ImportMailDataMailReference, MailAddress, Recipients, + EncryptedMailAddress, ImportMailData, ImportMailDataMailReference, MailAddress, Recipients, }; -use tutasdk::CustomId; +pub mod extend_mail_parser; +mod plain_text_to_html_converter; // todo: this is used for DataTransferType, so id really dont have to be unique, // but have to be valid length @@ -18,147 +20,147 @@ const FIXED_CUSTOM_ID: &str = "____"; #[derive(Default)] #[cfg_attr(test, derive(PartialEq, Debug))] -enum MailState { - #[default] - Received = 2, - Sent = 1, - Draft = 0, +pub(super) enum MailState { + #[default] + Received = 2, + Sent = 1, + Draft = 0, } #[repr(i64)] #[derive(Default)] #[cfg_attr(test, derive(PartialEq, Debug))] -enum ICalType { - #[default] - Nothing = 0, - ICalPublish = 1, - ICalRequest = 2, - ICalAdd = 3, - ICalCancel = 4, - ICalRefresh = 5, - ICalCounter = 6, - ICalDeclineCounter = 7, +pub(super) enum ICalType { + #[default] + Nothing = 0, + ICalPublish = 1, + ICalRequest = 2, + ICalAdd = 3, + ICalCancel = 4, + ICalRefresh = 5, + ICalCounter = 6, + ICalDeclineCounter = 7, } #[derive(Default)] #[cfg_attr(test, derive(PartialEq, Debug))] pub(super) enum ReplyType { - #[default] - Nothing = 0, - Reply = 1, - Forward = 2, - ReplyForward = 3, + #[default] + Nothing = 0, + Reply = 1, + Forward = 2, + ReplyForward = 3, } #[cfg_attr(test, derive(PartialEq, Debug))] -enum ImportableMailAttachment { - Attachment { - filename: Option, - content_type: String, - content_id: String, - content: Vec, - is_inline: bool, - }, - AttachedMessage { - message: ImportableMail, - }, +pub(super) enum ImportableMailAttachment { + Attachment { + filename: Option, + content_type: String, + content_id: String, + content: Vec, + is_inline: bool, + }, + AttachedMessage { + message: ImportableMail, + }, } #[cfg_attr(test, derive(PartialEq, Debug))] -enum BodyText { - Html(String), - Plain(String), +pub(super) enum BodyText { + Html(String), + Plain(String), } #[derive(Default, PartialEq)] #[cfg_attr(test, derive(Debug))] -pub struct MailContact { - pub mail_address: String, - pub name: String, +pub(super) struct MailContact { + pub mail_address: String, + pub name: String, } impl<'a> From> for MailContact { - fn from(value: mail_parser::Addr) -> Self { - Self { - name: value.name.unwrap_or_default().to_string(), - mail_address: value.address.unwrap_or_default().to_string(), - } - } + fn from(value: mail_parser::Addr) -> Self { + Self { + name: value.name.unwrap_or_default().to_string(), + mail_address: value.address.unwrap_or_default().to_string(), + } + } } impl From for MailAddress { - fn from(value: MailContact) -> Self { - Self { - _id: None, - address: value.mail_address, - name: value.name, - contact: None, - _finalIvs: Default::default(), - } - } + fn from(value: MailContact) -> Self { + Self { + _id: None, + address: value.mail_address, + name: value.name, + contact: None, + _finalIvs: Default::default(), + } + } } /// Input data for mail import service #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ImportableMail { - pub headers_string: String, - pub subject: String, - pub html_body_text: String, - pub attachments: Vec, - - pub date: Option, - - pub different_envelope_sender: Option, - pub from_addresses: Vec, - pub to_addresses: Vec, - pub cc_addresses: Vec, - pub bcc_addresses: Vec, - pub reply_to_addresses: Vec, - - pub ical_type: ICalType, - pub reply_type: ReplyType, - - pub mail_state: MailState, - pub is_phishing: bool, // https://turbo.fish/::%3Cphising%3E - pub unread: bool, - - pub message_id: Option, - pub in_reply_to: Option, - pub references: Vec, + pub(super) headers_string: String, + pub(super) subject: String, + pub(super) html_body_text: String, + pub(super) attachments: Vec, + + pub(super) date: Option, + + pub(super) different_envelope_sender: Option, + pub(super) from_addresses: Vec, + pub(super) to_addresses: Vec, + pub(super) cc_addresses: Vec, + pub(super) bcc_addresses: Vec, + pub(super) reply_to_addresses: Vec, + + pub(super) ical_type: ICalType, + pub(super) reply_type: ReplyType, + + pub(super) mail_state: MailState, + pub(super) is_phishing: bool, // https://turbo.fish/::%3Cphising%3E + pub(super) unread: bool, + + pub(super) message_id: Option, + pub(super) in_reply_to: Option, + pub(super) references: Vec, } impl ImportableMail { - /// Utility function to convert mail_parser::Address - /// to a list of tutasdk::MailAddress - /// in such a way that every address must have mail-address and optional name - /// - /// returns None, if any of the address have empty mail-address - /// - /// set the _id: of all mail address to random 4-byte long customId, - /// this will only be valid in dataTransferType context - fn map_to_tuta_mail_address(mail_parser_addresses: Cow
) -> Vec { - let address_list = match mail_parser_addresses.as_ref() { - Address::List(address_list) => Cow::Borrowed(address_list), - Address::Group(group_senders) => { - let group_addresses = group_senders - .iter() - .map(|group| group.addresses.as_slice()) - .collect::>() - .concat(); - - Cow::Owned(group_addresses) - } - }; - - address_list - .as_ref() - .into_iter() - .map(|address| MailContact { - mail_address: address.address().unwrap_or_default().to_string(), - name: address.name().unwrap_or_default().to_string(), - }) - .collect() - } + /// Utility function to convert mail_parser::Address + /// to a list of tutasdk::MailAddress + /// in such a way that every address must have mail-address and optional name + /// + /// returns None, if any of the address have empty mail-address + /// + /// set the _id: of all mail address to random 4-byte long customId, + /// this will only be valid in dataTransferType context + fn map_to_tuta_mail_address(mail_parser_addresses: Cow
) -> Vec { + let address_list = match mail_parser_addresses.as_ref() { + Address::List(address_list) => Cow::Borrowed(address_list), + Address::Group(group_senders) => { + let group_addresses = group_senders + .iter() + .map(|group| group.addresses.as_slice()) + .collect::>() + .concat(); + + Cow::Owned(group_addresses) + }, + }; + + address_list + .as_ref() + .into_iter() + .map(|address| MailContact { + mail_address: address.address().unwrap_or_default().to_string(), + name: address.name().unwrap_or_default().to_string(), + }) + .collect() + } fn handle_plain_text(email_body_as_html: &mut String, plain_text: &str) { let plain_text_as_html = plain_text_to_html_converter::plain_text_to_html(plain_text); @@ -181,48 +183,43 @@ impl ImportableMail { Ok(()) } - // from the parsed message - // return : - // .0 a single string that ca be display as email in html format - // .1 list of attachment found - fn process_all_parts( - parsed_message: &mail_parser::Message, - ) -> Result<(String, Vec), MailParseError> { - let mut email_body_as_html = String::new(); - let mut attachments = Vec::with_capacity(parsed_message.attachments.len()); + // from the parsed message + // return : + // .0 a single string that ca be display as email in html format + // .1 list of attachment found + fn process_all_parts( + parsed_message: &mail_parser::Message, + ) -> Result<(String, Vec), MailParseError> { + let mut email_body_as_html = String::new(); + let mut attachments = Vec::with_capacity(parsed_message.attachments.len()); + + // all the alternative of multipart/alternative that we chose not to include + let mut multipart_ignored_alternative = HashSet::new(); for (part_id, part) in parsed_message.parts.iter().enumerate() { + if multipart_ignored_alternative.contains(&part_id) { + continue; + } + // if not boundary attribute is defined in Content-Type, then the text is treated as comment. // see: russian.msg let probably_unbounded_message = parsed_message.attachments.contains(&part_id) && part_id == 0; - match &part.body { + match &part.body { PartType::Html(_) | PartType::Text(_) if probably_unbounded_message => { // is this a comment in mime? continue; }, - PartType::Text(text) => { - Self::handle_plain_text(&mut email_body_as_html, text.as_ref()); - }, - - PartType::Html(html_text) => { - Self::handle_html_text(&mut email_body_as_html, html_text.as_ref()) - }, - - PartType::Message(attached_message) => { - Self::handle_attached_message(&mut attachments, attached_message.to_owned())?; - } - - PartType::Binary(binary_content) | PartType::InlineBinary(binary_content) => { - let is_inline = if matches!(part.body, PartType::InlineBinary(_)) { - true - } else if matches!(part.body, PartType::Binary(_)) { - false - } else { - unreachable!(); - }; + PartType::Binary(binary_content) | PartType::InlineBinary(binary_content) => { + let is_inline = if matches!(part.body, PartType::InlineBinary(_)) { + true + } else if matches!(part.body, PartType::Binary(_)) { + false + } else { + unreachable!(); + }; Self::handle_binary( &mut attachments, &part.headers, @@ -231,8 +228,67 @@ impl ImportableMail { ); }, - PartType::Multipart(multi_part_msg) => { - continue; + // todo: of it is PartType::Text & PartType::Html, check for ConentDisposition Header + // and if it is attachment, treat it as attachment + PartType::Text(text) => { + let is_text_plain = part + .content_type() + .map(|content_type| { + let subtype = content_type.subtype().unwrap_or({ + // what do we do with the content-type: text + // with no subtype + // for now assume plain + if content_type.c_type == "text" { + "plain" + } else { + "" + } + }); + + let is_text_plain = content_type.c_type == "text" && subtype == "plain"; + // edu: https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html + // subtype of the multipart Content-Type. + // This type is syntactically identical to multipart/mixed, but the + // semantics are different. In particular, in a digest, the default + // Content-Type value for a body part is changed from "text/plain" to "message/rfc822". + let is_message_rfc822 = + content_type.c_type == "message" && subtype == "rfc833"; + + is_text_plain || is_message_rfc822 + }) + .unwrap_or({ + // what should we treat text that is not content-Type: text? + // fow now let's assume it's content-type: text/plain + true + }); + + if is_text_plain { + Self::handle_plain_text(&mut email_body_as_html, text.as_ref()); + } else { + Self::handle_binary( + &mut attachments, + &part.headers, + text.as_bytes().to_vec(), + false, + ); + } + }, + + PartType::Html(html_text) => { + Self::handle_html_text(&mut email_body_as_html, html_text.as_ref()) + }, + + PartType::Message(attached_message) => { + Self::handle_attached_message(&mut attachments, attached_message.to_owned())?; + }, + + PartType::Multipart(multi_part_ids) => { + Self::handle_multipart( + parsed_message, + &mut multipart_ignored_alternative, + part, + multi_part_ids, + ); }, } } @@ -240,6 +296,100 @@ impl ImportableMail { Ok((email_body_as_html, attachments)) } + fn handle_multipart( + parsed_message: &mail_parser::Message, + multipart_ignored_alternative: &mut HashSet, + part: &MessagePart, + multi_part_ids: &Vec, + ) { + let is_multipart_alternative = part + .content_type() + .map(|content_type| { + assert_eq!( + "multipart", content_type.c_type, + "Multipart is not multipart?" + ); + content_type.subtype() == Some("alternative") + }) + .unwrap_or_default(); + + if !is_multipart_alternative { + // we can only take care of multipart/alternative + // what to do for other multipart/* + return; + + // edu: https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html + // The primary subtype for multipart, "mixed", is intended for use when the body parts + // are independent and intended to be displayed serially. Any multipart subtypes that + // an implementation does not recognize should be treated as being of subtype "mixed". + } + + let mut best_alternative_yet = None; + for multipart_id in multi_part_ids { + // if this part was already ignored, + if multipart_ignored_alternative.contains(multipart_id) { + continue; + } + + let alternative_part = parsed_message + .part(*multipart_id) + .expect("Expected multipart part to be there?"); + + // for now, we can only decide between alternative between text/plain and text/html + let alternative_content_type = alternative_part + .content_type() + .expect("All multipart alternative should have a Content-Type header"); + + // todo: handle other content type. example: choosing one image from list of alternatives? + let is_text_plain = alternative_content_type.c_type == "text" + && alternative_content_type.subtype() == Some("plain"); + let is_text_html = alternative_content_type.c_type == "text" + && alternative_content_type.subtype() == Some("html"); + + if is_text_plain { + // always ignore plain. we can display html everytime + multipart_ignored_alternative.insert(*multipart_id); + } else if is_text_html { + // if we found a html, this is what we will select. + // if we had found and html already, we will still choose the new one. + // and insert the last one to ignored list + if let Some(last_choice) = best_alternative_yet { + multipart_ignored_alternative.insert(last_choice); + } + best_alternative_yet = Some(*multipart_id); + } else { + // "Can only choose multipart/alternative between text/plain and text/html" + // todo: this is not a good case + if let Some(last_choice) = best_alternative_yet { + multipart_ignored_alternative.insert(last_choice); + } + best_alternative_yet = Some(*multipart_id); + } + } + + // if we did not find any alternative, we will take the last one, + // don't have to do anything with chosen multipart, + // it will anyway be included in next iteration + if best_alternative_yet.is_none() { + let last_choice = multi_part_ids + .last() + .expect("Wait. how can i choose between empty sets of alternatives?"); + + // do we remove the last_choice from ignored list? + // the problem is: + // will the same alternative part can be referenced by multiple multipart block? + // if so, if we remove last_choice now, and this was also ignored by another multipart, + // we will display it anyhow. probably this is right, right? + assert!( + multipart_ignored_alternative.remove(last_choice), + "if we did not put last_choice in ignore list. why best_alternative_yet is none?" + ); + } + + // ps: we assume that the order is: + // multipart block should always come before all it's alternative + } + fn handle_binary<'a>( attachments: &mut Vec, header_values: &Vec>, @@ -289,1001 +439,264 @@ impl ImportableMail { content, }; attachments.push(this_attachment); - } + } } impl From for ImportMailData { - fn from(importable_mail: ImportableMail) -> Self { - let ImportableMail { - headers_string: headers, - subject, - html_body_text, - different_envelope_sender, - from_addresses, - cc_addresses, - bcc_addresses, - to_addresses, - date, - reply_to_addresses, - ical_type, - reply_type, - mail_state, - is_phishing, - unread, - message_id, - in_reply_to, - references, - attachments, - } = importable_mail; - - let date = date.unwrap_or_else(|| DateTime::from_system_time(SystemTime::now())); - - let reply_tos = reply_to_addresses - .into_iter() - .map(|reply_to| EncryptedMailAddress { - _id: Some(CustomId::from_custom_string(FIXED_CUSTOM_ID)), - _finalIvs: Default::default(), - name: reply_to.name, - address: reply_to.mail_address, - }) - .collect(); - - let bcc_addresses = bcc_addresses.into_iter().map(Into::into).collect(); - let cc_addresses = cc_addresses.into_iter().map(Into::into).collect(); - let to_addresses = to_addresses.into_iter().map(Into::into).collect(); - let from_addresses: Vec = from_addresses.into_iter().map(Into::into).collect(); - - let references = references - .into_iter() - .map(|reference| ImportMailDataMailReference { - _id: Some(CustomId::from_custom_string(FIXED_CUSTOM_ID)), - reference, - }) - .collect(); - - ImportMailData { - _id: Some(CustomId::from_custom_string(FIXED_CUSTOM_ID)), - _finalIvs: HashMap::new(), - compressedHeaders: headers, - subject, - compressedBodyText: html_body_text, - differentEnvelopeSender: different_envelope_sender, - sender: from_addresses - .first() - .cloned() - .unwrap_or(MailContact::default().into()), - recipients: Recipients { - _id: Some(CustomId::from_custom_string(FIXED_CUSTOM_ID)), - bccRecipients: bcc_addresses, - ccRecipients: cc_addresses, - toRecipients: to_addresses, - }, - replyTos: reply_tos, - unread, - confidential: false, - method: ical_type as i64, - phishingStatus: if is_phishing { 1 } else { 0 }, - replyType: reply_type as i64, - date, - state: mail_state as i64, - messageId: message_id, - inReplyTo: in_reply_to, - references, - importedAttachments: vec![], - } - } + fn from(importable_mail: ImportableMail) -> Self { + let ImportableMail { + headers_string: headers, + subject, + html_body_text, + different_envelope_sender, + from_addresses, + cc_addresses, + bcc_addresses, + to_addresses, + date, + reply_to_addresses, + ical_type, + reply_type, + mail_state, + is_phishing, + unread, + message_id, + in_reply_to, + references, + attachments, + } = importable_mail; + + let date = date.unwrap_or_else(|| DateTime::from_system_time(SystemTime::now())); + + let reply_tos = reply_to_addresses + .into_iter() + .map(|reply_to| EncryptedMailAddress { + _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), + _finalIvs: Default::default(), + name: reply_to.name, + address: reply_to.mail_address, + }) + .collect(); + + let bcc_addresses = bcc_addresses.into_iter().map(Into::into).collect(); + let cc_addresses = cc_addresses.into_iter().map(Into::into).collect(); + let to_addresses = to_addresses.into_iter().map(Into::into).collect(); + let from_addresses: Vec = from_addresses.into_iter().map(Into::into).collect(); + + let references = references + .into_iter() + .map(|reference| ImportMailDataMailReference { + _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), + reference, + }) + .collect(); + + ImportMailData { + _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), + _finalIvs: HashMap::new(), + compressedHeaders: headers, + subject, + compressedBodyText: html_body_text, + differentEnvelopeSender: different_envelope_sender, + sender: from_addresses + .first() + .cloned() + .unwrap_or(MailContact::default().into()), + recipients: Recipients { + _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), + bccRecipients: bcc_addresses, + ccRecipients: cc_addresses, + toRecipients: to_addresses, + }, + replyTos: reply_tos, + unread, + confidential: false, + method: ical_type as i64, + phishingStatus: if is_phishing { 1 } else { 0 }, + replyType: reply_type as i64, + date, + state: mail_state as i64, + messageId: message_id, + inReplyTo: in_reply_to, + references, + importedAttachments: vec![], + } + } } impl TryFrom for ImportableMail { - type Error = MailParseError; - fn try_from(imap_mail: ImapMail) -> Result { - let ImapMail { rfc822_full } = imap_mail; + type Error = MailParseError; + fn try_from(imap_mail: ImapMail) -> Result { + let ImapMail { rfc822_full } = imap_mail; - // parse the full mime message - let imap_mail = MessageParser::new() - .parse(rfc822_full.as_slice()) - .ok_or(MailParseError::InvalidMimeMessage)?; + // parse the full mime message + let imap_mail = MessageParser::new() + .parse(rfc822_full.as_slice()) + .ok_or(MailParseError::InvalidMimeMessage)?; - let mut importable_mail = Self::try_from(imap_mail)?; + let mut importable_mail = Self::try_from(imap_mail)?; - // example: - // add more details from imap if given, - importable_mail.is_phishing = false; - importable_mail.unread = true; + // example: + // add more details from imap if given, + importable_mail.is_phishing = false; + importable_mail.unread = true; - Ok(importable_mail) - } + Ok(importable_mail) + } } #[derive(Debug, Clone, PartialEq)] pub enum MailParseError { - InconsistentParts(&'static str), - NoSentDate, - NoRecipient, - NoFrom, - InvalidDate, - InvalidHtmlBody, - InvalidTextBody, - InvalidMimeMessage, - EmptyMailAddress, - Unknown(String), + InconsistentParts(&'static str), + NoSentDate, + NoRecipient, + NoFrom, + InvalidDate, + InvalidHtmlBody, + InvalidTextBody, + InvalidMimeMessage, + EmptyMailAddress, + Unknown(String), } /// allow to convert from parsed message impl<'x> TryFrom> for ImportableMail { - type Error = MailParseError; - - fn try_from(parsed_message: mail_parser::Message) -> Result { - let subject = parsed_message.subject().unwrap_or_default().to_string(); - - let (html_body_text, attachments) = ImportableMail::process_all_parts(&parsed_message)?; - - let date = parsed_message - .date() - .as_ref() - .map(|date_time| DateTime::from_millis(date_time.to_timestamp() as u64 * 1000)); - - let from_addresses = ImportableMail::map_to_tuta_mail_address( - parsed_message.from().map(Cow::Borrowed).unwrap_or_else(|| { - parsed_message - .sender() - .map(Cow::Borrowed) - .unwrap_or_else(|| Cow::Owned(mail_parser::Address::List(vec![]))) - }), - ) - .into_iter() - .map(|mut address| { - // we currently use the name as address if no address was defined on server side - if address.mail_address.is_empty() { - address.mail_address = address.name; - address.name = String::new(); - } - address - }) - .collect::>(); - - let different_envelope_sender = parsed_message - .sender() - .map(|sender| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(sender))) - // sender is allowed to be empty - .unwrap_or_default() - // there should only be one different envelope sender - .pop() - .map(|mut address| { - // we currently use the name as address if no address was defined on server side - if address.mail_address.is_empty() { - address.mail_address = address.name; - address.name = String::new(); - } - address - }) - // different envelope sender should not contain address listed in from_addresses; - .filter(|diff_sender| { - from_addresses - .iter() - .filter(|from| from.mail_address != diff_sender.mail_address) - .next() - .is_some() - }) - .map(|mail_address| mail_address.mail_address); - - let to_addresses = parsed_message - .to() - .map(|to| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(to))) - .unwrap_or_default() - .into_iter() - .filter(|address| !address.mail_address.trim().is_empty()) - .collect(); - - let cc_addresses = parsed_message - .cc() - .map(|cc| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(cc))) - .unwrap_or_default() - .into_iter() - .filter(|address| !address.mail_address.trim().is_empty()) - .collect(); - - let bcc_addresses = parsed_message - .bcc() - .map(|bcc| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(bcc))) - .unwrap_or_default() - .into_iter() - .filter(|address| !address.mail_address.trim().is_empty()) - .collect(); - - let reply_to_addresses = parsed_message - .reply_to() - .map(|reply_to| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(reply_to))) - .unwrap_or_default() - .into_iter() - .filter(|address| !address.mail_address.trim().is_empty()) - .collect(); - - let headers_string = parsed_message - .headers_raw() - .map(|(name, value)| name.to_string() + ":" + value) - .collect::>() - .join(""); - - let reply_type = get_reply_type_from_headers(parsed_message.headers()); - let message_id = parsed_message.message_id().map(String::from); - let in_reply_to = parsed_message.in_reply_to().as_text().map(String::from); - let references = match parsed_message.references() { - HeaderValue::Text(reference) => {vec![reference.to_string()]} - HeaderValue::TextList(references) => {references.iter().map(|cow| cow.to_string()).collect()} - _ => {vec![]} - }; - - Ok(Self { - headers_string, - html_body_text, - subject, - different_envelope_sender, - from_addresses, - to_addresses, - cc_addresses, - bcc_addresses, - reply_to_addresses, - date, - reply_type, - message_id, - in_reply_to, - references, - attachments, - - ical_type: Default::default(), - unread: false, - mail_state: Default::default(), - is_phishing: false, - }) - } -} - -// Keep in sync with MimeStringToSmtpMessageConverterTest ! -#[cfg(test)] -mod tests { - use crate::importer::importable_mail::{ImportableMail, ImportableMailAttachment, MailContact}; - use mail_parser::{MessageParser, MessagePartId}; - use serde::Deserialize; - use std::borrow::Cow; - use std::io::Read; - use tutasdk::date::DateTime; - - impl From for MailContact { - fn from(value: TestMailAddress) -> Self { - let TestMailAddress { - name, mail_address, .. - } = value; - Self { mail_address, name } - } - } - - impl From for ImportableMail { - fn from(mut expected_message: ExpectedMessage) -> Self { - let mut html_body_ids: Vec = vec![]; - let mut plain_body_ids: Vec = vec![]; - let mut attachment_ids: Vec = vec![]; - let mut body_parts = vec![]; - - expected_message.mail_headers.push_str("\n"); - let parsed_headers_res = MessageParser::default() - .parse_headers(expected_message.mail_headers.as_str()) - .unwrap(); - let root_part = mail_parser::MessagePart { - headers: parsed_headers_res.headers().to_vec(), - is_encoding_problem: false, - body: mail_parser::PartType::Text(Cow::Borrowed("")), - encoding: Default::default(), - offset_header: 0, - offset_body: 0, - offset_end: 0, - }; - body_parts.push(root_part); - - if let Some(plain_body_part) = expected_message.plain_body_text { - let plain_body_converted = mail_parser::MessagePart { - headers: vec![], - is_encoding_problem: false, - body: mail_parser::PartType::Text(Cow::Owned(plain_body_part)), - encoding: Default::default(), - offset_header: 0, - offset_body: 0, - offset_end: 0, - }; - plain_body_ids.push(body_parts.len()); - body_parts.push(plain_body_converted); - } - - if let Some(html_body_part) = expected_message.html_body_text { - let html_body_converted = mail_parser::MessagePart { - headers: vec![], - is_encoding_problem: false, - body: mail_parser::PartType::Html(Cow::Owned(html_body_part)), - encoding: Default::default(), - offset_header: 0, - offset_body: 0, - offset_end: 0, - }; - html_body_ids.push(body_parts.len()); - body_parts.push(html_body_converted); - } - - for attached_message in expected_message.attached_messages { - let attached_message_converted = mail_parser::MessagePart { - headers: vec![], - is_encoding_problem: false, - body: Default::default(), - encoding: Default::default(), - offset_header: 0, - offset_body: 0, - offset_end: 0, - }; - attachment_ids.push(body_parts.len()); - body_parts.push(attached_message_converted); - } - - for attached_file in expected_message.attached_files { - let attached_file_converted = mail_parser::MessagePart { - headers: vec![], - is_encoding_problem: false, - body: Default::default(), - encoding: Default::default(), - offset_header: 0, - offset_body: 0, - offset_end: 0, - }; - attachment_ids.push(body_parts.len()); - body_parts.push(attached_file_converted); - } - - let parsed_mail = mail_parser::Message { - html_body: html_body_ids, - text_body: plain_body_ids, - attachments: attachment_ids, - parts: body_parts, - raw_message: Default::default(), - }; - - ImportableMail::try_from(parsed_mail).unwrap() - } - } - - fn parseMail(msg: &str) -> ImportableMail { - let parsed_message = MessageParser::default() - .parse(msg) - .unwrap(); - - println!("{:?}", parsed_message.headers()); - let m: ImportableMail = parsed_message.try_into().unwrap(); - m - } - - #[test] - fn headers() { - let msg = r#"Message-ID: 123456 -Subject: Hello -From: A -To: B -Reply-To: Reply , Reply2 -References: <1234564@web.de> -In-Reply-To: <1234564@web.de> -Date: Thu, 7 Nov 2024 15:54:04 +0100 -Content-Type: multipart/mixed; boundary=frontier -"#; - println!("{}", msg); - let m: ImportableMail = parseMail(msg); - assert_eq!("123456", m.message_id.unwrap()); - assert_eq!(vec![ - MailContact { name: "Reply".to_string(), mail_address: "reply@tutanota.de".to_string() }, - MailContact { name: "Reply2".to_string(), mail_address: "reply2@tutanota.de".to_string() }, - ], m.reply_to_addresses); - assert_eq!(vec!["sadf@tutanota.de".to_string(), "1234564@web.de".to_string()], m.references); - assert_eq!("1234564@web.de", m.in_reply_to.unwrap()); - // assert_eq!("frontier", m.boundary); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(msg, m.headers_string); - } - - #[test] - fn bad_frontier() { - // todo!() - } - - #[test] - fn empty_references() { - // todo!() - } - - #[test] - fn empty_in_reply_to() { - // todo!() - } - - #[test] - fn text_plain_us_ascii() { - // todo!() - } - - #[test] - fn text_plain_utf8bit() { - // todo!() - } - - #[test] - fn text_plain_utf_explicit_8bit() { - // todo!() - } - - #[test] - fn text_plain_utf_quoted_printable() { - // todo!() - } - - #[test] - fn text_plain_utf_base64() { - // todo!() - } - - #[test] - fn text_plain_utf_invalid_base64() { - // todo!() - } - - #[test] - fn text_plain_format_flowed() { - // todo!() - } - - #[test] - fn text_plain_format_flowed_del_sp() { - // todo!() - } - - #[test] - fn text_plain_subject_encoded_word_Qencoding() { - // todo!() - } - - #[test] - fn text_plain_subject_encoded_word_Qencoding_turkish() { - // todo!() - } - - #[test] - fn from_encoded_word_Qencoding() { - // todo!() - } - - #[test] - fn from_encoded_word_Qencoding_colon() { - // todo!() - } - - #[test] - fn recipients_encoded_word_Qencoding_colon() { - // todo!() - } - - #[test] - fn recipients_encoded_word_Qencoding_partly() { - // todo!() - } - - - #[test] - fn text_plain_subject_encoded_word_base64() { - // todo!() - } - - - #[test] - fn text_html_only() { - // todo!() - } - - - #[test] - fn charset() { - // todo!() - } - - #[test] - fn text_html_inline_charset_definition_utf8() { - // todo!() - } - - #[test] - fn text_html_inline_charset_definition_western() { - // todo!() - } - #[test] - fn text_alternative() { - let msg = r#"Subject: Hello -From: A -To: B -Date: Thu, 7 Nov 2024 15:54:04 +0100 (CET) -Content-Type: multipart/alternative; boundary=frontier - ---frontier -Content-type: text/plain; charset=UTF-8; - -Hello Àâüß ---frontier -Content-type: text/html; charset=UTF-8; - -Hello Àâüß
---frontier-- -"#; - let m: ImportableMail = parseMail(msg); - - assert_eq!(&MailContact { mail_address: "a@tutanota.de".to_string(), name: "A".to_string() }, m.from_addresses.first().unwrap()); - assert_eq!(vec![MailContact { mail_address: "b@tutanota.de".to_string(), name: "B".to_string() }], m.to_addresses); - assert_eq!("Hello", m.subject); - assert_eq!("Hello Àâüß
", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - } - - #[test] - fn invalid_domains_in_mail_addresses() { - // todo!() - } - - #[test] - fn multiple_to_headers() { - // todo!() - } - - #[test] - fn attached_message() { - let msg = r#"Subject: parent message -From: A -To: B -Date: Thu, 7 Nov 2024 15:54:04 +0100 (CET) -Content-Type: multipart/mixed; boundary=frontier - ---frontier -Content-type: text/plain; charset=UTF-8; - -normal message ---frontier -Content-Type: message/rfc822; charset=UTF-8; - -Subject: attached message -From: D -To: E -Date: Thu, 7 Nov 2024 15:54:04 +0100 (CET) -Content-type: text/plain; charset=UTF-8; - -Hello Àâüß -"#; - - let m: ImportableMail = parseMail(msg); - - // assert_eq!(&MailContact { mail_address: "a@tutanota.de".to_string(), name: "A".to_string() }, m.from_addresses.first().unwrap()); - // assert_eq!(vec![MailContact { mail_address: "b@tutanota.de".to_string(), name: "B".to_string() }], m.to_addresses); - assert_eq!("parent message", m.subject); - assert_eq!("normal message", m.html_body_text); - // assert_eq!(Some(DateTime::from_millis(0)), m.date); - - let attachment = m.attachments.first().unwrap(); - match attachment { - ImportableMailAttachment::Attachment { .. } => {panic!("should be an attached message")} - ImportableMailAttachment::AttachedMessage { message } => { - // assert_eq!(MailContact{name: "D", mail_address: "d@tutanota.de"}, m.getSender()); - // assert_eq!(List.of(new SmtpMailContact("E", "e@tutanota.de")), m.getToRecipients()); - // assert_eq!("attached message", attached.getSubject()); - // assert_eq!("Hello Àâüß", attached.getPlainBodyText()); - // assert_eq!(null, attached.getHtmlBodyText()); - // assert_eq!(yesterday, attached.getSentDate()); - } - } - } - - #[test] - fn attachments() { - // todo!() - } - - #[test] - fn inline_attachment() { - // todo!() - } - - #[test] - fn attachment_to_attached_message() { - // todo!() - } - - #[test] - fn textAttachment() {} - - - #[test] - fn htmlAttachment() {} - - #[test] - fn multiple_plain_body_text_parts_are_concatenated() { - let eml_contents = r#"Message-Id: some-id -From: A -To: B -Date: Tue, 5 Nov 2024 13:18:59 +0000 -Content-Type: multipart/mixed; boundary=line - ---line -Content-type: text/plain; charset=UTF-8 - -first plain text in body - ---line -Content-Type: text/plain; charset=UTF-8 - -second plain text in body ---line-- -"#; - - let parsed_message = MessageParser::default() - .with_mime_headers() - .parse(eml_contents) - .unwrap(); - let text_contents = parsed_message - .text_bodies() - .map(|a| a.text_contents().unwrap()) - .collect::>() - .join(""); - assert_eq!( - "first plain text in body\nsecond plain text in body", - text_contents - ); - } - - #[test] - fn multiple_html_body_text_parts_are_concatenated() { - let eml_contents = r#"Message-Id: some-id -From: A -To: B -Date: Tue, 5 Nov 2024 13:18:59 +0000 -Content-Type: multipart/mixed; boundary=line - ---line -Content-type: text/html; charset=UTF-8 - -

first html text in body

- ---line -Content-Type: text/html; charset=UTF-8 - -

second html text in body

---line-- -"#; - - let parsed_message = MessageParser::default() - .with_mime_headers() - .parse(eml_contents) - .unwrap(); - let text_contents = parsed_message - .html_bodies() - .map(|a| a.text_contents().unwrap()) - .collect::>() - .join(""); - assert_eq!( - "

first html text in body

\n

second html in body

", - text_contents - ); - } - - #[test] - // todo! what does this test (map) - fn concatenate_alternative_html_text_parts() { - let eml_contents = r#"Message-Id: some-id -From: A -To: B -Date: Tue, 5 Nov 2024 13:18:59 +0000 -Content-Type: multipart/mixed; boundary=line - ---line -Content-type: text/plain; charset=UTF-8 - -first plain text in body - ---line -Content-Type: text/html; charset=UTF-8 - -

first html text in body

- ---line-- -"#; - - let parsed_message = MessageParser::default() - .with_mime_headers() - .parse(eml_contents) - .unwrap(); - for body_part in parsed_message.html_bodies() { - eprintln!("====="); - eprintln!("{body_part:#?}"); - } - } - - #[test] - // todo! what does this test (map) - fn concatenate_multiple_html_and_plain_text_parts() { - let eml_contents = r#"Message-Id: some-id -From: A -To: B -Date: Tue, 5 Nov 2024 13:18:59 +0000 -Content-Type: multipart/mixed; boundary=line - ---line -Content-type: text/html; charset=UTF-8 - -

first html text in body

- - ---line -Content-Type: img/gif; charset=UTF-8 -Content-Disposition: inline; filename=name.txt; - -first plain text in body ---line-- -"#; - - let parsed_message = MessageParser::default() - .with_mime_headers() - .parse(eml_contents) - .unwrap(); - - eprintln!("{:?}", parsed_message.text_body); - eprintln!("{:?}", parsed_message.html_body); - eprintln!("{:?}", parsed_message.attachments); - - let text_contents = parsed_message - .text_bodies() - .map(|a| a.text_contents().unwrap()) - .collect::>() - .join(""); - assert_eq!( - "

first html text in body

\nfirst plain text in body", - text_contents - ); - } - - #[test] - fn plain_body_text_parts_are_concatenated_with_html_body_parts_if_html_body_parts_already_existing() { - todo!() - } - - #[test] - fn plain_body_text_parts_are_converted_to_html_body_parts_if_html_body_parts_follow_afterwards() { - todo!() - } - - #[test] - fn text_attachment_with_disposition() { - todo!() - } - - #[test] - fn attachment_with_non_ascii_name() { - todo!() - } - - #[test] - fn attachment_filename_in_content_type() { - todo!() - } - - #[test] - fn attachment_filename_qencoding() { - todo!() - } - - #[test] - fn encrypted() { - todo!() - } - - #[test] - fn can_map_to_all_header_value() { - todo!() - } - - #[test] - fn recipient_groups() { - todo!() - } - - #[test] - fn undisclosed_recipients() { - todo!() - } - - #[test] - fn long_content_type() { - todo!() - } - - #[test] - fn normalize_header_value() {} - - #[test] - fn get_spf_result() { - // net yet used on rust - } - - #[test] - fn mail_from_with_delemiter() { - todo!() - } - - #[test] - fn incomplete_text_content_type() { - todo!() - } - - #[test] - fn calendar_content_type() { - todo!() - } - - #[test] - fn calendar_content_type_method() { - todo!() - } - - #[test] - fn invalid_content_types_default_to_text_plain() { - todo!() - } - - - #[derive(Debug, PartialEq, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct TestMailAddress { - name: String, - mail_address: String, - valid: bool, - } - - #[derive(Debug, PartialEq, Deserialize)] - #[serde(rename_all = "camelCase")] - struct ExpectedAttachedFile { - name: String, - data: String, - mime_type: String, - charset: Option, - content_id: String, - calender_method: Option<()>, - } - - #[derive(Debug, PartialEq, Deserialize)] - #[serde(rename_all = "camelCase")] - struct ExpectedMessage { - id: Option, - boundary: Option, - alternative_boundary: Option, - sender: TestMailAddress, - to_recipients: Vec, - cc_recipients: Vec, - bcc_recipients: Vec, - reply_to: Vec, - in_reply_to: Option, - references: Vec, - auto_submitted: Option<()>, - sent_date: Option, - subject: String, - plain_body_text: Option, - html_body_text: Option, - attached_messages: Vec<()>, - attached_files: Vec, - mail_headers: String, - spf_result: String, - list_unsubscribe: bool, - mail_authentication_result: Option<()>, - } - - #[derive(Debug, PartialEq, Deserialize)] - #[serde(rename_all = "camelCase")] - struct Exception { - clazz: String, - message: String, - } - - #[derive(Debug, PartialEq, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct FileContent { - exception: Option, - result: Option, - } - - impl FileContent { - fn read_from_file(file_path: &str) -> Result { - let file_content = std::fs::read_to_string(file_path) - .map_err(|_| format!("Cannot read content of: {file_path}"))?; - serde_json::from_str::(file_content.as_str()) - .map_err(|e| format!("Cannot read to valid ExpectedMessage struct. Error: {e:?}")) - } - } - - #[test] - fn mime_tools_test_messages() { - const DATA_DIR: &'static str = - concat!(env!("CARGO_MANIFEST_DIR"), "/test/mimetools-testmsgs"); - let source_message_paths = std::fs::read_dir(DATA_DIR) - .unwrap() - .map(Result::unwrap) - .filter(|path| path.file_name().to_str().unwrap().ends_with(".msg")); - - for message_path in source_message_paths { - let message_file_name = message_path.file_name().to_str().unwrap().to_string(); - eprint!("File: {message_file_name}"); - - // let message_file_content = std::fs::r(&message_path.path()).unwrap() - let mut message_file_content = vec![]; - std::fs::File::open(message_path.path()) - .unwrap() - .read_to_end(&mut message_file_content) - .unwrap(); - let parsed_message = MessageParser::default() - .parse(message_file_content.as_slice()) - .expect(format!("Cannot parse test message: {:?}", message_path.path()).as_str()); - - let expected_json_file_name = format!( - "{DATA_DIR}/{}", - message_file_name.replace(".msg", "-expected.json") - ); - let FileContent { - result: expected_result, - exception: expected_exception, - } = FileContent::read_from_file(expected_json_file_name.as_str()).unwrap(); - let parsed_message_result = ImportableMail::try_from(parsed_message.clone()); - - if expected_result.is_some() && expected_exception.is_none() { - let mut importable_mail = parsed_message_result.unwrap(); - let expected_importable_mail = ImportableMail::from(expected_result.unwrap()); - importable_mail.attachments = vec![]; - // everything else is related to multipart i suppose, - const IGNORED_FILES: [&str; 14] = [ - // files i suppose related to multipart - "2002_06_12_doublebound.msg", - "attachment-filename-encoding-Latin1.msg", - "attachment-filename-encoding-UTF8.msg", - "multi-bad.msg", - "multi-clen.msg", - "multi-digest.msg", - "multi-igor.msg", - "multi-igor2.msg", - "multi-nested.msg", - "multi-nested3.msg", - "multi-nested2.msg", - "multi-digest.msg", - "infinite.msg", // have encoding problem - "double-semicolon.msg", // do not know why this fail - ]; - if IGNORED_FILES + type Error = MailParseError; + + fn try_from(parsed_message: mail_parser::Message) -> Result { + let subject = parsed_message.subject().unwrap_or_default().to_string(); + + let (html_body_text, attachments) = ImportableMail::process_all_parts(&parsed_message)?; + + let date = parsed_message + .date() + .as_ref() + .map(|date_time| DateTime::from_millis(date_time.to_timestamp() as u64 * 1000)); + + let from_addresses = ImportableMail::map_to_tuta_mail_address( + parsed_message.from().map(Cow::Borrowed).unwrap_or_else(|| { + parsed_message + .sender() + .map(Cow::Borrowed) + .unwrap_or_else(|| Cow::Owned(mail_parser::Address::List(vec![]))) + }), + ) + .into_iter() + .map(|mut address| { + // we currently use the name as address if no address was defined on server side + if address.mail_address.is_empty() { + address.mail_address = address.name; + address.name = String::new(); + } + address + }) + .collect::>(); + + let different_envelope_sender = parsed_message + .sender() + .map(|sender| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(sender))) + // sender is allowed to be empty + .unwrap_or_default() + // there should only be one different envelope sender + .pop() + .map(|mut address| { + // we currently use the name as address if no address was defined on server side + if address.mail_address.is_empty() { + address.mail_address = address.name; + address.name = String::new(); + } + address + }) + // different envelope sender should not contain address listed in from_addresses; + .filter(|diff_sender| { + from_addresses .iter() - .filter(|f| f.starts_with(message_file_name.as_str())) + .filter(|from| from.mail_address != diff_sender.mail_address) .next() .is_some() - { - eprintln!(" ....ignored"); - continue; - } - eprintln!(); - assert_eq!(importable_mail, expected_importable_mail); - } else if expected_exception.is_some() && expected_result.is_none() { - // check that the parsing have failed, - // but we cannot check for the actual reason in `expected_exception` - // - // - // todo: should not badbound.msg fail on mail_parser::parse thing? why is it failing in ImportableMail::try_from()? - //assert!(parsed_message_result.is_err()); - eprintln!(); - } else if expected_result.is_none() && expected_exception.is_none() { - unreachable!() - } else if expected_exception.is_some() && expected_exception.is_some() { - unreachable!() - } else { - unreachable!() - } - } - } + }) + .map(|mail_address| mail_address.mail_address); + + let to_addresses = parsed_message + .to() + .map(|to| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(to))) + .unwrap_or_default() + .into_iter() + .filter(|address| !address.mail_address.trim().is_empty()) + .collect(); + + let cc_addresses = parsed_message + .cc() + .map(|cc| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(cc))) + .unwrap_or_default() + .into_iter() + .filter(|address| !address.mail_address.trim().is_empty()) + .collect(); + + let bcc_addresses = parsed_message + .bcc() + .map(|bcc| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(bcc))) + .unwrap_or_default() + .into_iter() + .filter(|address| !address.mail_address.trim().is_empty()) + .collect(); + + let reply_to_addresses = parsed_message + .reply_to() + .map(|reply_to| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(reply_to))) + .unwrap_or_default() + .into_iter() + .filter(|address| !address.mail_address.trim().is_empty()) + .collect(); + + let headers_string = parsed_message + .headers_raw() + .map(|(name, value)| name.to_string() + ":" + value) + .collect::>() + .join(""); + + let reply_type = extend_mail_parser::get_reply_type_from_headers(parsed_message.headers()); + let message_id = parsed_message.message_id().map(String::from); + let in_reply_to = parsed_message.in_reply_to().as_text().map(String::from); + let references = match parsed_message.references() { + HeaderValue::Text(reference) => { + vec![reference.to_string()] + }, + HeaderValue::TextList(references) => { + references.iter().map(|cow| cow.to_string()).collect() + }, + _ => { + vec![] + }, + }; + + Ok(Self { + headers_string, + html_body_text, + subject, + different_envelope_sender, + from_addresses, + to_addresses, + cc_addresses, + bcc_addresses, + reply_to_addresses, + date, + reply_type, + message_id, + in_reply_to, + references, + attachments, + + ical_type: Default::default(), + unread: false, + mail_state: Default::default(), + is_phishing: false, + }) + } } + +#[cfg(test)] +mod mime_string_to_importable_mail_test; + +#[cfg(test)] +mod msg_file_compatibility_test; diff --git a/packages/node-mimimi/src/importer/extend_mail_parser.rs b/packages/node-mimimi/src/importer/importable_mail/extend_mail_parser.rs similarity index 96% rename from packages/node-mimimi/src/importer/extend_mail_parser.rs rename to packages/node-mimimi/src/importer/importable_mail/extend_mail_parser.rs index fcea71b9810..6e4fd8e9ff3 100644 --- a/packages/node-mimimi/src/importer/extend_mail_parser.rs +++ b/packages/node-mimimi/src/importer/importable_mail/extend_mail_parser.rs @@ -97,7 +97,7 @@ impl<'a> MakeString for mail_parser::DateTime { let month = MONTH_OF_YEAR[*month as usize - 1]; Cow::Owned(format!( - "{weekday}, {day} {month} {year} {hh:02}:{mm:02}:{ss:02} +{tz_hh:02}{tz_mm:02}" + "{weekday}, {day:02} {month:02} {year} {hh:02}:{mm:02}:{ss:02} +{tz_hh:02}{tz_mm:02}" )) } } @@ -156,7 +156,7 @@ impl<'a> MakeString for mail_parser::ContentType<'a> { } } -pub fn make_mail_address(name: Option<&str>, address: Option<&str>) -> String { +fn make_mail_address(name: Option<&str>, address: Option<&str>) -> String { let name = name.unwrap_or_default(); let mut res = if name.is_empty() || name.starts_with("\"") { name.to_string() diff --git a/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs b/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs new file mode 100644 index 00000000000..2c9b6b8a953 --- /dev/null +++ b/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs @@ -0,0 +1,515 @@ +//! Keep in sync with MimeStringToSmtpMessageConverterTest ! + +use crate::importer::importable_mail::{ImportableMail, ImportableMailAttachment, MailContact}; +use mail_parser::MessageParser; +use tutasdk::date::DateTime; + +fn parse_mail(msg: &str) -> ImportableMail { + let parsed_message = MessageParser::default().parse(msg).unwrap(); + + println!("{:?}", parsed_message.headers()); + let m: ImportableMail = parsed_message.try_into().unwrap(); + m +} + +#[test] +fn headers() { + let msg = r#"Message-ID: 123456 +Subject: Hello +From: A +To: B +Reply-To: Reply , Reply2 +References: <1234564@web.de> +In-Reply-To: <1234564@web.de> +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-Type: multipart/mixed; boundary=frontier +"#; + println!("{}", msg); + let m: ImportableMail = parse_mail(msg); + assert_eq!("123456", m.message_id.unwrap()); + assert_eq!( + vec![ + MailContact { + name: "Reply".to_string(), + mail_address: "reply@tutanota.de".to_string() + }, + MailContact { + name: "Reply2".to_string(), + mail_address: "reply2@tutanota.de".to_string() + }, + ], + m.reply_to_addresses + ); + assert_eq!( + vec!["sadf@tutanota.de".to_string(), "1234564@web.de".to_string()], + m.references + ); + assert_eq!("1234564@web.de", m.in_reply_to.unwrap()); + // assert_eq!("frontier", m.boundary); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(msg, m.headers_string); +} + +#[test] +fn bad_frontier() { + // todo!() +} + +#[test] +fn empty_references() { + // todo!() +} + +#[test] +fn empty_in_reply_to() { + // todo!() +} + +#[test] +fn text_plain_us_ascii() { + // todo!() +} + +#[test] +fn text_plain_utf8bit() { + // todo!() +} + +#[test] +fn text_plain_utf_explicit_8bit() { + // todo!() +} + +#[test] +fn text_plain_utf_quoted_printable() { + // todo!() +} + +#[test] +fn text_plain_utf_base64() { + // todo!() +} + +#[test] +fn text_plain_utf_invalid_base64() { + // todo!() +} + +#[test] +fn text_plain_format_flowed() { + // todo!() +} + +#[test] +fn text_plain_format_flowed_del_sp() { + // todo!() +} + +#[test] +fn text_plain_subject_encoded_word_Qencoding() { + // todo!() +} + +#[test] +fn text_plain_subject_encoded_word_Qencoding_turkish() { + // todo!() +} + +#[test] +fn from_encoded_word_Qencoding() { + // todo!() +} + +#[test] +fn from_encoded_word_Qencoding_colon() { + // todo!() +} + +#[test] +fn recipients_encoded_word_Qencoding_colon() { + // todo!() +} + +#[test] +fn recipients_encoded_word_Qencoding_partly() { + // todo!() +} + +#[test] +fn text_plain_subject_encoded_word_base64() { + // todo!() +} + +#[test] +fn text_html_only() { + // todo!() +} + +#[test] +fn charset() { + // todo!() +} + +#[test] +fn text_html_inline_charset_definition_utf8() { + // todo!() +} + +#[test] +fn text_html_inline_charset_definition_western() { + // todo!() +} +#[test] +fn text_alternative() { + let msg = r#"Subject: Hello +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 (CET) +Content-Type: multipart/alternative; boundary=frontier + +--frontier +Content-type: text/plain; charset=UTF-8; + +Hello Àâüß +--frontier +Content-type: text/html; charset=UTF-8; + +Hello Àâüß
+--frontier-- +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!( + &MailContact { + mail_address: "a@tutanota.de".to_string(), + name: "A".to_string() + }, + m.from_addresses.first().unwrap() + ); + assert_eq!( + vec![MailContact { + mail_address: "b@tutanota.de".to_string(), + name: "B".to_string() + }], + m.to_addresses + ); + assert_eq!("Hello", m.subject); + assert_eq!( + "Hello Àâüß
", + m.html_body_text + ); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); +} + +#[test] +fn invalid_domains_in_mail_addresses() { + // todo!() +} + +#[test] +fn multiple_to_headers() { + // todo!() +} + +#[test] +fn attached_message() { + let msg = r#"Subject: parent message +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 (CET) +Content-Type: multipart/mixed; boundary=frontier + +--frontier +Content-type: text/plain; charset=UTF-8; + +normal message +--frontier +Content-Type: message/rfc822; charset=UTF-8; + +Subject: attached message +From: D +To: E +Date: Thu, 7 Nov 2024 15:54:04 +0100 (CET) +Content-type: text/plain; charset=UTF-8; + +Hello Àâüß +"#; + + let m: ImportableMail = parse_mail(msg); + + // assert_eq!(&MailContact { mail_address: "a@tutanota.de".to_string(), name: "A".to_string() }, m.from_addresses.first().unwrap()); + // assert_eq!(vec![MailContact { mail_address: "b@tutanota.de".to_string(), name: "B".to_string() }], m.to_addresses); + assert_eq!("parent message", m.subject); + assert_eq!("normal message", m.html_body_text); + // assert_eq!(Some(DateTime::from_millis(0)), m.date); + + let attachment = m.attachments.first().unwrap(); + match attachment { + ImportableMailAttachment::Attachment { .. } => { + panic!("should be an attached message") + }, + ImportableMailAttachment::AttachedMessage { message } => { + // assert_eq!(MailContact{name: "D", mail_address: "d@tutanota.de"}, m.getSender()); + // assert_eq!(List.of(new SmtpMailContact("E", "e@tutanota.de")), m.getToRecipients()); + // assert_eq!("attached message", attached.getSubject()); + // assert_eq!("Hello Àâüß", attached.getPlainBodyText()); + // assert_eq!(null, attached.getHtmlBodyText()); + // assert_eq!(yesterday, attached.getSentDate()); + }, + } +} + +#[test] +fn attachments() { + // todo!() +} + +#[test] +fn inline_attachment() { + // todo!() +} + +#[test] +fn attachment_to_attached_message() { + // todo!() +} + +#[test] +fn textAttachment() {} + +#[test] +fn htmlAttachment() {} + +#[test] +fn multiple_plain_body_text_parts_are_concatenated() { + let eml_contents = r#"Message-Id: some-id +From: A +To: B +Date: Tue, 5 Nov 2024 13:18:59 +0000 +Content-Type: multipart/mixed; boundary=line + +--line +Content-type: text/plain; charset=UTF-8 + +first plain text in body + +--line +Content-Type: text/plain; charset=UTF-8 + +second plain text in body +--line-- +"#; + + let parsed_message = MessageParser::default() + .with_mime_headers() + .parse(eml_contents) + .unwrap(); + let text_contents = parsed_message + .text_bodies() + .map(|a| a.text_contents().unwrap()) + .collect::>() + .join(""); + assert_eq!( + "first plain text in body\nsecond plain text in body", + text_contents + ); +} + +#[test] +fn multiple_html_body_text_parts_are_concatenated() { + let eml_contents = r#"Message-Id: some-id +From: A +To: B +Date: Tue, 5 Nov 2024 13:18:59 +0000 +Content-Type: multipart/mixed; boundary=line + +--line +Content-type: text/html; charset=UTF-8 + +

first html text in body

+ +--line +Content-Type: text/html; charset=UTF-8 + +

second html text in body

+--line-- +"#; + + let parsed_message = MessageParser::default() + .with_mime_headers() + .parse(eml_contents) + .unwrap(); + let text_contents = parsed_message + .html_bodies() + .map(|a| a.text_contents().unwrap()) + .collect::>() + .join(""); + assert_eq!( + "

first html text in body

\n

second html in body

", + text_contents + ); +} + +#[test] +// todo! what does this test (map) +fn concatenate_alternative_html_text_parts() { + let eml_contents = r#"Message-Id: some-id +From: A +To: B +Date: Tue, 5 Nov 2024 13:18:59 +0000 +Content-Type: multipart/mixed; boundary=line + +--line +Content-type: text/plain; charset=UTF-8 + +first plain text in body + +--line +Content-Type: text/html; charset=UTF-8 + +

first html text in body

+ +--line-- +"#; + + let parsed_message = MessageParser::default() + .with_mime_headers() + .parse(eml_contents) + .unwrap(); + for body_part in parsed_message.html_bodies() { + eprintln!("====="); + eprintln!("{body_part:#?}"); + } +} + +#[test] +// todo! what does this test (map) +fn concatenate_multiple_html_and_plain_text_parts() { + let eml_contents = r#"Message-Id: some-id +From: A +To: B +Date: Tue, 5 Nov 2024 13:18:59 +0000 +Content-Type: multipart/mixed; boundary=line + +--line +Content-type: text/html; charset=UTF-8 + +

first html text in body

+ + +--line +Content-Type: img/gif; charset=UTF-8 +Content-Disposition: inline; filename=name.txt; + +first plain text in body +--line-- +"#; + + let parsed_message = MessageParser::default() + .with_mime_headers() + .parse(eml_contents) + .unwrap(); + + eprintln!("{:?}", parsed_message.text_body); + eprintln!("{:?}", parsed_message.html_body); + eprintln!("{:?}", parsed_message.attachments); + + let text_contents = parsed_message + .text_bodies() + .map(|a| a.text_contents().unwrap()) + .collect::>() + .join(""); + assert_eq!( + "

first html text in body

\nfirst plain text in body", + text_contents + ); +} + +#[test] +fn plain_body_text_parts_are_concatenated_with_html_body_parts_if_html_body_parts_already_existing() +{ + todo!() +} + +#[test] +fn plain_body_text_parts_are_converted_to_html_body_parts_if_html_body_parts_follow_afterwards() { + todo!() +} + +#[test] +fn text_attachment_with_disposition() { + todo!() +} + +#[test] +fn attachment_with_non_ascii_name() { + todo!() +} + +#[test] +fn attachment_filename_in_content_type() { + todo!() +} + +#[test] +fn attachment_filename_qencoding() { + todo!() +} + +#[test] +fn encrypted() { + todo!() +} + +#[test] +fn can_map_to_all_header_value() { + todo!() +} + +#[test] +fn recipient_groups() { + todo!() +} + +#[test] +fn undisclosed_recipients() { + todo!() +} + +#[test] +fn long_content_type() { + todo!() +} + +#[test] +fn normalize_header_value() {} + +#[test] +fn get_spf_result() { + // net yet used on rust +} + +#[test] +fn mail_from_with_delemiter() { + todo!() +} + +#[test] +fn incomplete_text_content_type() { + todo!() +} + +#[test] +fn calendar_content_type() { + todo!() +} + +#[test] +fn calendar_content_type_method() { + todo!() +} + +#[test] +fn invalid_content_types_default_to_text_plain() { + todo!() +} diff --git a/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs b/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs new file mode 100644 index 00000000000..25e4150e516 --- /dev/null +++ b/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs @@ -0,0 +1,276 @@ +use crate::importer::importable_mail::{ImportableMail, MailContact}; +use serde::Deserialize; +use std::borrow::Cow; +use std::io::Read; + +#[test] +fn mime_tools_test_messages() { + const DATA_DIR: &'static str = concat!(env!("CARGO_MANIFEST_DIR"), "/test/mimetools-testmsgs"); + let source_message_paths = std::fs::read_dir(DATA_DIR) + .unwrap() + .map(Result::unwrap) + .map(|p| p.file_name().to_str().unwrap().to_string()) + .filter(|p| p.ends_with(".msg")); + + // everything else is related to multipart i suppose, + const IGNORED_FILES: &[&str] = &[ + "multi-igor.msg", + "multi-bad.msg", + "multi-digest.msg", + "2002_06_12_doublebound.msg", + "attachment-filename-encoding-Latin1.msg", + "attachment-filename-encoding-UTF8.msg", + "multi-digest.msg", + "multi-igor2.msg", + "multi-nested.msg", + "multi-nested3.msg", + "multi-nested2.msg", + "infinite.msg", // have encoding problem + ]; + + for message_file_name in source_message_paths + .filter(|p| { + IGNORED_FILES + .iter() + .filter(|f| f.starts_with(p.as_str())) + .next() + .is_none() + }) + .chain(IGNORED_FILES.iter().map(|s| { + eprintln!("Ignored file: "); + s.to_string() + })) + .map(|a| { + eprintln!("{a} .....Testing"); + a + }) { + let message_path = format!("{DATA_DIR}/{message_file_name}"); + + // let message_file_content = std::fs::r(&message_path.path()).unwrap() + let mut message_file_content = vec![]; + std::fs::File::open(message_path.as_str()) + .unwrap() + .read_to_end(&mut message_file_content) + .unwrap(); + let parsed_message = mail_parser::MessageParser::default() + .parse(message_file_content.as_slice()) + .unwrap(); + + let expected_json_file_name = format!( + "{DATA_DIR}/{}", + message_file_name.replace(".msg", "-expected.json") + ); + let FileContent { + result: expected_result, + exception: expected_exception, + } = FileContent::read_from_file(expected_json_file_name.as_str()).unwrap(); + let parsed_message_result = ImportableMail::try_from(parsed_message.clone()); + + if expected_result.is_some() && expected_exception.is_none() { + let mut importable_mail = parsed_message_result.unwrap(); + let mut expected_importable_mail = ImportableMail::from(expected_result.unwrap()); + + importable_mail.attachments.clear(); + expected_importable_mail.attachments.clear(); + + // we import raw headers and there is no need to compare them + importable_mail.headers_string = "".to_string(); + expected_importable_mail.headers_string = "".to_string(); + + assert_eq!(importable_mail, expected_importable_mail); + } else if expected_exception.is_some() && expected_result.is_none() { + // check that the parsing have failed, + // but we cannot check for the actual reason in `expected_exception` + // + // + // todo: should not badbound.msg fail on mail_parser::parse thing? why is it failing in ImportableMail::try_from()? + //assert!(parsed_message_result.is_err()); + } else if expected_result.is_none() && expected_exception.is_none() { + unreachable!() + } else if expected_exception.is_some() && expected_exception.is_some() { + unreachable!() + } else { + unreachable!() + } + } +} + +impl From for MailContact { + fn from(value: TestMailAddress) -> Self { + let TestMailAddress { + name, mail_address, .. + } = value; + Self { mail_address, name } + } +} + +impl From for ImportableMail { + fn from(mut expected_message: ExpectedMessage) -> Self { + // add a new line at end of headers for mail_parser::MessageParser::parse_headers + expected_message.mail_headers.push_str("\n"); + + let headers_string_clone = expected_message.mail_headers.clone(); + + let mut body_parts = vec![]; + let mut plain_body_ids = vec![]; + let mut html_body_ids = vec![]; + let mut attachment_ids = vec![]; + + let parsed_headers_res = mail_parser::MessageParser::default() + .parse_headers(expected_message.mail_headers.as_str()) + .unwrap(); + + let root_part = mail_parser::MessagePart { + headers: parsed_headers_res.headers().to_vec(), + is_encoding_problem: false, + body: mail_parser::PartType::Text(Cow::Borrowed("")), + encoding: Default::default(), + offset_header: 0, + offset_body: 0, + offset_end: 0, + }; + body_parts.push(root_part); + + if let Some(html_body_part) = expected_message.html_body_text { + let html_body_converted = mail_parser::MessagePart { + headers: vec![], + is_encoding_problem: false, + body: mail_parser::PartType::Html(Cow::Owned(html_body_part)), + encoding: Default::default(), + offset_header: 0, + offset_body: 0, + offset_end: 0, + }; + html_body_ids.push(body_parts.len()); + body_parts.push(html_body_converted); + } + // if there is both plain text and html in json file, + // probably that json if to test multipart/alternative ( todo: is this true? ) + // and since we always select html in multipart/alternative, + // we can skip adding plain text if html text was set. + // hence the `else if let` instead of `if let` + else if let Some(plain_body_part) = expected_message.plain_body_text { + let plain_body_converted = mail_parser::MessagePart { + headers: vec![], + is_encoding_problem: false, + body: mail_parser::PartType::Text(Cow::Owned(plain_body_part)), + encoding: Default::default(), + offset_header: 0, + offset_body: 0, + offset_end: 0, + }; + plain_body_ids.push(body_parts.len()); + body_parts.push(plain_body_converted); + } + + for attached_message in expected_message.attached_messages { + let attached_message_converted = mail_parser::MessagePart { + headers: vec![], + is_encoding_problem: false, + body: Default::default(), + encoding: Default::default(), + offset_header: 0, + offset_body: 0, + offset_end: 0, + }; + attachment_ids.push(body_parts.len()); + body_parts.push(attached_message_converted); + } + + for attached_file in expected_message.attached_files { + let attached_file_converted = mail_parser::MessagePart { + headers: vec![], + is_encoding_problem: false, + body: Default::default(), + encoding: Default::default(), + offset_header: 0, + offset_body: 0, + offset_end: 0, + }; + attachment_ids.push(body_parts.len()); + body_parts.push(attached_file_converted); + } + + let parsed_mail = mail_parser::Message { + html_body: html_body_ids, + text_body: plain_body_ids, + attachments: attachment_ids, + parts: body_parts, + // todo: + // will only work for .raw_header(), if we use other _raw function or + // try to access .raw_message in From: ImportableMail, + // this won't work + raw_message: Cow::Owned(headers_string_clone.as_bytes().to_vec()), + }; + + ImportableMail::try_from(parsed_mail).unwrap() + } +} + +#[derive(Debug, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TestMailAddress { + name: String, + mail_address: String, + valid: bool, +} + +#[derive(Debug, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ExpectedAttachedFile { + name: String, + data: String, + mime_type: String, + charset: Option, + content_id: String, + calender_method: Option<()>, +} + +#[derive(Debug, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ExpectedMessage { + id: Option, + boundary: Option, + alternative_boundary: Option, + sender: TestMailAddress, + to_recipients: Vec, + cc_recipients: Vec, + bcc_recipients: Vec, + reply_to: Vec, + in_reply_to: Option, + references: Vec, + auto_submitted: Option<()>, + sent_date: Option, + subject: String, + plain_body_text: Option, + html_body_text: Option, + attached_messages: Vec<()>, + attached_files: Vec, + mail_headers: String, + spf_result: String, + list_unsubscribe: bool, + mail_authentication_result: Option<()>, +} + +#[derive(Debug, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Exception { + clazz: String, + message: String, +} + +#[derive(Debug, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileContent { + exception: Option, + result: Option, +} + +impl FileContent { + fn read_from_file(file_path: &str) -> Result { + let file_content = std::fs::read_to_string(file_path) + .map_err(|_| format!("Cannot read content of: {file_path}"))?; + serde_json::from_str::(file_content.as_str()) + .map_err(|e| format!("Cannot read to valid ExpectedMessage struct. Error: {e:?}")) + } +} diff --git a/packages/node-mimimi/src/importer/plain_text_to_html_converter.rs b/packages/node-mimimi/src/importer/importable_mail/plain_text_to_html_converter.rs similarity index 94% rename from packages/node-mimimi/src/importer/plain_text_to_html_converter.rs rename to packages/node-mimimi/src/importer/importable_mail/plain_text_to_html_converter.rs index 572a925e852..075c8718a44 100644 --- a/packages/node-mimimi/src/importer/plain_text_to_html_converter.rs +++ b/packages/node-mimimi/src/importer/importable_mail/plain_text_to_html_converter.rs @@ -8,7 +8,7 @@ use regex::Regex; /// 3. **escapes** "&" with "&", "<" with "<", and ">" with ">" /// /// This code is ported from tutadb PlainTextToHtmlConverter -pub fn plain_text_to_html(plain_text: &str) -> String { +pub(super) fn plain_text_to_html(plain_text: &str) -> String { let mut result: String = String::from(""); let SEPARATOR: Regex = Regex::new("\r?\n").expect("invalid regex"); // todo! move to const let lines = SEPARATOR.split(plain_text); @@ -103,7 +103,9 @@ pub fn add_html_page_tags(html: String) -> String { } mod test { - use crate::importer::plain_text_to_html_converter::{add_html_page_tags, plain_text_to_html}; + use crate::importer::importable_mail::plain_text_to_html_converter::{ + add_html_page_tags, plain_text_to_html, + }; #[test] pub fn convert_to_html() { @@ -134,10 +136,7 @@ mod test { plain_text_to_html(">>> blockquote \r\n with line break")); // quote without text - assert_eq!( - "
", - plain_text_to_html(">") - ); + assert_eq!("
", plain_text_to_html(">")); // quote without text but newline assert_eq!( From e8be7dd3cae8db55402461e6153ae62836b99296 Mon Sep 17 00:00:00 2001 From: sug Date: Fri, 8 Nov 2024 17:45:28 +0100 Subject: [PATCH 08/32] wip: porting test from java --- .../mime_string_to_importable_mail_test.rs | 781 ++++++++++++++---- .../msg_file_compatibility_test.rs | 2 + .../plain_text_to_html_converter.rs | 2 +- 3 files changed, 624 insertions(+), 161 deletions(-) diff --git a/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs b/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs index 2c9b6b8a953..58bcbcd931e 100644 --- a/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs +++ b/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs @@ -1,15 +1,29 @@ //! Keep in sync with MimeStringToSmtpMessageConverterTest ! use crate::importer::importable_mail::{ImportableMail, ImportableMailAttachment, MailContact}; -use mail_parser::MessageParser; +use mail_parser::{MessageParser, MimeHeaders}; use tutasdk::date::DateTime; fn parse_mail(msg: &str) -> ImportableMail { - let parsed_message = MessageParser::default().parse(msg).unwrap(); - - println!("{:?}", parsed_message.headers()); - let m: ImportableMail = parsed_message.try_into().unwrap(); - m + MessageParser::default() + .parse(msg) + .unwrap() + .try_into() + .unwrap() +} + +// to be able to convert any (str/string, str/string).into() => MailContact +impl From<(N, A)> for MailContact +where + N: ToString, + A: ToString, +{ + fn from((name, address): (N, A)) -> Self { + Self { + mail_address: address.to_string(), + name: name.to_string(), + } + } } #[test] @@ -25,124 +39,288 @@ Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-Type: multipart/mixed; boundary=frontier "#; println!("{}", msg); - let m: ImportableMail = parse_mail(msg); + let m = parse_mail(msg); assert_eq!("123456", m.message_id.unwrap()); assert_eq!( + m.reply_to_addresses, vec![ - MailContact { - name: "Reply".to_string(), - mail_address: "reply@tutanota.de".to_string() - }, - MailContact { - name: "Reply2".to_string(), - mail_address: "reply2@tutanota.de".to_string() - }, + ("Reply", "reply@tutanota.de").into(), + ("Reply2", "reply2@tutanota.de").into(), ], - m.reply_to_addresses ); assert_eq!( + m.references, vec!["sadf@tutanota.de".to_string(), "1234564@web.de".to_string()], - m.references ); - assert_eq!("1234564@web.de", m.in_reply_to.unwrap()); - // assert_eq!("frontier", m.boundary); + assert_eq!(Some("1234564@web.de".to_string()), m.in_reply_to); assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); assert_eq!(msg, m.headers_string); } #[test] fn bad_frontier() { - // todo!() + let msg = "Content-Type: multipart/mixed; boundary=komma;ist;nicht;erlaubt\n"; + let parsed_message = MessageParser::default().parse(msg).unwrap(); + let attributes = parsed_message + .content_type() + .unwrap() + .attributes + .as_ref() + .unwrap(); + assert_eq!(attributes.as_slice(), [("boundary".into(), "komma".into())]); } #[test] fn empty_references() { - // todo!() + let msg = "Subject: Hello"; + let m = parse_mail(msg); + assert!(m.references.is_empty()); } #[test] fn empty_in_reply_to() { - // todo!() + let msg = "Subject: Hello"; + let m = parse_mail(msg); + assert_eq!(None, m.in_reply_to); } #[test] -fn text_plain_us_ascii() { - // todo!() +fn text_plain_us_ascii_7bit() { + let msg = r##"Subject: Hello +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 + +US-ASCII: !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~"##; + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(m.subject, "Hello",); + assert_eq!("US-ASCII: !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", m.html_body_text); + assert_eq!(m.date, Some(DateTime::from_millis(1730991244000))); } #[test] fn text_plain_utf8bit() { - // todo!() + let msg = r##"Subject: Hello +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-type: text/plain; charset=UTF-8 + +Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\{Β³|@"##; + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("b", "b@tutanota.de").into()]); + assert_eq!("Hello", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_utf_explicit_8bit() { - // todo!() + let msg = r##"Subject: Hello +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@"##; + + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("b", "b@tutanota.de").into()]); + assert_eq!("Hello", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_utf_quoted_printable() { - // todo!() + let msg = r##"Subject: Hello +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + +Tutanota: =C3=A4=C3=BC=C3=B6=C3=9F=E2=82=AC*#\{=C2=B3|@"##; + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!("Hello", m.subject); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_utf_base64() { - // todo!() + let msg = r##"Subject: Hello +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: base64 + +VHV0YW5vdGE6IMOkw7zDtsOf4oKsKiNce8KzfEA="##; + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Hello", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_utf_invalid_base64() { - // todo!() + let msg = r##"Subject: Hello +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: base64 + +VHV0YW5vdGE6IMOkw7zDtsOf4oKsKiNce8KzfEA"##; // skip the padding "=" to force an exception + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Hello", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_format_flowed() { - // todo!() + let msg = r#"Subject: Hello +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-type: text/plain; charset=UTF-8; format=flowed + +Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es \r\neinen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!"#; + let m = parse_mail(msg); + + assert_eq!("Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es einen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!", m.html_body_text); } #[test] fn text_plain_format_flowed_del_sp() { - // todo!() + let msg = r#"From: A +To: B +Subject: Hello +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-type: text/plain; charset=UTF-8; format=flowed; DelSp=yes + +Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es \r\neinen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!"#; + let m = parse_mail(msg); + assert_eq!( + "Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt eseinen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!", + m.html_body_text); } #[test] -fn text_plain_subject_encoded_word_Qencoding() { - // todo!() +fn text_plain_subject_encoded_word_qencoding() { + let msg = r#"Subject: Hello =?ISO-8859-1?Q?=E4=F6=FC=DF?=abc +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +"#; + let m = parse_mail(msg); + assert_eq!("Hello Àâüßabc", m.subject); } #[test] -fn text_plain_subject_encoded_word_Qencoding_turkish() { - // todo!() +fn text_plain_subject_encoded_word_qencoding_turkish() { + let msg = r#"Subject: =?iso-8859-9?Q?Paracard Hesap =D6zeti?= +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +"#; + let m = parse_mail(msg); + assert_eq!("Paracard Hesap Γ–zeti", m.subject); } #[test] -fn from_encoded_word_Qencoding() { - // todo!() +fn from_encoded_word_qencoding() { + let msg = r#"Subject: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0?==?utf-8?Q?=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= +From: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0?==?utf-8?Q?=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= +To: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0?==?utf-8?Q?=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= +Date: Thu, 7 Nov 2024 15:54:04 +0100 +"#; + let m = parse_mail(msg); + assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.subject); + assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.from_addresses[0].name); + assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.to_addresses[0].name); } #[test] -fn from_encoded_word_Qencoding_colon() { - // todo!() +fn from_encoded_word_qencoding_colon() { + let msg = r#"Subject: Hi +From: =?utf-8?Q?=D0=9B=D0=B8=D1=82=D1=80=D0=B5=D1=81=3A=20=D0=A1=D0=B0=D0=BC=D0=B8=D0=B7=D0=B4=D0=B0=D1=82?= +"#; + let m = parse_mail(msg); + assert_eq!( + m.from_addresses[0], + ("ЛитрСс: Π‘Π°ΠΌΠΈΠ·Π΄Π°Ρ‚", "mail@selfpub.ru").into() + ); } #[test] -fn recipients_encoded_word_Qencoding_colon() { - // todo!() +fn recipients_encoded_word_qencoding_colon() { + let msg = "To: =?utf-8?Q?=D0=9B=D0=B8=D1=82=D1=80=D0=B5=D1=81=3A=20=D0=A1=D0=B0=D0=BC=D0=B8=D0=B7=D0=B4=D0=B0=D1=82?= \n"; + let m = parse_mail(msg); + assert_eq!( + m.from_addresses[0], + ("ЛитрСс: Π‘Π°ΠΌΠΈΠ·Π΄Π°Ρ‚", "mail@selfpub.ru").into() + ); } #[test] -fn recipients_encoded_word_Qencoding_partly() { - // todo!() +fn recipients_encoded_word_qencoding_partly() { + let msg = "To: =?ISO-8859-1?Q?Andr=E9?= Pirard \n"; + let m = parse_mail(msg); + assert_eq!( + m.from_addresses[0], + ("AndrΓ© Pirard", "PIRARD@vm1.ulg.ac.be").into() + ); } #[test] fn text_plain_subject_encoded_word_base64() { - // todo!() + let msg = r#"Subject: =?utf-8?B?SGVsbG8gw6TDtsO8w58=?= +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +"#; + let m = parse_mail(msg); + assert_eq!("Hello Àâüß", m.subject); } #[test] fn text_html_only() { - // todo!() + let msg = r#"Subject: Hello +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-type: text/html; charset=UTF-8 + +Hello Àâüß
"#; + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!( + "Hello Àâüß
", + m.html_body_text + ); } #[test] @@ -152,12 +330,29 @@ fn charset() { #[test] fn text_html_inline_charset_definition_utf8() { - // todo!() + let msg = r#"Content-type: text/html +Content-Transfer-Encoding: 8bit + +

Π‘Π»Π°Π³ΠΎΠ΄Π°Ρ€ΠΈΠΌ Π’ΠΈ

"#; + let m = parse_mail(msg); + + assert_eq!( + "

Π‘Π»Π°Π³ΠΎΠ΄Π°Ρ€ΠΈΠΌ Π’ΠΈ

", + m.html_body_text + ); } #[test] fn text_html_inline_charset_definition_western() { - // todo!() + let msg = r#"Content-type: text/html +Content-Transfer-Encoding: base64 + +PGh0bWw+PGhlYWQ+PG1ldGEgY2hhcnNldD0iSVNPLTg4NTktMTUiPjwvaGVhZD48Ym9keT48cD6kIPbkPC9wPjwvYm9keT48L2h0bWw+"#; + let m = parse_mail(msg); + assert_eq!( + "

€ ΓΆΓ€

", + m.html_body_text + ); } #[test] fn text_alternative() { @@ -179,36 +374,53 @@ Content-type: text/html; charset=UTF-8; "#; let m: ImportableMail = parse_mail(msg); - assert_eq!( - &MailContact { - mail_address: "a@tutanota.de".to_string(), - name: "A".to_string() - }, - m.from_addresses.first().unwrap() - ); - assert_eq!( - vec![MailContact { - mail_address: "b@tutanota.de".to_string(), - name: "B".to_string() - }], - m.to_addresses - ); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); assert_eq!("Hello", m.subject); assert_eq!( "Hello Àâüß
", m.html_body_text ); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn invalid_domains_in_mail_addresses() { - // todo!() + let msg = r#"Subject: Hello +From: A +To: B , C , D +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!( + m.to_addresses, + vec![ + ("B", "b@a.example").into(), + ("C", "c@c.com").into(), + ("D", "d@d.invalid").into() + ] + ); } #[test] fn multiple_to_headers() { - // todo!() + let msg = r#"Subject: Hello +From: A +To: B , C +To: D +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!( + m.to_addresses, + vec![ + ("B", "b@b.org").into(), + ("C", "c@c.com").into(), + ("D", "d@d.net").into() + ] + ); } #[test] @@ -216,7 +428,7 @@ fn attached_message() { let msg = r#"Subject: parent message From: A To: B -Date: Thu, 7 Nov 2024 15:54:04 +0100 (CET) +Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-Type: multipart/mixed; boundary=frontier --frontier @@ -229,56 +441,189 @@ Content-Type: message/rfc822; charset=UTF-8; Subject: attached message From: D To: E -Date: Thu, 7 Nov 2024 15:54:04 +0100 (CET) +Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/plain; charset=UTF-8; + Hello Àâüß "#; let m: ImportableMail = parse_mail(msg); - // assert_eq!(&MailContact { mail_address: "a@tutanota.de".to_string(), name: "A".to_string() }, m.from_addresses.first().unwrap()); - // assert_eq!(vec![MailContact { mail_address: "b@tutanota.de".to_string(), name: "B".to_string() }], m.to_addresses); - assert_eq!("parent message", m.subject); - assert_eq!("normal message", m.html_body_text); - // assert_eq!(Some(DateTime::from_millis(0)), m.date); - - let attachment = m.attachments.first().unwrap(); - match attachment { - ImportableMailAttachment::Attachment { .. } => { - panic!("should be an attached message") - }, - ImportableMailAttachment::AttachedMessage { message } => { - // assert_eq!(MailContact{name: "D", mail_address: "d@tutanota.de"}, m.getSender()); - // assert_eq!(List.of(new SmtpMailContact("E", "e@tutanota.de")), m.getToRecipients()); - // assert_eq!("attached message", attached.getSubject()); - // assert_eq!("Hello Àâüß", attached.getPlainBodyText()); - // assert_eq!(null, attached.getHtmlBodyText()); - // assert_eq!(yesterday, attached.getSentDate()); - }, + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(m.subject, "parent message"); + assert_eq!(m.html_body_text, "normal message"); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + + assert_eq!(m.attachments.len(), 1); + for attachment in m.attachments { + match attachment { + ImportableMailAttachment::Attachment { .. } => { + // todo: + // should we have got the attachment here or in AttachedMessage? + }, + ImportableMailAttachment::AttachedMessage { message: attached } => { + assert_eq!(attached.from_addresses, vec![("D", "d@tutanota.de").into()]); + assert_eq!(attached.to_addresses, vec![("E", "e@tutanota.de").into()]); + assert_eq!(attached.subject, "attached message"); + assert_eq!(attached.html_body_text, "Hello Àâüß"); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date) + }, + } } } #[test] fn attachments() { - // todo!() + let msg = r#"Subject: multiple attachments +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-Type: multipart/mixed; boundary=frontier + +--frontier +Content-type: text/plain; charset=UTF-8; + +Hello Àâüß +--frontier +Content-type: application/octet-stream; +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=a1.txt; + +Zmlyc3QgYXR0YWNobWVudA== +--frontier +Content-type: application/pdf; +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=a2.pdf; + +c2Vjb25kIGF0dGFjaG1lbnQ= +--frontier +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=withoutContentType.pdf; + +c2Vjb25kIGF0dGFjaG1lbnQ= +--frontier-- +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(m.subject, "multiple attachments"); + + assert_eq!(m.attachments.len(), 3); + todo!() } #[test] fn inline_attachment() { - // todo!() + let msg = r#"Subject: inline attachment +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-Type: multipart/mixed; boundary=frontier + +--frontier +Content-type: text/html; charset=UTF-8; + + +--frontier +Content-type: application/octet-stream; +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename=a1.png; +Content-ID: <123@tutanota.de>; + +Zmlyc3QgYXR0YWNobWVudA== +--frontier-- +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + + todo!() } #[test] fn attachment_to_attached_message() { - // todo!() + let msg = r#"Subject: parent message +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-Type: message/rfc822; charset=UTF-8; + +Subject: attached message +From: D +To: E +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-type: application/octet-stream; +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=indirectly_attached.txt; + +Zmlyc3QgYXR0YWNobWVudA== +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() } #[test] -fn textAttachment() {} +fn text_attachment() { + let msg = r#"Subject: text attachment +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-Type: multipart/mixed; boundary=frontier + +--frontier +Content-type: text/plain; charset=UTF-8 + +Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@ +--frontier +Content-type: text/plain; charset=UTF-8 +Content-Disposition: attachment; filename=a1.txt; + +Abc, die Katze liegt im Schnee ! Àâü?ß ! +--frontier-- +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() +} #[test] -fn htmlAttachment() {} +fn html_attachment() { + let msg = r#"Subject: html attachment +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-Type: multipart/mixed; boundary=frontier + +--frontier +Content-type: text/plain; charset=UTF-8 + +Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@ +--frontier +Content-type: text/html; charset=UTF-8 +Content-Disposition: attachment; filename=a1.html; + +Hello Àâüß
+--frontier-- +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() +} #[test] fn multiple_plain_body_text_parts_are_concatenated() { @@ -317,43 +662,36 @@ second plain text in body #[test] fn multiple_html_body_text_parts_are_concatenated() { - let eml_contents = r#"Message-Id: some-id -From: A -To: B -Date: Tue, 5 Nov 2024 13:18:59 +0000 -Content-Type: multipart/mixed; boundary=line + let msg = r#"Message-Id: some-id +Subject: multiple text/html parts concatenated +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-Type: multipart/mixed; boundary=frontier ---line +--frontier Content-type: text/html; charset=UTF-8 -

first html text in body

- ---line -Content-Type: text/html; charset=UTF-8 +Hello Àâüß
+--frontier +Content-type: text/html; charset=UTF-8 -

second html text in body

---line-- +Test Test
+--frontier-- "#; - let parsed_message = MessageParser::default() - .with_mime_headers() - .parse(eml_contents) - .unwrap(); - let text_contents = parsed_message - .html_bodies() - .map(|a| a.text_contents().unwrap()) - .collect::>() - .join(""); - assert_eq!( - "

first html text in body

\n

second html in body

", - text_contents - ); + let m = parse_mail(msg); + + assert_eq!("multiple text/html parts concatenated", m.subject); + assert_eq!("Hello Àâüß
Test Test
", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(0, m.attachments.len()); } #[test] // todo! what does this test (map) fn concatenate_alternative_html_text_parts() { - let eml_contents = r#"Message-Id: some-id + let msg = r#"Message-Id: some-id From: A To: B Date: Tue, 5 Nov 2024 13:18:59 +0000 @@ -371,21 +709,14 @@ Content-Type: text/html; charset=UTF-8 --line-- "#; - - let parsed_message = MessageParser::default() - .with_mime_headers() - .parse(eml_contents) - .unwrap(); - for body_part in parsed_message.html_bodies() { - eprintln!("====="); - eprintln!("{body_part:#?}"); - } + let m = parse_mail(msg); + todo!() } #[test] // todo! what does this test (map) fn concatenate_multiple_html_and_plain_text_parts() { - let eml_contents = r#"Message-Id: some-id + let msg = r#"Message-Id: some-id From: A To: B Date: Tue, 5 Nov 2024 13:18:59 +0000 @@ -405,84 +736,179 @@ first plain text in body --line-- "#; - let parsed_message = MessageParser::default() - .with_mime_headers() - .parse(eml_contents) - .unwrap(); - - eprintln!("{:?}", parsed_message.text_body); - eprintln!("{:?}", parsed_message.html_body); - eprintln!("{:?}", parsed_message.attachments); - - let text_contents = parsed_message - .text_bodies() - .map(|a| a.text_contents().unwrap()) - .collect::>() - .join(""); - assert_eq!( - "

first html text in body

\nfirst plain text in body", - text_contents - ); + let m = parse_mail(msg); + todo!() } #[test] fn plain_body_text_parts_are_concatenated_with_html_body_parts_if_html_body_parts_already_existing() { + let msg = r#"Subject: multiple text/html and text/plain parts concatenated to single text/html +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-Type: multipart/mixed; boundary=frontier + +--frontier +Content-type: text/html; charset=UTF-8 + +Hello Àâüß
+--frontier +Content-type: text/html; charset=UTF-8 + +Test Test
+--frontier +Content-type: text/plain; charset=UTF-8 + +Abc, die Katze liegt im Schnee ! Àâü?ß ! +--frontier- +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!( + "multiple text/html and text/plain parts concatenated to single text/html", + m.subject + ); todo!() } #[test] fn plain_body_text_parts_are_converted_to_html_body_parts_if_html_body_parts_follow_afterwards() { - todo!() + let msg = r#" + +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_attachment_with_disposition() { - todo!() + let msg = r#"Subject: text attachment +From: A +To: B +Date: " + new MailDateFormat().format(date) + " +Content-type: text/plain; charset=UTF-8 +Content-Disposition: attachment; filename=a1.txt; + +Abc, die Katze liegt im Schnee ! Àâü?ß ! +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn attachment_with_non_ascii_name() { - todo!() + let msg = r#" + +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn attachment_filename_in_content_type() { - todo!() + let msg = r#" + +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn attachment_filename_qencoding() { - todo!() + let msg = r#" + +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn encrypted() { - todo!() + let msg = r#" + +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn can_map_to_all_header_value() { - todo!() + let msg = r#" + +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn recipient_groups() { - todo!() + let msg = r#" + +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn undisclosed_recipients() { - todo!() + let msg = r#" + +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn long_content_type() { - todo!() + let msg = r#" + +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] -fn normalize_header_value() {} +fn normalize_header_value() { + let msg = r#" + +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); +} #[test] fn get_spf_result() { @@ -491,25 +917,60 @@ fn get_spf_result() { #[test] fn mail_from_with_delemiter() { - todo!() + let msg = r#" + +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn incomplete_text_content_type() { - todo!() + let msg = r#" + +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn calendar_content_type() { - todo!() + let msg = r#" + +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn calendar_content_type_method() { - todo!() + let msg = r#" + +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn invalid_content_types_default_to_text_plain() { - todo!() + let msg = r#" + +"#; + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } diff --git a/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs b/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs index 25e4150e516..7b0ce089a01 100644 --- a/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs +++ b/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs @@ -1,3 +1,5 @@ +//! keep in sync with MimeToolsTestMessages.java + use crate::importer::importable_mail::{ImportableMail, MailContact}; use serde::Deserialize; use std::borrow::Cow; diff --git a/packages/node-mimimi/src/importer/importable_mail/plain_text_to_html_converter.rs b/packages/node-mimimi/src/importer/importable_mail/plain_text_to_html_converter.rs index 075c8718a44..167b31046ab 100644 --- a/packages/node-mimimi/src/importer/importable_mail/plain_text_to_html_converter.rs +++ b/packages/node-mimimi/src/importer/importable_mail/plain_text_to_html_converter.rs @@ -88,7 +88,7 @@ fn get_line_quote_level(line: String) -> i32 { /** * Adds and tags to the given html */ -pub fn add_html_page_tags(html: String) -> String { +fn add_html_page_tags(html: String) -> String { format!( "\r\n\ \r\n\ From 7a3c866c7cda9a1b04f89628f7f9b8aee609dc2a Mon Sep 17 00:00:00 2001 From: sug Date: Fri, 8 Nov 2024 17:57:52 +0100 Subject: [PATCH 09/32] wip: porting test from java 2 --- .../mime_string_to_importable_mail_test.rs | 120 +++++++++++++----- 1 file changed, 85 insertions(+), 35 deletions(-) diff --git a/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs b/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs index 58bcbcd931e..e5ccad42427 100644 --- a/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs +++ b/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs @@ -782,6 +782,7 @@ fn plain_body_text_parts_are_converted_to_html_body_parts_if_html_body_parts_fol assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() } #[test] @@ -800,82 +801,110 @@ Abc, die Katze liegt im Schnee ! Àâü?ß ! assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() } #[test] fn attachment_with_non_ascii_name() { - let msg = r#" + let msg = r#"Subject: text attachment +From: A +To: B +Date: " + new MailDateFormat().format(date) + " +Content-type: text/plain; charset=UTF-8; name=\"=?ISO-8859-1?Q?a=F6i=2Epdf?=\" +Content-Disposition: attachment; filename*=ISO-8859-1''%61%F6%69%2E%70%64%66 -"#; +Abc, die Katze liegt im Schnee ! Àâü?ß ! "#; let m: ImportableMail = parse_mail(msg); assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() } #[test] fn attachment_filename_in_content_type() { - let msg = r#" + let msg = r#"Subject: message with named file attachment +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-type: application/octet-stream; name=indirectly_attached.txt; +Content-Transfer-Encoding: base64 -"#; +Zmlyc3QgYXR0YWNobWVudA=="#; let m: ImportableMail = parse_mail(msg); assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() } #[test] fn attachment_filename_qencoding() { - let msg = r#" + let msg = r#"Subject: message with named file attachment +From: A +To: B +Date: " + new MailDateFormat().format(date) + " +Content-type: application/octet-stream; name==?utf-8?Q?=C3=A4=C3=B6=C3=9F=E2=82=AC.txt?=; +Content-Transfer-Encoding: base64 -"#; +Zmlyc3QgYXR0YWNobWVudA=="#; let m: ImportableMail = parse_mail(msg); assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() } #[test] fn encrypted() { - let msg = r#" + let msg = r#"Subject: Hello +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-type: multipart/encrypted; boundary=frontier -"#; +--frontier +Content-Type: application/octet-stream +Content-Transfer-Encoding: base64 + +SGFsbG8= +--frontier--"#; let m: ImportableMail = parse_mail(msg); assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() } #[test] -fn can_map_to_all_header_value() { - let msg = r#" - -"#; +fn recipient_groups() { + let msg = r#"Subject: Hello +From: A +To: foo:a@b.example.de,c@d.example.de,e@f.example.de; +Reply-To: ??? +Date: Thu, 7 Nov 2024 15:54:04 +0100"#; let m: ImportableMail = parse_mail(msg); assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() } #[test] -fn recipient_groups() { - let msg = r#" - -"#; +fn undisclosed_recipients() { + let msg = r#"To: undisclosed-recipients:;"#; let m: ImportableMail = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert!(m.to_addresses.is_empty()); } #[test] -fn undisclosed_recipients() { +fn can_map_to_all_header_value() { let msg = r#" "#; @@ -888,14 +917,19 @@ fn undisclosed_recipients() { #[test] fn long_content_type() { - let msg = r#" + let msg = r#"From: A +Content-type: multipart/mixed; boundary=frontier +--frontier +Content-Type: text/plain; charset=us-ascii; name=withoutContentType.pdf +Content-Disposition: attachment; filename=withoutContentType.pdf; + +Message +--frontier-- "#; let m: ImportableMail = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() } #[test] @@ -918,37 +952,53 @@ fn get_spf_result() { #[test] fn mail_from_with_delemiter() { let msg = r#" +Message-ID: 123456 +Subject: Hello +From: A,B +To: B +References: <1234564@web.de> +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-Type: multipart/mixed; boundary=frontier +--frontier "#; let m: ImportableMail = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() } #[test] fn incomplete_text_content_type() { let msg = r#" +Subject: Hello +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-type: text + +any body text + +--frontier "#; let m: ImportableMail = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() } #[test] fn calendar_content_type() { - let msg = r#" - + let msg = r#"Message-ID: 123456 +Subject: Hello +From: A +To: B +References: <1234564@web.de> +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-Type: text/calendar; charset=\"UTF-8\"; method=REQUEST "#; let m: ImportableMail = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() } #[test] From f4f093844da505791fbd2e4354eb96d74cf5402e Mon Sep 17 00:00:00 2001 From: map Date: Fri, 8 Nov 2024 18:11:51 +0100 Subject: [PATCH 10/32] prepared train ride --- .../mime_string_to_importable_mail_test.rs | 898 +++++++++++------- 1 file changed, 552 insertions(+), 346 deletions(-) diff --git a/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs b/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs index e5ccad42427..2d6038fc15f 100644 --- a/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs +++ b/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs @@ -5,30 +5,30 @@ use mail_parser::{MessageParser, MimeHeaders}; use tutasdk::date::DateTime; fn parse_mail(msg: &str) -> ImportableMail { - MessageParser::default() - .parse(msg) - .unwrap() - .try_into() - .unwrap() + MessageParser::default() + .parse(msg) + .unwrap() + .try_into() + .unwrap() } // to be able to convert any (str/string, str/string).into() => MailContact impl From<(N, A)> for MailContact where - N: ToString, - A: ToString, + N: ToString, + A: ToString, { - fn from((name, address): (N, A)) -> Self { - Self { - mail_address: address.to_string(), - name: name.to_string(), - } - } + fn from((name, address): (N, A)) -> Self { + Self { + mail_address: address.to_string(), + name: name.to_string(), + } + } } #[test] fn headers() { - let msg = r#"Message-ID: 123456 + let msg = r#"Message-ID: 123456 Subject: Hello From: A To: B @@ -38,90 +38,90 @@ In-Reply-To: <1234564@web.de> Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-Type: multipart/mixed; boundary=frontier "#; - println!("{}", msg); - let m = parse_mail(msg); - assert_eq!("123456", m.message_id.unwrap()); - assert_eq!( + println!("{}", msg); + let m = parse_mail(msg); + assert_eq!("123456", m.message_id.unwrap()); + assert_eq!( m.reply_to_addresses, vec![ ("Reply", "reply@tutanota.de").into(), ("Reply2", "reply2@tutanota.de").into(), ], ); - assert_eq!( + assert_eq!( m.references, vec!["sadf@tutanota.de".to_string(), "1234564@web.de".to_string()], ); - assert_eq!(Some("1234564@web.de".to_string()), m.in_reply_to); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(msg, m.headers_string); + assert_eq!(Some("1234564@web.de".to_string()), m.in_reply_to); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(msg, m.headers_string); } #[test] fn bad_frontier() { - let msg = "Content-Type: multipart/mixed; boundary=komma;ist;nicht;erlaubt\n"; - let parsed_message = MessageParser::default().parse(msg).unwrap(); - let attributes = parsed_message - .content_type() - .unwrap() - .attributes - .as_ref() - .unwrap(); - assert_eq!(attributes.as_slice(), [("boundary".into(), "komma".into())]); + let msg = "Content-Type: multipart/mixed; boundary=komma;ist;nicht;erlaubt\n"; + let parsed_message = MessageParser::default().parse(msg).unwrap(); + let attributes = parsed_message + .content_type() + .unwrap() + .attributes + .as_ref() + .unwrap(); + assert_eq!(attributes.as_slice(), [("boundary".into(), "komma".into())]); } #[test] fn empty_references() { - let msg = "Subject: Hello"; - let m = parse_mail(msg); - assert!(m.references.is_empty()); + let msg = "Subject: Hello"; + let m = parse_mail(msg); + assert!(m.references.is_empty()); } #[test] fn empty_in_reply_to() { - let msg = "Subject: Hello"; - let m = parse_mail(msg); - assert_eq!(None, m.in_reply_to); + let msg = "Subject: Hello"; + let m = parse_mail(msg); + assert_eq!(None, m.in_reply_to); } #[test] fn text_plain_us_ascii_7bit() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 US-ASCII: !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~"##; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(m.subject, "Hello",); - assert_eq!("US-ASCII: !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", m.html_body_text); - assert_eq!(m.date, Some(DateTime::from_millis(1730991244000))); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(m.subject, "Hello",); + assert_eq!("US-ASCII: !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", m.html_body_text); + assert_eq!(m.date, Some(DateTime::from_millis(1730991244000))); } #[test] fn text_plain_utf8bit() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/plain; charset=UTF-8 Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\{Β³|@"##; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("b", "b@tutanota.de").into()]); - assert_eq!("Hello", m.subject); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("b", "b@tutanota.de").into()]); + assert_eq!("Hello", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_utf_explicit_8bit() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -130,18 +130,18 @@ Content-Transfer-Encoding: 8bit Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@"##; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("b", "b@tutanota.de").into()]); - assert_eq!("Hello", m.subject); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("b", "b@tutanota.de").into()]); + assert_eq!("Hello", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_utf_quoted_printable() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -149,18 +149,18 @@ Content-type: text/plain; charset=UTF-8 Content-Transfer-Encoding: quoted-printable Tutanota: =C3=A4=C3=BC=C3=B6=C3=9F=E2=82=AC*#\{=C2=B3|@"##; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); - assert_eq!("Hello", m.subject); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!("Hello", m.subject); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_utf_base64() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -168,18 +168,18 @@ Content-type: text/plain; charset=UTF-8 Content-Transfer-Encoding: base64 VHV0YW5vdGE6IMOkw7zDtsOf4oKsKiNce8KzfEA="##; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!("Hello", m.subject); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Hello", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_utf_invalid_base64() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -187,176 +187,176 @@ Content-type: text/plain; charset=UTF-8 Content-Transfer-Encoding: base64 VHV0YW5vdGE6IMOkw7zDtsOf4oKsKiNce8KzfEA"##; // skip the padding "=" to force an exception - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!("Hello", m.subject); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Hello", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_format_flowed() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/plain; charset=UTF-8; format=flowed Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es \r\neinen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!"#; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!("Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es einen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!", m.html_body_text); + assert_eq!("Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es einen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!", m.html_body_text); } #[test] fn text_plain_format_flowed_del_sp() { - let msg = r#"From: A + let msg = r#"From: A To: B Subject: Hello Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/plain; charset=UTF-8; format=flowed; DelSp=yes Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es \r\neinen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!"#; - let m = parse_mail(msg); - assert_eq!( - "Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt eseinen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!", - m.html_body_text); + let m = parse_mail(msg); + assert_eq!( + "Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt eseinen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!", + m.html_body_text); } #[test] fn text_plain_subject_encoded_word_qencoding() { - let msg = r#"Subject: Hello =?ISO-8859-1?Q?=E4=F6=FC=DF?=abc + let msg = r#"Subject: Hello =?ISO-8859-1?Q?=E4=F6=FC=DF?=abc From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 "#; - let m = parse_mail(msg); - assert_eq!("Hello Àâüßabc", m.subject); + let m = parse_mail(msg); + assert_eq!("Hello Àâüßabc", m.subject); } #[test] fn text_plain_subject_encoded_word_qencoding_turkish() { - let msg = r#"Subject: =?iso-8859-9?Q?Paracard Hesap =D6zeti?= + let msg = r#"Subject: =?iso-8859-9?Q?Paracard Hesap =D6zeti?= From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 "#; - let m = parse_mail(msg); - assert_eq!("Paracard Hesap Γ–zeti", m.subject); + let m = parse_mail(msg); + assert_eq!("Paracard Hesap Γ–zeti", m.subject); } #[test] fn from_encoded_word_qencoding() { - let msg = r#"Subject: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0?==?utf-8?Q?=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= + let msg = r#"Subject: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0?==?utf-8?Q?=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= From: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0?==?utf-8?Q?=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= To: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0?==?utf-8?Q?=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= Date: Thu, 7 Nov 2024 15:54:04 +0100 "#; - let m = parse_mail(msg); - assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.subject); - assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.from_addresses[0].name); - assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.to_addresses[0].name); + let m = parse_mail(msg); + assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.subject); + assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.from_addresses[0].name); + assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.to_addresses[0].name); } #[test] fn from_encoded_word_qencoding_colon() { - let msg = r#"Subject: Hi + let msg = r#"Subject: Hi From: =?utf-8?Q?=D0=9B=D0=B8=D1=82=D1=80=D0=B5=D1=81=3A=20=D0=A1=D0=B0=D0=BC=D0=B8=D0=B7=D0=B4=D0=B0=D1=82?= "#; - let m = parse_mail(msg); - assert_eq!( - m.from_addresses[0], - ("ЛитрСс: Π‘Π°ΠΌΠΈΠ·Π΄Π°Ρ‚", "mail@selfpub.ru").into() - ); + let m = parse_mail(msg); + assert_eq!( + m.from_addresses[0], + ("ЛитрСс: Π‘Π°ΠΌΠΈΠ·Π΄Π°Ρ‚", "mail@selfpub.ru").into() + ); } #[test] fn recipients_encoded_word_qencoding_colon() { - let msg = "To: =?utf-8?Q?=D0=9B=D0=B8=D1=82=D1=80=D0=B5=D1=81=3A=20=D0=A1=D0=B0=D0=BC=D0=B8=D0=B7=D0=B4=D0=B0=D1=82?= \n"; - let m = parse_mail(msg); - assert_eq!( - m.from_addresses[0], - ("ЛитрСс: Π‘Π°ΠΌΠΈΠ·Π΄Π°Ρ‚", "mail@selfpub.ru").into() - ); + let msg = "To: =?utf-8?Q?=D0=9B=D0=B8=D1=82=D1=80=D0=B5=D1=81=3A=20=D0=A1=D0=B0=D0=BC=D0=B8=D0=B7=D0=B4=D0=B0=D1=82?= \n"; + let m = parse_mail(msg); + assert_eq!( + m.from_addresses[0], + ("ЛитрСс: Π‘Π°ΠΌΠΈΠ·Π΄Π°Ρ‚", "mail@selfpub.ru").into() + ); } #[test] fn recipients_encoded_word_qencoding_partly() { - let msg = "To: =?ISO-8859-1?Q?Andr=E9?= Pirard \n"; - let m = parse_mail(msg); - assert_eq!( - m.from_addresses[0], - ("AndrΓ© Pirard", "PIRARD@vm1.ulg.ac.be").into() - ); + let msg = "To: =?ISO-8859-1?Q?Andr=E9?= Pirard \n"; + let m = parse_mail(msg); + assert_eq!( + m.from_addresses[0], + ("AndrΓ© Pirard", "PIRARD@vm1.ulg.ac.be").into() + ); } #[test] fn text_plain_subject_encoded_word_base64() { - let msg = r#"Subject: =?utf-8?B?SGVsbG8gw6TDtsO8w58=?= + let msg = r#"Subject: =?utf-8?B?SGVsbG8gw6TDtsO8w58=?= From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 "#; - let m = parse_mail(msg); - assert_eq!("Hello Àâüß", m.subject); + let m = parse_mail(msg); + assert_eq!("Hello Àâüß", m.subject); } #[test] fn text_html_only() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/html; charset=UTF-8 Hello Àâüß
"#; - let m = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!( - "Hello Àâüß
", - m.html_body_text - ); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!( + "Hello Àâüß
", + m.html_body_text + ); } #[test] fn charset() { - // todo!() + // todo!() } #[test] fn text_html_inline_charset_definition_utf8() { - let msg = r#"Content-type: text/html + let msg = r#"Content-type: text/html Content-Transfer-Encoding: 8bit

Π‘Π»Π°Π³ΠΎΠ΄Π°Ρ€ΠΈΠΌ Π’ΠΈ

"#; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!( - "

Π‘Π»Π°Π³ΠΎΠ΄Π°Ρ€ΠΈΠΌ Π’ΠΈ

", - m.html_body_text - ); + assert_eq!( + "

Π‘Π»Π°Π³ΠΎΠ΄Π°Ρ€ΠΈΠΌ Π’ΠΈ

", + m.html_body_text + ); } #[test] fn text_html_inline_charset_definition_western() { - let msg = r#"Content-type: text/html + let msg = r#"Content-type: text/html Content-Transfer-Encoding: base64 PGh0bWw+PGhlYWQ+PG1ldGEgY2hhcnNldD0iSVNPLTg4NTktMTUiPjwvaGVhZD48Ym9keT48cD6kIPbkPC9wPjwvYm9keT48L2h0bWw+"#; - let m = parse_mail(msg); - assert_eq!( - "

€ ΓΆΓ€

", - m.html_body_text - ); + let m = parse_mail(msg); + assert_eq!( + "

€ ΓΆΓ€

", + m.html_body_text + ); } #[test] fn text_alternative() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 (CET) @@ -372,60 +372,60 @@ Content-type: text/html; charset=UTF-8; Hello Àâüß
--frontier-- "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!("Hello", m.subject); - assert_eq!( - "Hello Àâüß
", - m.html_body_text - ); + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!("Hello", m.subject); + assert_eq!( + "Hello Àâüß
", + m.html_body_text + ); } #[test] fn invalid_domains_in_mail_addresses() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B , C , D "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!( - m.to_addresses, - vec![ - ("B", "b@a.example").into(), - ("C", "c@c.com").into(), - ("D", "d@d.invalid").into() - ] - ); + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!( + m.to_addresses, + vec![ + ("B", "b@a.example").into(), + ("C", "c@c.com").into(), + ("D", "d@d.invalid").into() + ] + ); } #[test] fn multiple_to_headers() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B , C To: D "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!( - m.to_addresses, - vec![ - ("B", "b@b.org").into(), - ("C", "c@c.com").into(), - ("D", "d@d.net").into() - ] - ); + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!( + m.to_addresses, + vec![ + ("B", "b@b.org").into(), + ("C", "c@c.com").into(), + ("D", "d@d.net").into() + ] + ); } #[test] fn attached_message() { - let msg = r#"Subject: parent message + let msg = r#"Subject: parent message From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -448,35 +448,55 @@ Content-type: text/plain; charset=UTF-8; Hello Àâüß "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(m.subject, "parent message"); - assert_eq!(m.html_body_text, "normal message"); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - - assert_eq!(m.attachments.len(), 1); - for attachment in m.attachments { - match attachment { - ImportableMailAttachment::Attachment { .. } => { - // todo: - // should we have got the attachment here or in AttachedMessage? - }, - ImportableMailAttachment::AttachedMessage { message: attached } => { - assert_eq!(attached.from_addresses, vec![("D", "d@tutanota.de").into()]); - assert_eq!(attached.to_addresses, vec![("E", "e@tutanota.de").into()]); - assert_eq!(attached.subject, "attached message"); - assert_eq!(attached.html_body_text, "Hello Àâüß"); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date) - }, - } - } + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(m.subject, "parent message"); + assert_eq!(m.html_body_text, "normal message"); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + + assert_eq!(m.attachments.len(), 1); + for attachment in m.attachments { + match attachment { + ImportableMailAttachment::Attachment { .. } => { + // todo: + // should we have got the attachment here or in AttachedMessage? + } + ImportableMailAttachment::AttachedMessage { message: attached } => { + assert_eq!(attached.from_addresses, vec![("D", "d@tutanota.de").into()]); + assert_eq!(attached.to_addresses, vec![("E", "e@tutanota.de").into()]); + assert_eq!(attached.subject, "attached message"); + assert_eq!(attached.html_body_text, "Hello Àâüß"); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date) + } + } + } + +// assertEquals(new SmtpMailContact("A", "a@tutanota.de"), m.getSender()); + // assertEquals(List.of(new SmtpMailContact("B", "b@tutanota.de")), m.getToRecipients()); + // assertEquals("parent message", m.getSubject()); + // assertEquals("normal message", m.getPlainBodyText()); + // assertEquals(null, m.getHtmlBodyText()); + // assertEquals(date, m.getSentDate()); + // + // assertEquals(0, m.getAttachedMessages().size()); + // assertEquals(1, m.getAttachedFiles().size()); + // SmtpAttachment attachement = m.getAttachedFiles().get(0); + // SmtpMessage attached = mimeStringToSmtpMessageConverter.mimeToSmtpMessage(mimeStringToSmtpMessageConverter.dataToMimeMessage(attachement.getData()), + // null); + // + // assertEquals(new SmtpMailContact("D", "d@tutanota.de"), attached.getSender()); + // assertEquals(List.of(new SmtpMailContact("E", "e@tutanota.de")), attached.getToRecipients()); + // assertEquals("attached message", attached.getSubject()); + // assertEquals("Hello Àâüß", attached.getPlainBodyText()); + // assertEquals(null, attached.getHtmlBodyText()); + // assertEquals(yesterday, attached.getSentDate()); } #[test] fn attachments() { - let msg = r#"Subject: multiple attachments + let msg = r#"Subject: multiple attachments From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -505,19 +525,49 @@ Content-Disposition: attachment; filename=withoutContentType.pdf; c2Vjb25kIGF0dGFjaG1lbnQ= --frontier-- "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(m.subject, "multiple attachments"); - - assert_eq!(m.attachments.len(), 3); - todo!() + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(m.subject, "multiple attachments"); + + assert_eq!(m.attachments.len(), 3); + todo!() + +// assertEquals("a@tutanota.de", m.getSender().getMailAddress()); + // assertEquals("A", m.getSender().getName()); + // assertEquals(1, m.getToRecipients().size()); + // assertEquals("b@tutanota.de", m.getToRecipients().get(0).getMailAddress()); + // assertEquals("B", m.getToRecipients().get(0).getName()); + // assertEquals("multiple attachments", m.getSubject()); + // assertEquals("Hello Àâüß", m.getPlainBodyText()); + // assertEquals(null, m.getHtmlBodyText()); + // assertEquals(date, m.getSentDate()); + // + // assertEquals(3, m.getAttachedFiles().size()); + // SmtpAttachment a1 = m.getAttachedFiles().get(0); + // SmtpAttachment a2 = m.getAttachedFiles().get(1); + // SmtpAttachment a3 = m.getAttachedFiles().get(2); + // + // assertEquals("a1.txt", a1.getName()); + // assertArrayEquals(EncodingConverter.base64ToBytes("Zmlyc3QgYXR0YWNobWVudA=="), a1.getData()); + // assertEquals("application/octet-stream", a1.getMimeType()); + // assertNull(a1.getCharset()); + // + // assertEquals("a2.pdf", a2.getName()); + // assertArrayEquals(EncodingConverter.base64ToBytes("c2Vjb25kIGF0dGFjaG1lbnQ="), a2.getData()); + // assertEquals("application/pdf", a2.getMimeType()); + // assertNull(a1.getCharset()); + // + // assertEquals("withoutContentType.pdf", a3.getName()); + // assertArrayEquals(EncodingConverter.base64ToBytes("c2Vjb25kIGF0dGFjaG1lbnQ="), a3.getData()); + // assertEquals("text/plain", a3.getMimeType()); + // assertNull(a1.getCharset()); } #[test] fn inline_attachment() { - let msg = r#"Subject: inline attachment + let msg = r#"Subject: inline attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -536,18 +586,27 @@ Content-ID: <123@tutanota.de>; Zmlyc3QgYXR0YWNobWVudA== --frontier-- "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - - todo!() + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + + todo!() + +// assertEquals(1, m.getAttachedFiles().size()); + // SmtpAttachment a1 = m.getAttachedFiles().get(0); + // + // assertEquals("a1.png", a1.getName()); + // assertArrayEquals(EncodingConverter.base64ToBytes("Zmlyc3QgYXR0YWNobWVudA=="), a1.getData()); + // assertEquals("application/octet-stream", a1.getMimeType()); + // assertEquals("123@tutanota.de", a1.getContentId()); + // assertNull(a1.getCharset()); } #[test] fn attachment_to_attached_message() { - let msg = r#"Subject: parent message + let msg = r#"Subject: parent message From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -563,17 +622,38 @@ Content-Disposition: attachment; filename=indirectly_attached.txt; Zmlyc3QgYXR0YWNobWVudA== "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - todo!() + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() + +// assertEquals(null, m.getPlainBodyText()); + // assertEquals(null, m.getHtmlBodyText()); + // + // // messages are handled as attached files. + // assertEquals(0, m.getAttachedMessages().size()); + // assertEquals(1, m.getAttachedFiles().size()); + // + // SmtpAttachment attachedFile = m.getAttachedFiles().get(0); + // SmtpMessage attached = mimeStringToSmtpMessageConverter.mimeToSmtpMessage(mimeStringToSmtpMessageConverter.dataToMimeMessage(attachedFile.getData()), + // null); + // + // assertEquals("attached message", attached.getSubject()); + // assertEquals(null, attached.getHtmlBodyText()); + // + // assertEquals(0, attached.getAttachedMessages().size()); + // assertEquals(1, attached.getAttachedFiles().size()); + // + // SmtpAttachment indirectAttachment = attached.getAttachedFiles().get(0); + // assertEquals("indirectly_attached.txt", indirectAttachment.getName()); + // assertArrayEquals(EncodingConverter.base64ToBytes("Zmlyc3QgYXR0YWNobWVudA=="), indirectAttachment.getData()); } #[test] fn text_attachment() { - let msg = r#"Subject: text attachment + let msg = r#"Subject: text attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -590,17 +670,28 @@ Content-Disposition: attachment; filename=a1.txt; Abc, die Katze liegt im Schnee ! Àâü?ß ! --frontier-- "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - todo!() + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() + +// assertEquals("text attachment", m.getSubject()); + // assertEquals("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.getPlainBodyText()); + // assertEquals(null, m.getHtmlBodyText()); + // assertEquals(date, m.getSentDate()); + // + // assertEquals(1, m.getAttachedFiles().size()); + // SmtpAttachment a1 = m.getAttachedFiles().get(0); + // + // assertEquals("a1.txt", a1.getName()); + // assertArrayEquals("Abc, die Katze liegt im Schnee ! Àâü?ß ! ".getBytes("UTF-8"), a1.getData()); } #[test] fn html_attachment() { - let msg = r#"Subject: html attachment + let msg = r#"Subject: html attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -617,17 +708,28 @@ Content-Disposition: attachment; filename=a1.html; Hello Àâüß
--frontier-- "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - todo!() + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() + +// assertEquals("html attachment", m.getSubject()); + // assertEquals("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.getPlainBodyText()); + // assertEquals(null, m.getHtmlBodyText()); + // assertEquals(date, m.getSentDate()); + // + // assertEquals(1, m.getAttachedFiles().size()); + // SmtpAttachment a1 = m.getAttachedFiles().get(0); + // + // assertEquals("a1.html", a1.getName()); + // assertArrayEquals("Hello Àâüß
".getBytes("UTF-8"), a1.getData()); } #[test] fn multiple_plain_body_text_parts_are_concatenated() { - let eml_contents = r#"Message-Id: some-id + let eml_contents = r#"Message-Id: some-id From: A To: B Date: Tue, 5 Nov 2024 13:18:59 +0000 @@ -645,24 +747,55 @@ second plain text in body --line-- "#; - let parsed_message = MessageParser::default() - .with_mime_headers() - .parse(eml_contents) - .unwrap(); - let text_contents = parsed_message - .text_bodies() - .map(|a| a.text_contents().unwrap()) - .collect::>() - .join(""); - assert_eq!( - "first plain text in body\nsecond plain text in body", - text_contents - ); + let parsed_message = MessageParser::default() + .with_mime_headers() + .parse(eml_contents) + .unwrap(); + let text_contents = parsed_message + .text_bodies() + .map(|a| a.text_contents().unwrap()) + .collect::>() + .join(""); + assert_eq!( + "first plain text in body\nsecond plain text in body", + text_contents + ); + +// String firstPlainBodyText = "Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@"; + // String secondPlainBodyText = "Abc, die Katze liegt im Schnee ! Àâü?ß !"; + // StringBuilder b = new StringBuilder() + // .append("Subject: multiple text/plain parts concatenated\n") + // .append("From: A \n") + // .append("To: B \n") + // .append("Date: " + new MailDateFormat().format(date) + "\n") + // .append("Content-Type: multipart/mixed; boundary=frontier\n") + // .append("\n") + // .append("--frontier\n") + // .append("Content-type: text/plain; charset=UTF-8\n") + // .append("\n") + // .append(firstPlainBodyText + "\n") + // .append("--frontier\n") + // .append("Content-type: text/plain; charset=UTF-8\n") + // .append("\n") + // .append(secondPlainBodyText + "\n") + // .append("--frontier--"); + // + // SmtpMessage m = mimeStringToSmtpMessageConverter.mimeToSmtpMessage( + // mimeStringToSmtpMessageConverter.dataToMimeMessage(b.toString().getBytes(StandardCharsets.UTF_8)), + // null + // ); + // + // assertEquals("multiple text/plain parts concatenated", m.getSubject()); + // String concatenatedPlainBodyText = firstPlainBodyText.concat(secondPlainBodyText); + // assertEquals(concatenatedPlainBodyText, m.getPlainBodyText()); + // assertEquals(null, m.getHtmlBodyText()); + // assertEquals(date, m.getSentDate()); + // assertEquals(0, m.getAttachedFiles().size()); } #[test] fn multiple_html_body_text_parts_are_concatenated() { - let msg = r#"Message-Id: some-id + let msg = r#"Message-Id: some-id Subject: multiple text/html parts concatenated From: A To: B @@ -680,18 +813,18 @@ Content-type: text/html; charset=UTF-8 --frontier-- "#; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!("multiple text/html parts concatenated", m.subject); - assert_eq!("Hello Àâüß
Test Test
", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(0, m.attachments.len()); + assert_eq!("multiple text/html parts concatenated", m.subject); + assert_eq!("Hello Àâüß
Test Test
", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(0, m.attachments.len()); } #[test] // todo! what does this test (map) fn concatenate_alternative_html_text_parts() { - let msg = r#"Message-Id: some-id + let msg = r#"Message-Id: some-id From: A To: B Date: Tue, 5 Nov 2024 13:18:59 +0000 @@ -709,14 +842,14 @@ Content-Type: text/html; charset=UTF-8 --line-- "#; - let m = parse_mail(msg); - todo!() + let m = parse_mail(msg); + todo!() } #[test] // todo! what does this test (map) fn concatenate_multiple_html_and_plain_text_parts() { - let msg = r#"Message-Id: some-id + let msg = r#"Message-Id: some-id From: A To: B Date: Tue, 5 Nov 2024 13:18:59 +0000 @@ -736,14 +869,14 @@ first plain text in body --line-- "#; - let m = parse_mail(msg); - todo!() + let m = parse_mail(msg); + todo!() } #[test] fn plain_body_text_parts_are_concatenated_with_html_body_parts_if_html_body_parts_already_existing() { - let msg = r#"Subject: multiple text/html and text/plain parts concatenated to single text/html + let msg = r#"Subject: multiple text/html and text/plain parts concatenated to single text/html From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -763,31 +896,45 @@ Content-type: text/plain; charset=UTF-8 Abc, die Katze liegt im Schnee ! Àâü?ß ! --frontier- "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!( - "multiple text/html and text/plain parts concatenated to single text/html", - m.subject - ); - todo!() + let m: ImportableMail = parse_mail(msg); + + assert_eq!( + "multiple text/html and text/plain parts concatenated to single text/html", + m.subject + ); + todo!() + +// assertEquals("multiple text/html and text/plain parts concatenated to single text/html", m.getSubject()); + // String concatenatedHtmlBodyText = firstHtmlBodyText.concat(secondHtmlBodyText).concat(PlainTextHtmlConverter.plainTextToHtml(firstPlainBodyText)); + // assertEquals(null, m.getPlainBodyText()); + // assertEquals(concatenatedHtmlBodyText, m.getHtmlBodyText()); + // assertEquals(date, m.getSentDate()); + // assertEquals(0, m.getAttachedFiles().size()); } #[test] fn plain_body_text_parts_are_converted_to_html_body_parts_if_html_body_parts_follow_afterwards() { - let msg = r#" + let msg = r#" "#; - let m: ImportableMail = parse_mail(msg); + let m: ImportableMail = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); todo!() + +// assertEquals("multiple plain/text and text/html parts concatenated to single text/html", m.getSubject()); + // String concatenatedHtmlBodyText = PlainTextHtmlConverter.plainTextToHtml(firstPlainBodyText).concat(firstHtmlBodyText).concat(secondHtmlBodyText); + // assertEquals(null, m.getPlainBodyText()); + // assertEquals(concatenatedHtmlBodyText, m.getHtmlBodyText()); + // assertEquals(date, m.getSentDate()); + // assertEquals(0, m.getAttachedFiles().size()); } #[test] fn text_attachment_with_disposition() { - let msg = r#"Subject: text attachment + let msg = r#"Subject: text attachment From: A To: B Date: " + new MailDateFormat().format(date) + " @@ -796,12 +943,23 @@ Content-Disposition: attachment; filename=a1.txt; Abc, die Katze liegt im Schnee ! Àâü?ß ! "#; - let m: ImportableMail = parse_mail(msg); + let m: ImportableMail = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); todo!() + +// assertEquals("text attachment", m.getSubject()); + // assertNull(m.getPlainBodyText()); + // assertNull(m.getHtmlBodyText()); + // assertEquals(date, m.getSentDate()); + // + // assertEquals(1, m.getAttachedFiles().size()); + // SmtpAttachment a1 = m.getAttachedFiles().get(0); + // + // assertEquals("a1.txt", a1.getName()); + // assertArrayEquals("Abc, die Katze liegt im Schnee ! Àâü?ß ! ".getBytes("UTF-8"), a1.getData()); } #[test] @@ -814,12 +972,15 @@ Content-type: text/plain; charset=UTF-8; name=\"=?ISO-8859-1?Q?a=F6i=2Epdf?=\" Content-Disposition: attachment; filename*=ISO-8859-1''%61%F6%69%2E%70%64%66 Abc, die Katze liegt im Schnee ! Àâü?ß ! "#; - let m: ImportableMail = parse_mail(msg); + let m: ImportableMail = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); todo!() + +// SmtpAttachment a1 = m.getAttachedFiles().get(0); + // assertEquals("aΓΆi.pdf", a1.getName()); } #[test] @@ -832,12 +993,15 @@ Content-type: application/octet-stream; name=indirectly_attached.txt; Content-Transfer-Encoding: base64 Zmlyc3QgYXR0YWNobWVudA=="#; - let m: ImportableMail = parse_mail(msg); + let m: ImportableMail = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); todo!() +// SmtpAttachment indirectAttachment = m.getAttachedFiles().get(0); + // assertEquals("indirectly_attached.txt", indirectAttachment.getName()); + // assertArrayEquals(EncodingConverter.base64ToBytes("Zmlyc3QgYXR0YWNobWVudA=="), indirectAttachment.getData()); } #[test] @@ -850,12 +1014,16 @@ Content-type: application/octet-stream; name==?utf-8?Q?=C3=A4=C3=B6=C3=9F=E2=82= Content-Transfer-Encoding: base64 Zmlyc3QgYXR0YWNobWVudA=="#; - let m: ImportableMail = parse_mail(msg); + let m: ImportableMail = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); todo!() + +// SmtpAttachment indirectAttachment = m.getAttachedFiles().get(0); + // assertEquals("Γ€ΓΆΓŸβ‚¬.txt", indirectAttachment.getName()); + // assertArrayEquals(EncodingConverter.base64ToBytes("Zmlyc3QgYXR0YWNobWVudA=="), indirectAttachment.getData()); } #[test] @@ -872,12 +1040,14 @@ Content-Transfer-Encoding: base64 SGFsbG8= --frontier--"#; - let m: ImportableMail = parse_mail(msg); + let m: ImportableMail = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); todo!() + +// assertEquals(1, m.getAttachedFiles().size()); } #[test] @@ -887,12 +1057,16 @@ From: A To: foo:a@b.example.de,c@d.example.de,e@f.example.de; Reply-To: ??? Date: Thu, 7 Nov 2024 15:54:04 +0100"#; - let m: ImportableMail = parse_mail(msg); + let m: ImportableMail = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); todo!() + +// assertEquals("a@b.example.de", m.getToRecipients().get(0).getMailAddress()); + // assertEquals("c@d.example.de", m.getToRecipients().get(1).getMailAddress()); + // assertEquals("e@f.example.de", m.getToRecipients().get(2).getMailAddress()); } #[test] @@ -903,18 +1077,6 @@ fn undisclosed_recipients() { assert!(m.to_addresses.is_empty()); } -#[test] -fn can_map_to_all_header_value() { - let msg = r#" - -"#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); -} - #[test] fn long_content_type() { let msg = r#"From: A @@ -927,31 +1089,51 @@ Content-Disposition: attachment; filename=withoutContentType.pdf; Message --frontier-- "#; - let m: ImportableMail = parse_mail(msg); + let m: ImportableMail = parse_mail(msg); todo!() +// assertEquals("text/plain", m.getAttachedFiles().get(0).getMimeType()); + // assertEquals("us-ascii", m.getAttachedFiles().get(0).getCharset()); + // assertEquals("withoutContentType.pdf", m.getAttachedFiles().get(0).getName()); } #[test] fn normalize_header_value() { - let msg = r#" - -"#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() +// // trim and remove LF and CR + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" ").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" ").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" \r \n ").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" \n \r ").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds("\n\r \r\n").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" | \n\r | ").toArray(new String[0])); + // assertArrayEquals(new String[0], MimeStringToSmtpMessageConverter.stripMessageIds(" <>").toArray(new String[0])); + // + // // remove comments + // assertArrayEquals(new String[0], MimeStringToSmtpMessageConverter.stripMessageIds(" (abc)").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" (abc) ").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" (abc) (def) ").toArray(new String[0])); + // + // // illegal comments + // assertArrayEquals(new String[0], MimeStringToSmtpMessageConverter.stripMessageIds(" (abc").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" abc) ").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" abc (abc").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" )abc def( ").toArray(new String[0])); + // + // // ids in comments are currently recognized + // assertArrayEquals(new String[]{"a@b", "g@h", "i@d"}, + // MimeStringToSmtpMessageConverter.stripMessageIds(" (abc) (def) ()").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" (abc def) ").toArray(new String[0])); } #[test] fn get_spf_result() { - // net yet used on rust + // net yet used on rust } #[test] fn mail_from_with_delemiter() { - let msg = r#" + let msg = r#" Message-ID: 123456 Subject: Hello From: A,B @@ -962,14 +1144,17 @@ Content-Type: multipart/mixed; boundary=frontier --frontier "#; - let m: ImportableMail = parse_mail(msg); + let m: ImportableMail = parse_mail(msg); todo!() +// assertEquals("A, B ", m.getSender().getMailAddress()); + // assertEquals("", m.getSender().getName()); + // assertFalse(m.getSender().isValid()); } #[test] fn incomplete_text_content_type() { - let msg = r#" + let msg = r#" Subject: Hello From: A To: B @@ -981,9 +1166,10 @@ any body text --frontier "#; - let m: ImportableMail = parse_mail(msg); + let m: ImportableMail = parse_mail(msg); todo!() +// assertEquals("any body text", m.getPlainBodyText()); } #[test] @@ -996,31 +1182,51 @@ References: <1234564@web.de> Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-Type: text/calendar; charset=\"UTF-8\"; method=REQUEST "#; - let m: ImportableMail = parse_mail(msg); + let m: ImportableMail = parse_mail(msg); todo!() +// assertEquals("a@tutanota.de", m.getSender().getMailAddress()); + // assertEquals("A", m.getSender().getName()); + // assertEquals("text/calendar", m.getAttachedFiles().get(0).getMimeType()); + // assertEquals("REQUEST", m.getAttachedFiles().get(0).getCalendarMethod()); } #[test] fn calendar_content_type_method() { - let msg = r#" - + let msg = r#"Message-ID: 123456 +Subject: Hello +From: A +To: B +References: <1234564@web.de> +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-Type: text/calendar; charset="UTF-8"; method=request; "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + match m.attachments.first().unwrap() { + ImportableMailAttachment::Attachment { content_type, .. } => { + assert_eq!("text/calendar", content_type); + // todo! assert_eq!("REQUEST", calendar_method) + } + ImportableMailAttachment::AttachedMessage { .. } => { panic!("") } + }; } #[test] fn invalid_content_types_default_to_text_plain() { - let msg = r#" - -"#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + let invalid_content_types = vec![ + "Content-Type:", + "Content-Type: _", + "Content-Type: text", + "Content-Type; text/html", + "Content-Type; invalid/type", + "Content-Type: application/pdf; no_parameter_name.pdf" + ]; + for invalid_content_type in invalid_content_types { + let parsed = MessageParser::default() + .parse(invalid_content_type) + .unwrap(); + assert_eq!("text/plain", parsed.content_type().unwrap().c_type.to_string()); + } } From 94b33e4d3f59c14de4a6c26111e9df8d585c72b1 Mon Sep 17 00:00:00 2001 From: nig Date: Mon, 11 Nov 2024 11:08:13 +0100 Subject: [PATCH 11/32] add reduce_to_chunks utility --- packages/node-mimimi/src/importer.rs | 71 +++++++++++------- packages/node-mimimi/src/lib.rs | 1 + packages/node-mimimi/src/reduce_to_chunks.rs | 76 ++++++++++++++++++++ 3 files changed, 122 insertions(+), 26 deletions(-) create mode 100644 packages/node-mimimi/src/reduce_to_chunks.rs diff --git a/packages/node-mimimi/src/importer.rs b/packages/node-mimimi/src/importer.rs index 74a3a95d8e7..0b46907ee1e 100644 --- a/packages/node-mimimi/src/importer.rs +++ b/packages/node-mimimi/src/importer.rs @@ -2,13 +2,16 @@ use crate::importer::file_reader::import_client::{FileImport, FileIterationError use crate::importer::imap_reader::import_client::{ImapImport, ImapIterationError}; use crate::importer::imap_reader::ImapImportConfig; use crate::importer::importable_mail::ImportableMail; +use crate::reduce_to_chunks::reduce_to_chunks; use crate::tuta::credentials::TutaCredentials; use napi::bindgen_prelude::Error as NapiError; +use serde::Serialize; use std::sync::Arc; use tutasdk::crypto::aes::Iv; use tutasdk::crypto::key::GenericAesKey; use tutasdk::crypto::randomizer_facade::RandomizerFacade; -use tutasdk::entities::generated::tutanota::{ImportMailData, ImportMailPostIn, ImportMailPostOut}; +use tutasdk::entities::generated::tutanota::{ImportMailData, ImportMailPostIn}; +use tutasdk::entities::json_size_estimator::estimate_json_size; use tutasdk::login::Credentials; use tutasdk::net::native_rest_client::NativeRestClient; use tutasdk::services::generated::tutanota::ImportMailService; @@ -100,7 +103,7 @@ impl Importer { }; let import_res = match next_importable_mail { - Ok(next_importable_mail) => self.import_one_mail(next_importable_mail).await, + Ok(next_importable_mail) => self.import_all_mail(vec![next_importable_mail]).await, // source says, all the iteration have ended, Err(IterationError::File(FileIterationError::SourceEnd)) @@ -144,10 +147,10 @@ impl Importer { /// once we get the ImportableMail from either of source, /// continue to the uploading counterpart - async fn import_one_mail( + async fn import_all_mail( &self, - importable_mail: ImportableMail, - ) -> Result { + importable_mail: Vec, + ) -> Result, ()> { let new_aes_256_key = GenericAesKey::from_bytes( self.randomizer_facade .generate_random_array::<{ tutasdk::crypto::aes::AES_256_KEY_SIZE }>() @@ -162,31 +165,47 @@ impl Importer { let owner_enc_session_key = mail_group_key.encrypt_key(&new_aes_256_key, Iv::generate(&self.randomizer_facade)); - let import_mail_data = ImportMailData::from(importable_mail); - let import_mail_post_in = ImportMailPostIn { - ownerEncSessionKey: owner_enc_session_key.object, - ownerGroup: self.target_owner_group.clone(), - ownerKeyVersion: owner_enc_session_key.version, - imports: vec![import_mail_data], - targetMailFolder: self.target_mail_folder.clone(), - _format: 0, - _errors: None, - _finalIvs: Default::default(), + let import_count = importable_mail.len(); + let all_imports = importable_mail + .into_iter() + .map(ImportMailData::from) + .collect(); + + const MAX_REQUEST_SIZE: usize = 1024 * 1024 * 10; + let Ok(import_chunks) = reduce_to_chunks(all_imports, MAX_REQUEST_SIZE, estimate_json_size) + else { + // one of the elements does not fit into a chunk + return Err(()); }; + let mut mails: Vec = Vec::with_capacity(import_count); + for imports in import_chunks { + let import_mail_post_in = ImportMailPostIn { + ownerEncSessionKey: owner_enc_session_key.object.clone(), + ownerGroup: self.target_owner_group.clone(), + ownerKeyVersion: owner_enc_session_key.version, + imports, + targetMailFolder: self.target_mail_folder.clone(), + _format: 0, + _errors: None, + _finalIvs: Default::default(), + }; - let service_params = ExtraServiceParams { - session_key: Some(new_aes_256_key), - ..Default::default() - }; + let service_params = ExtraServiceParams { + session_key: Some(new_aes_256_key.clone()), + ..Default::default() + }; - let import_mail_post_out = self - .logged_in_sdk - .get_service_executor() - .post::(import_mail_post_in, service_params) - .await - .expect("Cannot execute ImportMailService"); + let mut import_mail_post_out = self + .logged_in_sdk + .get_service_executor() + .post::(import_mail_post_in, service_params) + .await + .expect("Cannot execute ImportMailService"); + + mails.append(&mut import_mail_post_out.mails); + } - Ok(import_mail_post_out) + Ok(mails) } } diff --git a/packages/node-mimimi/src/lib.rs b/packages/node-mimimi/src/lib.rs index a1733b1c0fa..194c50bfb03 100644 --- a/packages/node-mimimi/src/lib.rs +++ b/packages/node-mimimi/src/lib.rs @@ -5,3 +5,4 @@ pub mod importer; pub mod logging; pub mod tuta; mod tuta_imap; +mod reduce_to_chunks; diff --git a/packages/node-mimimi/src/reduce_to_chunks.rs b/packages/node-mimimi/src/reduce_to_chunks.rs new file mode 100644 index 00000000000..6fd4cce16cf --- /dev/null +++ b/packages/node-mimimi/src/reduce_to_chunks.rs @@ -0,0 +1,76 @@ +/// split a given vector of elements into a vector of chunks not exceeding max_size, where the +/// chunks size is calculated by summing up the elements sizes as given by the sizer function. +/// +/// the number of chunks is not guaranteed to be optimal. +pub fn reduce_to_chunks(mut seq: Vec, max_size: usize, sizer: impl Fn(&T) -> usize) -> Result>, ()> { + let mut output: Vec> = Vec::new(); + loop { + let mut current_chunk_size = 0_usize; + if seq.is_empty() { + break; + } + let mut split_count: usize = 0_usize; + + 'chunker: for element in seq.iter() { + let size = sizer(element); + if size > max_size { + return Err(()); + } + if current_chunk_size.saturating_add(size) > max_size { + // chunk is full - the next element + break 'chunker; + } else { + current_chunk_size = current_chunk_size.saturating_add(size); + split_count += 1; + } + }; + let len = seq.len(); + let rest = seq.split_off(split_count); + output.push(seq); + seq = rest; + assert_eq!(seq.len() + split_count, len); + } + + Ok(output) +} + + +#[cfg(test)] +mod tests { + use crate::reduce_to_chunks::reduce_to_chunks; + + #[test] + fn reduce_to_chunks_simple() { + assert_eq!(vec![ + vec![1, 2, 3], + vec![4], + vec![5], + vec![6] + ], + reduce_to_chunks::(vec![1, 2, 3, 4, 5, 6], 6, |item| { *item }).unwrap() + ); + } + + #[test] + fn reduce_to_chunks_no_split() { + assert_eq!(vec![ + vec![1, 2, 3, 4, 5, 6], + ], + reduce_to_chunks::(vec![1, 2, 3, 4, 5, 6], 21, |item| { *item }).unwrap() + ); + } + + fn reduce_to_chunks_empty() { + assert_eq!( + Vec::>::new(), + reduce_to_chunks::(vec![], 0, |item| { *item }).unwrap() + ); + } + + fn split_too_big() { + assert_eq!( + Err(()), + reduce_to_chunks::(vec![1, 10, 11], 2, |item| { *item }) + ); + } +} \ No newline at end of file From e920d7596050d271c6e8bd17614d36dcfd69ee66 Mon Sep 17 00:00:00 2001 From: nig Date: Mon, 11 Nov 2024 17:59:49 +0100 Subject: [PATCH 12/32] further iteration improvements for multi mail import --- packages/node-mimimi/src/importer.rs | 12 ++- packages/node-mimimi/src/reduce_to_chunks.rs | 93 +++++++++++++------- 2 files changed, 64 insertions(+), 41 deletions(-) diff --git a/packages/node-mimimi/src/importer.rs b/packages/node-mimimi/src/importer.rs index 0b46907ee1e..dce377d8749 100644 --- a/packages/node-mimimi/src/importer.rs +++ b/packages/node-mimimi/src/importer.rs @@ -102,6 +102,8 @@ impl Importer { .map_err(IterationError::File), }; + + let import_res = match next_importable_mail { Ok(next_importable_mail) => self.import_all_mail(vec![next_importable_mail]).await, @@ -168,15 +170,11 @@ impl Importer { let import_count = importable_mail.len(); let all_imports = importable_mail .into_iter() - .map(ImportMailData::from) - .collect(); + .map(ImportMailData::from); const MAX_REQUEST_SIZE: usize = 1024 * 1024 * 10; - let Ok(import_chunks) = reduce_to_chunks(all_imports, MAX_REQUEST_SIZE, estimate_json_size) - else { - // one of the elements does not fit into a chunk - return Err(()); - }; + let import_chunks: Vec> = reduce_to_chunks(all_imports, MAX_REQUEST_SIZE, Box::new(estimate_json_size)).collect(); + let mut mails: Vec = Vec::with_capacity(import_count); for imports in import_chunks { let import_mail_post_in = ImportMailPostIn { diff --git a/packages/node-mimimi/src/reduce_to_chunks.rs b/packages/node-mimimi/src/reduce_to_chunks.rs index 6fd4cce16cf..0746dfb7b9f 100644 --- a/packages/node-mimimi/src/reduce_to_chunks.rs +++ b/packages/node-mimimi/src/reduce_to_chunks.rs @@ -1,37 +1,60 @@ -/// split a given vector of elements into a vector of chunks not exceeding max_size, where the -/// chunks size is calculated by summing up the elements sizes as given by the sizer function. -/// -/// the number of chunks is not guaranteed to be optimal. -pub fn reduce_to_chunks(mut seq: Vec, max_size: usize, sizer: impl Fn(&T) -> usize) -> Result>, ()> { - let mut output: Vec> = Vec::new(); - loop { - let mut current_chunk_size = 0_usize; - if seq.is_empty() { - break; - } - let mut split_count: usize = 0_usize; +use std::iter::Peekable; +use std::ops::Deref; - 'chunker: for element in seq.iter() { - let size = sizer(element); - if size > max_size { - return Err(()); +struct ChunkingIterator +where + Inner: Iterator, +{ + inner: Peekable, + max_size: usize, + sizer: Box usize>, +} + +impl Iterator for ChunkingIterator +where + Inner: Iterator, +{ + type Item = Vec; + fn next(&mut self) -> Option { + let mut seq = &mut self.inner; + let element = seq.peek(); + let Some(mut element) = element else { + return None; + }; + + let mut chunk: Vec = Vec::new(); + let mut current_chunk_size = 0_usize; + loop { + let element_size = self.sizer.deref()(element); + if element_size > self.max_size { + // this element is too big for one chunk. we might just ignore that and make a + // one-element chunk that fails to upload, or we stop iteration here. + // this discards any elements already in the chunk + return None; } - if current_chunk_size.saturating_add(size) > max_size { - // chunk is full - the next element - break 'chunker; + let new_chunk_size = current_chunk_size.saturating_add(element_size); + if new_chunk_size > self.max_size { + // chunk is full - this element goes into the next chunk. + // because we used peek() it'll still be available for the next call to this function. + return Some(chunk); } else { - current_chunk_size = current_chunk_size.saturating_add(size); - split_count += 1; + current_chunk_size = new_chunk_size; + chunk.push(seq.next().expect("got None from next even though peek() gave Some")); + element = match seq.peek() { + None => break, + Some(e) => e + }; } - }; - let len = seq.len(); - let rest = seq.split_off(split_count); - output.push(seq); - seq = rest; - assert_eq!(seq.len() + split_count, len); + } + Some(chunk) } - - Ok(output) +} +/// split a given vector of elements into a vector of chunks not exceeding max_size, where the +/// chunks size is calculated by summing up the elements sizes as given by the sizer function. +/// +/// the number of chunks is not guaranteed to be optimal. +pub fn reduce_to_chunks<'element, Element: 'element>(mut seq: impl Iterator, max_size: usize, sizer: Box usize>) -> impl Iterator> { + ChunkingIterator { inner: seq.peekable(), max_size, sizer } } @@ -47,7 +70,7 @@ mod tests { vec![5], vec![6] ], - reduce_to_chunks::(vec![1, 2, 3, 4, 5, 6], 6, |item| { *item }).unwrap() + reduce_to_chunks::(vec![1, 2, 3, 4, 5, 6].into_iter(), 6, Box::new(|item| { *item })).collect::>>() ); } @@ -56,21 +79,23 @@ mod tests { assert_eq!(vec![ vec![1, 2, 3, 4, 5, 6], ], - reduce_to_chunks::(vec![1, 2, 3, 4, 5, 6], 21, |item| { *item }).unwrap() + reduce_to_chunks::(vec![1, 2, 3, 4, 5, 6].into_iter(), 21, Box::new(|item| { *item })).collect::>>() ); } + #[test] fn reduce_to_chunks_empty() { assert_eq!( Vec::>::new(), - reduce_to_chunks::(vec![], 0, |item| { *item }).unwrap() + reduce_to_chunks::(vec![].into_iter(), 0, Box::new(|item| { *item })).collect::>>() ); } + #[test] fn split_too_big() { assert_eq!( - Err(()), - reduce_to_chunks::(vec![1, 10, 11], 2, |item| { *item }) + Vec::>::new(), + reduce_to_chunks::(vec![1, 10, 11].into_iter(), 2, Box::new(|item| { *item })).collect::>>() ); } } \ No newline at end of file From 2ce1248f94fcea3dc1450b63e226464ece67ebc8 Mon Sep 17 00:00:00 2001 From: nig Date: Tue, 12 Nov 2024 12:40:30 +0100 Subject: [PATCH 13/32] multimail import --- packages/node-mimimi/src/importer.rs | 172 +-- .../src/importer/file_reader/import_client.rs | 2 +- .../src/importer/imap_reader/import_client.rs | 262 ++-- .../src/importer/importable_mail.rs | 18 +- .../importable_mail/extend_mail_parser.rs | 26 +- .../mime_string_to_importable_mail_test.rs | 12 +- .../msg_file_compatibility_test.rs | 4 +- .../plain_text_to_html_converter.rs | 234 ++-- packages/node-mimimi/src/reduce_to_chunks.rs | 9 +- packages/node-mimimi/src/tuta_imap/client.rs | 6 +- .../src/tuta_imap/client/tls_stream.rs | 154 +- .../node-mimimi/src/tuta_imap/client/types.rs | 2 +- .../src/tuta_imap/testing/jvm_singeleton.rs | 2 +- .../sdk/src/entities/json_size_estimator.rs | 1233 ++++++++--------- tuta-sdk/rust/sdk/src/key_loader_facade.rs | 1 - 15 files changed, 1067 insertions(+), 1070 deletions(-) diff --git a/packages/node-mimimi/src/importer.rs b/packages/node-mimimi/src/importer.rs index dce377d8749..51177048cf8 100644 --- a/packages/node-mimimi/src/importer.rs +++ b/packages/node-mimimi/src/importer.rs @@ -5,8 +5,7 @@ use crate::importer::importable_mail::ImportableMail; use crate::reduce_to_chunks::reduce_to_chunks; use crate::tuta::credentials::TutaCredentials; use napi::bindgen_prelude::Error as NapiError; -use serde::Serialize; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use tutasdk::crypto::aes::Iv; use tutasdk::crypto::key::GenericAesKey; use tutasdk::crypto::randomizer_facade::RandomizerFacade; @@ -64,7 +63,7 @@ struct Importer { logged_in_sdk: Arc, target_owner_group: GeneratedId, target_mail_folder: IdTupleGenerated, - import_source: ImportSource, + import_source: Arc>, randomizer_facade: RandomizerFacade, } @@ -85,74 +84,62 @@ pub enum IterationError { File(FileIterationError), } -impl Importer { - pub async fn continue_import(&mut self) -> Result { - let mut failed_import_count = 0_u32; - let mut success_import_count = 0_u32; - - 'walk_through_source: loop { - let next_importable_mail = match &mut self.import_source { - ImportSource::RemoteImap { imap_import_client } => imap_import_client - .fetch_next_mail() - .await - .map_err(IterationError::Imap), - - ImportSource::LocalFile { fs_email_client } => fs_email_client - .get_next_importable_mail() - .map_err(IterationError::File), - }; - - - - let import_res = match next_importable_mail { - Ok(next_importable_mail) => self.import_all_mail(vec![next_importable_mail]).await, +struct ImportSourceIterator { + // it would be nice to not need the mutex, but when the importer continues the import, + // it mutates its own state and also calls mutating functions on the source. solving this + // probably requires a bigger restructure of the code (it's very OOP atm) + source: Arc>, +} - // source says, all the iteration have ended, - Err(IterationError::File(FileIterationError::SourceEnd)) - | Err(IterationError::Imap(ImapIterationError::SourceEnd)) => { - break 'walk_through_source; - } +impl Iterator for ImportSourceIterator { + type Item = ImportableMail; + + fn next(&mut self) -> Option { + let mut source = self.source.lock().unwrap(); + let next_importable_mail = match &mut *source { + // the other way (converting fs_source to an async_iterator) would be nicer, but that's a nightly feature + ImportSource::RemoteImap { imap_import_client } => imap_import_client + .fetch_next_mail() + .map_err(IterationError::Imap), + ImportSource::LocalFile { fs_email_client } => fs_email_client + .get_next_importable_mail() + .map_err(IterationError::File), + }; - Err(e) => { - panic!("Cannot get next email from source: {e:?}") - } - }; + match next_importable_mail { + Ok(next_importable_mail) => Some(next_importable_mail), - match import_res { - // this import has been success, - Ok(_imported_mail_response) => success_import_count += 1, + // source says, all the iteration have ended, + Err(IterationError::File(FileIterationError::SourceEnd)) + | Err(IterationError::Imap(ImapIterationError::SourceEnd)) => { + None + } - Err(_) => { - // todo: save the ImportableMail to some fail list, - // since, in this iteration the source will not give this mail again, - failed_import_count += 1; - } + Err(e) => { + // once we handle this case we will need another iterator that filters (and logs) the + // errors so we don't have to handle the error case during the chunking + upload + panic!("Cannot get next email from source: {e:?}") } } + } +} - if failed_import_count > 0 { - // some mail failed to import: - self.status = ImportStatus { - state: ImportState::Postponed, - imported_mails: success_import_count, - }; - } else { - // nothing failed, - self.status = ImportStatus { - state: ImportState::Finished, - imported_mails: success_import_count, - }; - }; - +impl Importer { + pub async fn continue_import(&mut self) -> Result { + let source_iterator = ImportSourceIterator { source: Arc::clone(&self.import_source) }; + let _ = self.import_all_mail(source_iterator).await; Ok(self.status.clone()) } /// once we get the ImportableMail from either of source, /// continue to the uploading counterpart - async fn import_all_mail( - &self, - importable_mail: Vec, - ) -> Result, ()> { + async fn import_all_mail( + &mut self, + importable_mails: Iter, + ) -> Result, ()> + where + Iter: Iterator, + { let new_aes_256_key = GenericAesKey::from_bytes( self.randomizer_facade .generate_random_array::<{ tutasdk::crypto::aes::AES_256_KEY_SIZE }>() @@ -163,20 +150,24 @@ impl Importer { .logged_in_sdk .get_current_sym_group_key(&self.target_owner_group) .await - .map_err(|e| ())?; + .map_err(|_e| ())?; let owner_enc_session_key = mail_group_key.encrypt_key(&new_aes_256_key, Iv::generate(&self.randomizer_facade)); - let import_count = importable_mail.len(); - let all_imports = importable_mail - .into_iter() - .map(ImportMailData::from); - const MAX_REQUEST_SIZE: usize = 1024 * 1024 * 10; - let import_chunks: Vec> = reduce_to_chunks(all_imports, MAX_REQUEST_SIZE, Box::new(estimate_json_size)).collect(); - - let mut mails: Vec = Vec::with_capacity(import_count); + let import_chunks: Vec> = reduce_to_chunks( + importable_mails.map(ImportMailData::from), + MAX_REQUEST_SIZE, + Box::new(estimate_json_size), + ).collect(); + + let mut mails: Vec = Vec::new(); + let mut new_status = ImportStatus { + state: ImportState::Running, + imported_mails: 0, + }; for imports in import_chunks { + let import_len = imports.len(); let import_mail_post_in = ImportMailPostIn { ownerEncSessionKey: owner_enc_session_key.object.clone(), ownerGroup: self.target_owner_group.clone(), @@ -193,16 +184,35 @@ impl Importer { ..Default::default() }; - let mut import_mail_post_out = self + let response = self .logged_in_sdk .get_service_executor() .post::(import_mail_post_in, service_params) - .await - .expect("Cannot execute ImportMailService"); + .await; - mails.append(&mut import_mail_post_out.mails); + match response { + // this import has been success, + Ok(mut imported_post_out) => { + mails.append(&mut imported_post_out.mails); + new_status = ImportStatus { + state: ImportState::Running, + imported_mails: self.status.imported_mails.saturating_add(u32::try_from(import_len).unwrap_or(u32::MAX)), + }; + } + + Err(_) => { + // todo: save the ImportableMails to some fail list, + // since, in this iteration the source will not give these mail again, + new_status = ImportStatus { + state: ImportState::Postponed, + imported_mails: self.status.imported_mails, + }; + } + } } + new_status.state = if new_status.state == ImportState::Postponed { ImportState::Postponed } else { ImportState::Finished }; + self.status = new_status; Ok(mails) } } @@ -212,7 +222,7 @@ impl ImporterApi { logged_in_sdk: Arc, target_owner_group: GeneratedId, target_mail_folder: IdTupleGenerated, - import_source: ImportSource, + import_source: Arc>, ) -> Self { let import_inner = Importer { logged_in_sdk, @@ -248,11 +258,11 @@ impl ImporterApi { let logged_in_sdk_future = Self::create_sdk(tuta_credentials); let fs_email_client = FileImport::new(source_paths) - .map_err(|e| NapiError::from_reason("Cannot create file import"))?; - let import_source = ImportSource::LocalFile { fs_email_client }; + .map_err(|_e| NapiError::from_reason("Cannot create file import"))?; + let import_source = Arc::new(Mutex::new(ImportSource::LocalFile { fs_email_client })); let logged_in_sdk = logged_in_sdk_future .await - .map_err(|e| NapiError::from_reason("Cannot create logged in sdk"))?; + .map_err(|_e| NapiError::from_reason("Cannot create logged in sdk"))?; Ok(ImporterApi::new( logged_in_sdk, @@ -293,7 +303,7 @@ impl ImporterApi { impl ImporterApi { // once Self::continue_import return custom error, // do the error conversion here, or in trait - fn error_conversion(err: E) -> napi::Error { + fn error_conversion(_err: E) -> napi::Error { todo!() } @@ -395,9 +405,9 @@ mod tests { }, }; - let import_source = ImportSource::RemoteImap { + let import_source = Arc::new(Mutex::new(ImportSource::RemoteImap { imap_import_client: ImapImport::new(imap_import_config), - }; + })); let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng); let target_mail_folder = get_test_import_folder_id(&logged_in_sdk, MailSetKind::Archive) .await @@ -431,9 +441,9 @@ mod tests { .await .unwrap(); - let import_source = ImportSource::LocalFile { + let import_source = Arc::new(Mutex::new(ImportSource::LocalFile { fs_email_client: FileImport::new(source_paths).unwrap(), - }; + })); let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng); let target_mail_folder = get_test_import_folder_id(&logged_in_sdk, MailSetKind::Archive) .await diff --git a/packages/node-mimimi/src/importer/file_reader/import_client.rs b/packages/node-mimimi/src/importer/file_reader/import_client.rs index cff2f179e1c..6ae0bb9f476 100644 --- a/packages/node-mimimi/src/importer/file_reader/import_client.rs +++ b/packages/node-mimimi/src/importer/file_reader/import_client.rs @@ -50,7 +50,7 @@ impl FileImport { match mbox_source.next() { Some(Ok(mbox_item)) => Ok(mbox_item.unwrap_contents()), - Some(Err(e)) => Err(FileIterationError::MboxParseError), + Some(Err(_e)) => Err(FileIterationError::MboxParseError), None => { self.mbox_sources.pop(); diff --git a/packages/node-mimimi/src/importer/imap_reader/import_client.rs b/packages/node-mimimi/src/importer/imap_reader/import_client.rs index e8237d1690c..5ca9f71ebe1 100644 --- a/packages/node-mimimi/src/importer/imap_reader/import_client.rs +++ b/packages/node-mimimi/src/importer/imap_reader/import_client.rs @@ -6,153 +6,153 @@ use imap_codec::imap_types::response::StatusKind; use std::num::NonZeroU32; pub struct ImapImport { - import_config: ImapImportConfig, - imap_client: TutaImapClient, + import_config: ImapImportConfig, + imap_client: TutaImapClient, - import_state: ImapImportState, + import_state: ImapImportState, } pub struct ImapImportState { - done_fetched_mailbox: Vec>, - current_mailbox: Option>, - next_target_mailbox: Vec>, + done_fetched_mailbox: Vec>, + current_mailbox: Option>, + next_target_mailbox: Vec>, - /// List of mail id that have been fetched from current_mailbox, - fetched_from_current_mailbox: Vec, + /// List of mail id that have been fetched from current_mailbox, + fetched_from_current_mailbox: Vec, } impl ImapImportState { - /// add currently selected mailbox to done, - /// and pop one from next target mailbox - pub fn finish_current_mailbox(&mut self) { - let current_mailbox = self.current_mailbox.as_mut().expect("No current mailbox"); - self.done_fetched_mailbox.push(current_mailbox.clone()); - self.current_mailbox = self.next_target_mailbox.pop(); - self.fetched_from_current_mailbox.clear(); - } - - /// this id of current mailbox was fetched - pub fn fetched_from_current_mailbox(&mut self, id: NonZeroU32) { - assert!(self.current_mailbox.is_some(), "No current mailbox"); - self.fetched_from_current_mailbox.push(id); - } + /// add currently selected mailbox to done, + /// and pop one from next target mailbox + pub fn finish_current_mailbox(&mut self) { + let current_mailbox = self.current_mailbox.as_mut().expect("No current mailbox"); + self.done_fetched_mailbox.push(current_mailbox.clone()); + self.current_mailbox = self.next_target_mailbox.pop(); + self.fetched_from_current_mailbox.clear(); + } + + /// this id of current mailbox was fetched + pub fn fetched_from_current_mailbox(&mut self, id: NonZeroU32) { + assert!(self.current_mailbox.is_some(), "No current mailbox"); + self.fetched_from_current_mailbox.push(id); + } } #[derive(Debug, PartialEq, Clone)] pub enum ImapIterationError { - /// All mail form remote server have been visited at least once, - SourceEnd, + /// All mail form remote server have been visited at least once, + SourceEnd, - /// when executing a command, received a non-ok status, - NonOkCommandStatus, + /// when executing a command, received a non-ok status, + NonOkCommandStatus, - /// Can not convert ImapMail to ConvertableMail - MailParseError(MailParseError), + /// Can not convert ImapMail to ConvertableMail + MailParseError(MailParseError), - /// Can not login to imap server - NoLogin, + /// Can not login to imap server + NoLogin, } impl ImapImport { - pub fn new(import_config: ImapImportConfig) -> Self { - let imap_client = TutaImapClient::new( - import_config.credentials.host.as_str(), - import_config.credentials.port, - ); - - Self { - imap_client, - import_config, - - import_state: ImapImportState { - done_fetched_mailbox: vec![], - next_target_mailbox: vec![Mailbox::Inbox], - current_mailbox: None, - fetched_from_current_mailbox: vec![], - }, - } - } - - /// High level abstraction to read next mail from imap, - /// will switch to next mailbox, if everything from current mailbox is fetched, - pub async fn fetch_next_mail(&mut self) -> Result { - self.ensure_logged_in()?; - - while self.imap_client.latest_search_results.is_empty() { - if self.import_state.current_mailbox.is_some() { - self.import_state.finish_current_mailbox(); - } - - // select next mailbox - // and search for all available mails - self.ensure_mailbox_selected()?; - - self.imap_client - .search_all_uid() - .eq(&StatusKind::Ok) - .then_some(()) - .ok_or(ImapIterationError::NonOkCommandStatus)?; - } - - // search for the last ( oldest ? ) mail - let next_mail_id = self.imap_client.latest_search_results.pop().unwrap(); - self.imap_client - .fetch_mail_by_uid(next_mail_id) - .eq(&StatusKind::Ok) - .then_some(()) - .ok_or(ImapIterationError::NonOkCommandStatus)?; - let next_mail_imap = self.imap_client.latest_mails.remove(&next_mail_id).unwrap(); - - // mark this id have been fetched - self.import_state.fetched_from_current_mailbox(next_mail_id); - - ImportableMail::try_from(next_mail_imap).map_err(ImapIterationError::MailParseError) - } - - fn ensure_mailbox_selected(&mut self) -> Result<(), ImapIterationError> { - if self.import_state.current_mailbox.is_none() { - let next_mailbox_to_select = self - .import_state - .next_target_mailbox - .pop() - .ok_or(ImapIterationError::SourceEnd)?; - self.import_state.current_mailbox = Some(next_mailbox_to_select); - self.import_state.fetched_from_current_mailbox = vec![]; - } - - // if something from current mailbox is selected, it means we are already in selected state - if !self.import_state.fetched_from_current_mailbox.is_empty() { - return Ok(()); - } - - let target_mailbox = self - .import_state - .current_mailbox - .as_ref() - .ok_or(ImapIterationError::SourceEnd)?; - self.imap_client - .select_mailbox(target_mailbox.clone()) - .eq(&StatusKind::Ok) - .then_some(()) - .ok_or(ImapIterationError::NonOkCommandStatus) - } - - fn ensure_logged_in(&mut self) -> Result<(), ImapIterationError> { - if self.imap_client.is_logged_in() { - return Ok(()); - } - - match &self.import_config.credentials.login_mechanism { - LoginMechanism::Plain { username, password } => self - .imap_client - .plain_login(username.as_str(), password.as_str()) - .eq(&StatusKind::Ok) - .then_some(()) - .ok_or(ImapIterationError::NoLogin), - - LoginMechanism::OAuth { access_token: _ } => { - unimplemented!() - }, - } - } + pub fn new(import_config: ImapImportConfig) -> Self { + let imap_client = TutaImapClient::new( + import_config.credentials.host.as_str(), + import_config.credentials.port, + ); + + Self { + imap_client, + import_config, + + import_state: ImapImportState { + done_fetched_mailbox: vec![], + next_target_mailbox: vec![Mailbox::Inbox], + current_mailbox: None, + fetched_from_current_mailbox: vec![], + }, + } + } + + /// High level abstraction to read next mail from imap, + /// will switch to next mailbox, if everything from current mailbox is fetched, + pub fn fetch_next_mail(&mut self) -> Result { + self.ensure_logged_in()?; + + while self.imap_client.latest_search_results.is_empty() { + if self.import_state.current_mailbox.is_some() { + self.import_state.finish_current_mailbox(); + } + + // select next mailbox + // and search for all available mails + self.ensure_mailbox_selected()?; + + self.imap_client + .search_all_uid() + .eq(&StatusKind::Ok) + .then_some(()) + .ok_or(ImapIterationError::NonOkCommandStatus)?; + } + + // search for the last ( oldest ? ) mail + let next_mail_id = self.imap_client.latest_search_results.pop().unwrap(); + self.imap_client + .fetch_mail_by_uid(next_mail_id) + .eq(&StatusKind::Ok) + .then_some(()) + .ok_or(ImapIterationError::NonOkCommandStatus)?; + let next_mail_imap = self.imap_client.latest_mails.remove(&next_mail_id).unwrap(); + + // mark this id have been fetched + self.import_state.fetched_from_current_mailbox(next_mail_id); + + ImportableMail::try_from(next_mail_imap).map_err(ImapIterationError::MailParseError) + } + + fn ensure_mailbox_selected(&mut self) -> Result<(), ImapIterationError> { + if self.import_state.current_mailbox.is_none() { + let next_mailbox_to_select = self + .import_state + .next_target_mailbox + .pop() + .ok_or(ImapIterationError::SourceEnd)?; + self.import_state.current_mailbox = Some(next_mailbox_to_select); + self.import_state.fetched_from_current_mailbox = vec![]; + } + + // if something from current mailbox is selected, it means we are already in selected state + if !self.import_state.fetched_from_current_mailbox.is_empty() { + return Ok(()); + } + + let target_mailbox = self + .import_state + .current_mailbox + .as_ref() + .ok_or(ImapIterationError::SourceEnd)?; + self.imap_client + .select_mailbox(target_mailbox.clone()) + .eq(&StatusKind::Ok) + .then_some(()) + .ok_or(ImapIterationError::NonOkCommandStatus) + } + + fn ensure_logged_in(&mut self) -> Result<(), ImapIterationError> { + if self.imap_client.is_logged_in() { + return Ok(()); + } + + match &self.import_config.credentials.login_mechanism { + LoginMechanism::Plain { username, password } => self + .imap_client + .plain_login(username.as_str(), password.as_str()) + .eq(&StatusKind::Ok) + .then_some(()) + .ok_or(ImapIterationError::NoLogin), + + LoginMechanism::OAuth { access_token: _ } => { + unimplemented!() + } + } + } } diff --git a/packages/node-mimimi/src/importer/importable_mail.rs b/packages/node-mimimi/src/importer/importable_mail.rs index 7092ad174aa..492eda3936c 100644 --- a/packages/node-mimimi/src/importer/importable_mail.rs +++ b/packages/node-mimimi/src/importer/importable_mail.rs @@ -33,7 +33,7 @@ pub(super) enum MailState { pub(super) enum ICalType { #[default] Nothing = 0, - ICalPublish = 1, + ICalPublishh = 1, ICalRequest = 2, ICalAdd = 3, ICalCancel = 4, @@ -154,7 +154,7 @@ impl ImportableMail { address_list .as_ref() - .into_iter() + .iter() .map(|address| MailContact { mail_address: address.address().unwrap_or_default().to_string(), name: address.name().unwrap_or_default().to_string(), @@ -390,9 +390,9 @@ impl ImportableMail { // multipart block should always come before all it's alternative } - fn handle_binary<'a>( + fn handle_binary( attachments: &mut Vec, - header_values: &Vec>, + header_values: &Vec>, binary_content: Vec, is_inline: bool, ) { @@ -403,8 +403,7 @@ impl ImportableMail { }); let content_type_attributes = content_type // get attributes_of_content_type if content-type is there - .map(mail_parser::ContentType::attributes) - .flatten() + .and_then(mail_parser::ContentType::attributes) // if can-not get attributes, default to empty list of attributes .unwrap_or_default(); let filename = content_type_attributes @@ -463,7 +462,7 @@ impl From for ImportMailData { message_id, in_reply_to, references, - attachments, + attachments: _attachments, } = importable_mail; let date = date.unwrap_or_else(|| DateTime::from_system_time(SystemTime::now())); @@ -610,10 +609,7 @@ impl<'x> TryFrom> for ImportableMail { // different envelope sender should not contain address listed in from_addresses; .filter(|diff_sender| { from_addresses - .iter() - .filter(|from| from.mail_address != diff_sender.mail_address) - .next() - .is_some() + .iter().any(|from| from.mail_address != diff_sender.mail_address) }) .map(|mail_address| mail_address.mail_address); diff --git a/packages/node-mimimi/src/importer/importable_mail/extend_mail_parser.rs b/packages/node-mimimi/src/importer/importable_mail/extend_mail_parser.rs index 6e4fd8e9ff3..45a1077da22 100644 --- a/packages/node-mimimi/src/importer/importable_mail/extend_mail_parser.rs +++ b/packages/node-mimimi/src/importer/importable_mail/extend_mail_parser.rs @@ -1,4 +1,4 @@ -///! Extends the functionality of mail_parser crate +//! Extends the functionality of mail_parser crate use crate::importer::importable_mail::ReplyType; use mail_parser::HeaderName; use std::borrow::Cow; @@ -12,11 +12,9 @@ pub(super) fn get_reply_type_from_headers<'a>(headers: &'a [mail_parser::Header< if header.value().make_string().trim().is_empty() { is_forward = true; } - } else if header.name == HeaderName::References { - if header.value().make_string().trim().is_empty() { - is_reply = true; - } - } + } else if header.name == HeaderName::References && header.value().make_string().trim().is_empty() { + is_reply = true; + } if is_reply && is_forward { break; } @@ -40,7 +38,7 @@ pub(super) trait MakeString { impl<'a> MakeString for [mail_parser::Header<'a>] { fn make_string(&self) -> Cow { - self.into_iter() + self.iter() .map(MakeString::make_string) .collect::>() .join("\n") @@ -75,10 +73,10 @@ impl<'a> MakeString for mail_parser::HeaderValue<'a> { } } -impl<'a> MakeString for mail_parser::DateTime { +impl MakeString for mail_parser::DateTime { fn make_string(&self) -> Cow { - const DAY_OF_WEEK: [&'static str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; - const MONTH_OF_YEAR: [&'static str; 12] = [ + const DAY_OF_WEEK: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + const MONTH_OF_YEAR: [&str; 12] = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec", ]; @@ -102,7 +100,7 @@ impl<'a> MakeString for mail_parser::DateTime { } } -impl<'a> MakeString for mail_parser::Received<'a> { +impl<'x> MakeString for mail_parser::Received<'x> { fn make_string(&self) -> Cow { Cow::Borrowed("todo!()") } @@ -112,12 +110,12 @@ impl<'a> MakeString for mail_parser::Address<'a> { fn make_string(&self) -> Cow { match self { mail_parser::Address::List(address_list) => address_list - .into_iter() + .iter() .map(|addr| make_mail_address(addr.name(), addr.address())) .collect::>() .join(",") .into(), - mail_parser::Address::Group(group_list) => { + mail_parser::Address::Group(_group_list) => { todo!() }, } @@ -128,7 +126,7 @@ impl<'a> MakeString for mail_parser::ContentType<'a> { fn make_string(&self) -> Cow { let attribute_str = self.attributes.as_ref().map(|attributes| { attributes - .into_iter() + .iter() .map(|(name, value)| { if value.is_empty() { name.to_string() diff --git a/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs b/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs index 2d6038fc15f..e41707c9526 100644 --- a/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs +++ b/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs @@ -842,7 +842,7 @@ Content-Type: text/html; charset=UTF-8 --line-- "#; - let m = parse_mail(msg); + let _m = parse_mail(msg); todo!() } @@ -869,7 +869,7 @@ first plain text in body --line-- "#; - let m = parse_mail(msg); + let _m = parse_mail(msg); todo!() } @@ -1089,7 +1089,7 @@ Content-Disposition: attachment; filename=withoutContentType.pdf; Message --frontier-- "#; - let m: ImportableMail = parse_mail(msg); + let _m: ImportableMail = parse_mail(msg); todo!() // assertEquals("text/plain", m.getAttachedFiles().get(0).getMimeType()); @@ -1144,7 +1144,7 @@ Content-Type: multipart/mixed; boundary=frontier --frontier "#; - let m: ImportableMail = parse_mail(msg); + let _m: ImportableMail = parse_mail(msg); todo!() // assertEquals("A, B ", m.getSender().getMailAddress()); @@ -1166,7 +1166,7 @@ any body text --frontier "#; - let m: ImportableMail = parse_mail(msg); + let _m: ImportableMail = parse_mail(msg); todo!() // assertEquals("any body text", m.getPlainBodyText()); @@ -1182,7 +1182,7 @@ References: <1234564@web.de> Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-Type: text/calendar; charset=\"UTF-8\"; method=REQUEST "#; - let m: ImportableMail = parse_mail(msg); + let _m: ImportableMail = parse_mail(msg); todo!() // assertEquals("a@tutanota.de", m.getSender().getMailAddress()); diff --git a/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs b/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs index 7b0ce089a01..1d68c08acd2 100644 --- a/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs +++ b/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs @@ -165,7 +165,7 @@ impl From for ImportableMail { body_parts.push(plain_body_converted); } - for attached_message in expected_message.attached_messages { + for _attached_message in expected_message.attached_messages { let attached_message_converted = mail_parser::MessagePart { headers: vec![], is_encoding_problem: false, @@ -179,7 +179,7 @@ impl From for ImportableMail { body_parts.push(attached_message_converted); } - for attached_file in expected_message.attached_files { + for _attached_file in expected_message.attached_files { let attached_file_converted = mail_parser::MessagePart { headers: vec![], is_encoding_problem: false, diff --git a/packages/node-mimimi/src/importer/importable_mail/plain_text_to_html_converter.rs b/packages/node-mimimi/src/importer/importable_mail/plain_text_to_html_converter.rs index 167b31046ab..51f6811fb24 100644 --- a/packages/node-mimimi/src/importer/importable_mail/plain_text_to_html_converter.rs +++ b/packages/node-mimimi/src/importer/importable_mail/plain_text_to_html_converter.rs @@ -9,88 +9,92 @@ use regex::Regex; /// /// This code is ported from tutadb PlainTextToHtmlConverter pub(super) fn plain_text_to_html(plain_text: &str) -> String { - let mut result: String = String::from(""); - let SEPARATOR: Regex = Regex::new("\r?\n").expect("invalid regex"); // todo! move to const - let lines = SEPARATOR.split(plain_text); - let mut previous_quote_level = 0; - for (i, line) in lines.enumerate() { - let line_quote_level = get_line_quote_level(line.to_string()); - - if i > 0 && (previous_quote_level == line_quote_level) { - // only append an explicit newline (
) if the quoteLevel does not change (implicit newline in case of
) - result.push_str("
") - } - - result.push_str( - "
" - .repeat( - (previous_quote_level - line_quote_level) - .try_into() - .unwrap_or(0), - ) - .as_str(), - ); - result.push_str( - "
" - .repeat( - (line_quote_level - previous_quote_level) - .try_into() - .unwrap_or(0), - ) - .as_str(), - ); - - if line_quote_level > 0 { - if line.len() > line_quote_level as usize { - let quote_block_start_index: usize = (line_quote_level + 1) as usize; - let indented_line: &str = &line[quote_block_start_index..]; - let escaped_line = escape_plain_text_line(indented_line); - result.push_str(&*escaped_line) - } - } else { - let escaped_line = escape_plain_text_line(line); - result.push_str(&*escaped_line); // skip '> ', '>> ', ... - } - previous_quote_level = line_quote_level - } - - result.push_str( - "
" - .repeat((previous_quote_level).try_into().unwrap_or(0)) - .as_str(), - ); - - result + let mut result: String = String::from(""); + let separator: Regex = Regex::new("\r?\n").expect("invalid regex"); // todo! move to const + let lines = separator.split(plain_text); + let mut previous_quote_level = 0; + for (i, line) in lines.enumerate() { + let line_quote_level = get_line_quote_level(line.to_string()); + + if i > 0 && (previous_quote_level == line_quote_level) { + // only append an explicit newline (
) if the quoteLevel does not change (implicit newline in case of
) + result.push_str("
") + } + + result.push_str( + "
" + .repeat( + (previous_quote_level - line_quote_level) + .try_into() + .unwrap_or(0), + ) + .as_str(), + ); + result.push_str( + "
" + .repeat( + (line_quote_level - previous_quote_level) + .try_into() + .unwrap_or(0), + ) + .as_str(), + ); + + if line_quote_level > 0 { + if line.len() > line_quote_level as usize { + let quote_block_start_index: usize = (line_quote_level + 1) as usize; + let indented_line: &str = &line[quote_block_start_index..]; + let escaped_line = escape_plain_text_line(indented_line); + result.push_str(&escaped_line) + } + } else { + let escaped_line = escape_plain_text_line(line); + result.push_str(&escaped_line); // skip '> ', '>> ', ... + } + previous_quote_level = line_quote_level + } + + result.push_str( + "
" + .repeat((previous_quote_level).try_into().unwrap_or(0)) + .as_str(), + ); + + result } fn escape_plain_text_line(line: &str) -> String { - let escaped_line = line.replace("&", "&"); - let escaped_line = escaped_line.replace("<", "<"); - let escaped_line = escaped_line.replace(">", ">"); - escaped_line + let escaped_line = line.replace("&", "&"); + let escaped_line = escaped_line.replace("<", "<"); + + escaped_line.replace(">", ">") } fn get_line_quote_level(line: String) -> i32 { - let mut line_open_blockquotes = 0; - for char in line.chars() { - if char == '>' { - line_open_blockquotes += 1; - } else if char == ' ' { - break; - } else { - line_open_blockquotes = 0; - break; - } - } - line_open_blockquotes + let mut line_open_blockquotes = 0; + for char in line.chars() { + if char == '>' { + line_open_blockquotes += 1; + } else if char == ' ' { + break; + } else { + line_open_blockquotes = 0; + break; + } + } + line_open_blockquotes } -/** - * Adds and tags to the given html - */ -fn add_html_page_tags(html: String) -> String { - format!( - "\r\n\ +#[cfg(test)] +mod test { + use crate::importer::importable_mail::plain_text_to_html_converter::plain_text_to_html; + + /** + * Adds and tags to the given html + */ + fn add_html_page_tags(html: String) -> String { + format!( + "\r\n\ \r\n\ \r\n\ \r\n\ @@ -98,56 +102,52 @@ fn add_html_page_tags(html: String) -> String { {}\ \r\n\ \r\n", - html - ) -} + html + ) + } -mod test { - use crate::importer::importable_mail::plain_text_to_html_converter::{ - add_html_page_tags, plain_text_to_html, - }; - #[test] - pub fn convert_to_html() { - assert_eq!("Test-Mail im Plain-Text und mit komischen Zeichen: & \"~ΓΆΓ€β₯£Τ»Β³@
weiter gehts in der naechsten Zeile", + #[test] + pub fn convert_to_html() { + assert_eq!("Test-Mail im Plain-Text und mit komischen Zeichen: & \"~ΓΆΓ€β₯£Τ»Β³@
weiter gehts in der naechsten Zeile", plain_text_to_html("Test-Mail im Plain-Text und mit komischen Zeichen: & \"~ΓΆΓ€β₯£Τ»Β³@\r\nweiter gehts in der naechsten Zeile")); - assert_eq!( - "
simple blockquote
", - plain_text_to_html("> simple blockquote") - ); + assert_eq!( + "
simple blockquote
", + plain_text_to_html("> simple blockquote") + ); - assert_eq!( - "
blockquote
with line break
", - plain_text_to_html("> blockquote \r\n> with line break") - ); + assert_eq!( + "
blockquote
with line break
", + plain_text_to_html("> blockquote \r\n> with line break") + ); - assert_eq!( - "
blockquote
with line break
", - plain_text_to_html(">> blockquote \r\n> with line break") - ); + assert_eq!( + "
blockquote
with line break
", + plain_text_to_html(">> blockquote \r\n> with line break") + ); - assert_eq!( - "
blockquote
with line break
", - plain_text_to_html("> blockquote \r\n>> with line break") - ); + assert_eq!( + "
blockquote
with line break
", + plain_text_to_html("> blockquote \r\n>> with line break") + ); - assert_eq!("
blockquote
with line break", + assert_eq!("
blockquote
with line break", plain_text_to_html(">>> blockquote \r\n with line break")); - // quote without text - assert_eq!("
", plain_text_to_html(">")); + // quote without text + assert_eq!("
", plain_text_to_html(">")); - // quote without text but newline - assert_eq!( - "

", - plain_text_to_html(">\r\n>") - ); - } + // quote without text but newline + assert_eq!( + "

", + plain_text_to_html(">\r\n>") + ); + } - #[test] - pub fn test_add_html_page_tags() { - let expected = "\r\n\ + #[test] + pub fn test_add_html_page_tags() { + let expected = "\r\n\ \r\n\ \r\n\ \r\n\ @@ -155,9 +155,9 @@ mod test { Test-Mail im Plain-Text\ \r\n\ \r\n"; - assert_eq!( - expected, - add_html_page_tags("Test-Mail im Plain-Text".to_string()) - ); - } + assert_eq!( + expected, + add_html_page_tags("Test-Mail im Plain-Text".to_string()) + ); + } } diff --git a/packages/node-mimimi/src/reduce_to_chunks.rs b/packages/node-mimimi/src/reduce_to_chunks.rs index 0746dfb7b9f..645f5e83789 100644 --- a/packages/node-mimimi/src/reduce_to_chunks.rs +++ b/packages/node-mimimi/src/reduce_to_chunks.rs @@ -16,11 +16,8 @@ where { type Item = Vec; fn next(&mut self) -> Option { - let mut seq = &mut self.inner; - let element = seq.peek(); - let Some(mut element) = element else { - return None; - }; + let seq = &mut self.inner; + let mut element = seq.peek()?; let mut chunk: Vec = Vec::new(); let mut current_chunk_size = 0_usize; @@ -53,7 +50,7 @@ where /// chunks size is calculated by summing up the elements sizes as given by the sizer function. /// /// the number of chunks is not guaranteed to be optimal. -pub fn reduce_to_chunks<'element, Element: 'element>(mut seq: impl Iterator, max_size: usize, sizer: Box usize>) -> impl Iterator> { +pub fn reduce_to_chunks<'element, Element: 'element>(seq: impl Iterator, max_size: usize, sizer: Box usize>) -> impl Iterator> { ChunkingIterator { inner: seq.peekable(), max_size, sizer } } diff --git a/packages/node-mimimi/src/tuta_imap/client.rs b/packages/node-mimimi/src/tuta_imap/client.rs index 4913e8e8e26..3a1eb8a596f 100644 --- a/packages/node-mimimi/src/tuta_imap/client.rs +++ b/packages/node-mimimi/src/tuta_imap/client.rs @@ -256,7 +256,7 @@ impl TutaImapClient { // command continuation request fn process_cmd_continutation_response( &self, - cmd_continutation_response: CommandContinuationRequest, + _cmd_continutation_response: CommandContinuationRequest, ) -> Result<(), ()> { Ok(()) } @@ -275,7 +275,7 @@ impl TutaImapClient { self.unreceived_status .insert(tag.to_static(), body.to_static()); }, - Status::Bye(response_bye) => { + Status::Bye(_response_bye) => { log::warn!("Received bye from server. byeeeee."); self.connection_state = ConnectionState::Logout; }, @@ -547,7 +547,7 @@ mod tests { assert_eq!(StatusKind::Ok, import_client.fetch_mail_by_uid(message_id)); let imap_mail = import_client.latest_mails.get(&message_id).unwrap(); - let parsed_mail = mail_parser::MessageParser::new() + let _parsed_mail = mail_parser::MessageParser::new() .parse(imap_mail.rfc822_full.as_slice()) .unwrap(); // assert_eq!( diff --git a/packages/node-mimimi/src/tuta_imap/client/tls_stream.rs b/packages/node-mimimi/src/tuta_imap/client/tls_stream.rs index da00b4cf44f..936337810ea 100644 --- a/packages/node-mimimi/src/tuta_imap/client/tls_stream.rs +++ b/packages/node-mimimi/src/tuta_imap/client/tls_stream.rs @@ -11,99 +11,99 @@ use std::time::Duration; pub type SecuredStream = rustls::StreamOwned; pub struct TlsStream { - buffer_controller: BufReader, + buffer_controller: BufReader, } impl TlsStream { - pub fn new(address: &str, port: u16) -> Self { - let tcp_address = SocketAddr::V4(SocketAddrV4::new( - std::net::Ipv4Addr::from_str(address).unwrap(), - port, - )); - let tcp_stream = TcpStream::connect_timeout(&tcp_address, Duration::from_secs(10)).unwrap(); + pub fn new(address: &str, port: u16) -> Self { + let tcp_address = SocketAddr::V4(SocketAddrV4::new( + std::net::Ipv4Addr::from_str(address).unwrap(), + port, + )); + let tcp_stream = TcpStream::connect_timeout(&tcp_address, Duration::from_secs(10)).unwrap(); - let dangerous_config = ClientConfig::builder() - .dangerous() - .with_custom_certificate_verifier(Arc::new(MockSsl)) - .with_no_client_auth(); - let client_connection = rustls::ClientConnection::new( - Arc::new(dangerous_config), - address.to_string().try_into().unwrap(), - ) - .unwrap(); + let dangerous_config = ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(MockSsl)) + .with_no_client_auth(); + let client_connection = rustls::ClientConnection::new( + Arc::new(dangerous_config), + address.to_string().try_into().unwrap(), + ) + .unwrap(); - let buffer_controller = BufReader::new(SecuredStream::new(client_connection, tcp_stream)); - TlsStream { buffer_controller } - } + let buffer_controller = BufReader::new(SecuredStream::new(client_connection, tcp_stream)); + TlsStream { buffer_controller } + } - pub fn write_imap_command(&mut self, encoded_command: &[u8]) -> std::io::Result { - let writer = self.buffer_controller.get_mut(); - let written = writer.write(encoded_command)?; - writer.flush()?; - Ok(written) - } + pub fn write_imap_command(&mut self, encoded_command: &[u8]) -> std::io::Result { + let writer = self.buffer_controller.get_mut(); + let written = writer.write(encoded_command)?; + writer.flush()?; + Ok(written) + } - pub fn read_until_crlf(&mut self) -> std::io::Result> { - let mut line_until_crlf = Vec::new(); - self.buffer_controller - .read_until_slice(b"\r\n", &mut line_until_crlf)?; + pub fn read_until_crlf(&mut self) -> std::io::Result> { + let mut line_until_crlf = Vec::new(); + self.buffer_controller + .read_until_slice(b"\r\n", &mut line_until_crlf)?; - Ok(line_until_crlf) - } + Ok(line_until_crlf) + } - pub fn read_exact(&mut self, target: &mut Vec) -> std::io::Result<()> { - self.buffer_controller.read_exact(target) - } + pub fn read_exact(&mut self, target: &mut Vec) -> std::io::Result<()> { + self.buffer_controller.read_exact(target) + } } #[derive(Debug)] pub struct MockSsl; impl ServerCertVerifier for MockSsl { - fn verify_server_cert( - &self, - end_entity: &CertificateDer<'_>, - intermediates: &[CertificateDer<'_>], - server_name: &ServerName<'_>, - ocsp_response: &[u8], - now: UnixTime, - ) -> Result { - Ok(ServerCertVerified::assertion()) - } + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp_response: &[u8], + _now: UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } - fn verify_tls12_signature( - &self, - message: &[u8], - cert: &CertificateDer<'_>, - dss: &DigitallySignedStruct, - ) -> Result { - Ok(HandshakeSignatureValid::assertion()) - } + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } - fn verify_tls13_signature( - &self, - message: &[u8], - cert: &CertificateDer<'_>, - dss: &DigitallySignedStruct, - ) -> Result { - Ok(HandshakeSignatureValid::assertion()) - } + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } - fn supported_verify_schemes(&self) -> Vec { - vec![ - SignatureScheme::RSA_PKCS1_SHA1, - SignatureScheme::ECDSA_SHA1_Legacy, - SignatureScheme::RSA_PKCS1_SHA256, - SignatureScheme::ECDSA_NISTP256_SHA256, - SignatureScheme::RSA_PKCS1_SHA384, - SignatureScheme::ECDSA_NISTP384_SHA384, - SignatureScheme::RSA_PKCS1_SHA512, - SignatureScheme::ECDSA_NISTP521_SHA512, - SignatureScheme::RSA_PSS_SHA256, - SignatureScheme::RSA_PSS_SHA384, - SignatureScheme::RSA_PSS_SHA512, - SignatureScheme::ED25519, - SignatureScheme::ED448, - ] - } + fn supported_verify_schemes(&self) -> Vec { + vec![ + SignatureScheme::RSA_PKCS1_SHA1, + SignatureScheme::ECDSA_SHA1_Legacy, + SignatureScheme::RSA_PKCS1_SHA256, + SignatureScheme::ECDSA_NISTP256_SHA256, + SignatureScheme::RSA_PKCS1_SHA384, + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::RSA_PKCS1_SHA512, + SignatureScheme::ECDSA_NISTP521_SHA512, + SignatureScheme::RSA_PSS_SHA256, + SignatureScheme::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA512, + SignatureScheme::ED25519, + SignatureScheme::ED448, + ] + } } diff --git a/packages/node-mimimi/src/tuta_imap/client/types.rs b/packages/node-mimimi/src/tuta_imap/client/types.rs index e435134d1b0..f63d481bb93 100644 --- a/packages/node-mimimi/src/tuta_imap/client/types.rs +++ b/packages/node-mimimi/src/tuta_imap/client/types.rs @@ -18,7 +18,7 @@ impl ImapMail { imap_mail.rfc822_full = rfc822_text.0.unwrap().into_inner().to_vec(); }, - MessageDataItem::Envelope(envelope) => {}, + MessageDataItem::Envelope(_envelope) => {}, MessageDataItem::Body(_) => {}, MessageDataItem::BodyExt { .. } => {}, MessageDataItem::BodyStructure(_) => {}, diff --git a/packages/node-mimimi/src/tuta_imap/testing/jvm_singeleton.rs b/packages/node-mimimi/src/tuta_imap/testing/jvm_singeleton.rs index cd9e4cb9877..929c1826b61 100644 --- a/packages/node-mimimi/src/tuta_imap/testing/jvm_singeleton.rs +++ b/packages/node-mimimi/src/tuta_imap/testing/jvm_singeleton.rs @@ -4,7 +4,7 @@ use j4rs::{ClasspathEntry, JvmBuilder}; static mut START_JVM_INVOCATION_COUNTER: i32 = 0; pub fn start_or_attach_to_jvm() -> i32 { - /// todo: SAFETY??? + // todo: SAFETY??? unsafe { if START_JVM_INVOCATION_COUNTER == 0 { // create exactly one jvm and attach to it whenever we create a new IMAP test server diff --git a/tuta-sdk/rust/sdk/src/entities/json_size_estimator.rs b/tuta-sdk/rust/sdk/src/entities/json_size_estimator.rs index 10df57efa5d..15e94d1cbaa 100644 --- a/tuta-sdk/rust/sdk/src/entities/json_size_estimator.rs +++ b/tuta-sdk/rust/sdk/src/entities/json_size_estimator.rs @@ -1,8 +1,5 @@ use crate::entities::Entity; -use serde::ser::{ - SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple, - SerializeTupleStruct, SerializeTupleVariant, StdError, -}; +use serde::ser::{SerializeMap, SerializeSeq, SerializeStruct, StdError}; use serde::{ser, Serialize, Serializer}; use std::fmt::{Debug, Display, Formatter}; @@ -14,9 +11,9 @@ const MINIMUM_ENCRYPTED_SLICE_SIZE: usize = 65; /// needed for limiting the request size, therefore should prefer to overestimate. pub fn estimate_json_size(value: &T) -> usize where - T: Serialize + Entity, + T: Serialize + Entity, { - value.serialize(&mut SizeEstimatingSerializer).unwrap() + value.serialize(&mut SizeEstimatingSerializer).unwrap() } struct SizeEstimatingSerializer; @@ -27,15 +24,15 @@ struct SizeEstimationError(String); impl StdError for SizeEstimationError {} impl Display for SizeEstimationError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.0) - } + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } } impl ser::Error for SizeEstimationError { - fn custom(msg: T) -> Self { - Self(msg.to_string()) - } + fn custom(msg: T) -> Self { + Self(msg.to_string()) + } } /// main serializer for all entities and their field values. @@ -44,210 +41,210 @@ impl ser::Error for SizeEstimationError { /// it assumes that certain types will be encrypted and/or b64 /// encoded. impl<'a> Serializer for &'a mut SizeEstimatingSerializer { - type Ok = usize; - type Error = SizeEstimationError; - type SerializeSeq = SizeEstimatingCompoundSerializer; - type SerializeTuple = ser::Impossible; - type SerializeTupleStruct = ser::Impossible; - type SerializeTupleVariant = ser::Impossible; - type SerializeMap = SizeEstimatingCompoundSerializer; - type SerializeStruct = SizeEstimatingCompoundSerializer; - type SerializeStructVariant = ser::Impossible; - - fn serialize_bool(self, v: bool) -> Result { - Ok(if v { 4 } else { 5 }) - } - - fn serialize_i8(self, _v: i8) -> Result { - unimplemented!("serialize_i8"); - } - - fn serialize_i16(self, _v: i16) -> Result { - unimplemented!("serialize_i16"); - } - - fn serialize_i32(self, _v: i32) -> Result { - unimplemented!("serialize_i32"); - } - - fn serialize_i64(self, _v: i64) -> Result { - unimplemented!("serialize_i64"); - } - - fn serialize_u8(self, _v: u8) -> Result { - unimplemented!("serialize u8"); - } - - fn serialize_u16(self, _v: u16) -> Result { - unimplemented!("serialize_u16") - } - - fn serialize_u32(self, v: u32) -> Result { - Ok((v + 1).ilog10() as usize + 1) - } - - fn serialize_u64(self, v: u64) -> Result { - Ok((v + 1).ilog10() as usize + 1) - } - - fn serialize_f32(self, _v: f32) -> Result { - unimplemented!("serialize_f32") - } - - fn serialize_f64(self, _v: f64) -> Result { - unimplemented!("serialize_f64") - } - - fn serialize_char(self, _v: char) -> Result { - unimplemented!("serialize_char") - } - - fn serialize_str(self, v: &str) -> Result { - self.serialize_bytes(v.as_bytes()) - } - - fn serialize_bytes(self, v: &[u8]) -> Result { - // return the byte length of the resulting utf-8 string when b64-encoding the given - // byte slice with padding, taking into account that we're probably going to encrypt the value. - // +2 for the quotes - Ok(enc_base64_size_with_pad(v) + 2) - } - - fn serialize_none(self) -> Result { - Ok("null".len()) - } - - fn serialize_some(self, value: &T) -> Result - where - T: ?Sized + Serialize, - { - value.serialize(self) - } - - fn serialize_unit(self) -> Result { - Ok("null".len()) - } - - fn serialize_unit_struct(self, _name: &'static str) -> Result { - unimplemented!("serialize_unit_struct") - } - - fn serialize_unit_variant( - self, - _name: &'static str, - _variant_index: u32, - _variant: &'static str, - ) -> Result { - unimplemented!("serialize_unit_variant") - } - - fn serialize_newtype_struct( - self, - name: &'static str, - value: &T, - ) -> Result - where - T: ?Sized + Serialize, - { - use crate::date::DATETIME_STRUCT_NAME; - use crate::id::custom_id::CUSTOM_ID_STRUCT_NAME; - use crate::id::generated_id::GENERATED_ID_STRUCT_NAME; - - match name { - DATETIME_STRUCT_NAME | GENERATED_ID_STRUCT_NAME | CUSTOM_ID_STRUCT_NAME => { - value.serialize(self) - }, - _ => unimplemented!("serialize_newtype_struct"), - } - } - - fn serialize_newtype_variant( - self, - _name: &'static str, - _variant_index: u32, - _variant: &'static str, - _value: &T, - ) -> Result - where - T: ?Sized + Serialize, - { - unimplemented!("serialize_newtype_variant") - } - - fn serialize_seq(self, len: Option) -> Result { - let Some(len) = len else { - return Err(SizeEstimationError("serialize_map".into())); - }; - // starting with the brackets + commas - Ok(SizeEstimatingCompoundSerializer( - CompoundType::Seq, - 2 + len.saturating_sub(1), - )) - } - - fn serialize_tuple(self, _len: usize) -> Result { - unreachable!() - } - - fn serialize_tuple_struct( - self, - _name: &'static str, - _len: usize, - ) -> Result { - unreachable!() - } - - fn serialize_tuple_variant( - self, - _name: &'static str, - _variant_index: u32, - _variant: &'static str, - _len: usize, - ) -> Result { - unreachable!() - } - - /// maps are only used for the _finalIvs field which is not encrypted. - fn serialize_map(self, len: Option) -> Result { - let Some(len) = len else { - return Err(SizeEstimationError("serialize_map".into())); - }; - // starting with the braces + colons + one comma for each field after the first - Ok(SizeEstimatingCompoundSerializer( - CompoundType::Map, - 2 + (len + len).saturating_sub(1), - )) - } - - fn serialize_struct( - self, - name: &'static str, - len: usize, - ) -> Result { - use crate::id::id_tuple::{ID_TUPLE_CUSTOM_NAME, ID_TUPLE_GENERATED_NAME}; - use CompoundType::*; - match name { - // braces + one comma - ID_TUPLE_GENERATED_NAME | ID_TUPLE_CUSTOM_NAME => { - Ok(SizeEstimatingCompoundSerializer(IdTuple, 3)) - }, - // braces + colons + one comma for each field after the first - _ => Ok(SizeEstimatingCompoundSerializer( - Struct, - 2 + (len + len).saturating_sub(1), - )), - } - } - - fn serialize_struct_variant( - self, - _name: &'static str, - _variant_index: u32, - _variant: &'static str, - _len: usize, - ) -> Result { - unreachable!() - } + type Ok = usize; + type Error = SizeEstimationError; + type SerializeSeq = SizeEstimatingCompoundSerializer; + type SerializeTuple = ser::Impossible; + type SerializeTupleStruct = ser::Impossible; + type SerializeTupleVariant = ser::Impossible; + type SerializeMap = SizeEstimatingCompoundSerializer; + type SerializeStruct = SizeEstimatingCompoundSerializer; + type SerializeStructVariant = ser::Impossible; + + fn serialize_bool(self, v: bool) -> Result { + Ok(if v { 4 } else { 5 }) + } + + fn serialize_i8(self, _v: i8) -> Result { + unimplemented!("serialize_i8"); + } + + fn serialize_i16(self, _v: i16) -> Result { + unimplemented!("serialize_i16"); + } + + fn serialize_i32(self, _v: i32) -> Result { + unimplemented!("serialize_i32"); + } + + fn serialize_i64(self, _v: i64) -> Result { + unimplemented!("serialize_i64"); + } + + fn serialize_u8(self, _v: u8) -> Result { + unimplemented!("serialize u8"); + } + + fn serialize_u16(self, _v: u16) -> Result { + unimplemented!("serialize_u16") + } + + fn serialize_u32(self, v: u32) -> Result { + Ok((v + 1).ilog10() as usize + 1) + } + + fn serialize_u64(self, v: u64) -> Result { + Ok((v + 1).ilog10() as usize + 1) + } + + fn serialize_f32(self, _v: f32) -> Result { + unimplemented!("serialize_f32") + } + + fn serialize_f64(self, _v: f64) -> Result { + unimplemented!("serialize_f64") + } + + fn serialize_char(self, _v: char) -> Result { + unimplemented!("serialize_char") + } + + fn serialize_str(self, v: &str) -> Result { + self.serialize_bytes(v.as_bytes()) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + // return the byte length of the resulting utf-8 string when b64-encoding the given + // byte slice with padding, taking into account that we're probably going to encrypt the value. + // +2 for the quotes + Ok(enc_base64_size_with_pad(v) + 2) + } + + fn serialize_none(self) -> Result { + Ok("null".len()) + } + + fn serialize_some(self, value: &T) -> Result + where + T: ?Sized + Serialize, + { + value.serialize(self) + } + + fn serialize_unit(self) -> Result { + Ok("null".len()) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + unimplemented!("serialize_unit_struct") + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result { + unimplemented!("serialize_unit_variant") + } + + fn serialize_newtype_struct( + self, + name: &'static str, + value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + use crate::date::DATETIME_STRUCT_NAME; + use crate::id::custom_id::CUSTOM_ID_STRUCT_NAME; + use crate::id::generated_id::GENERATED_ID_STRUCT_NAME; + + match name { + DATETIME_STRUCT_NAME | GENERATED_ID_STRUCT_NAME | CUSTOM_ID_STRUCT_NAME => { + value.serialize(self) + } + _ => unimplemented!("serialize_newtype_struct"), + } + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + unimplemented!("serialize_newtype_variant") + } + + fn serialize_seq(self, len: Option) -> Result { + let Some(len) = len else { + return Err(SizeEstimationError("serialize_map".into())); + }; + // starting with the brackets + commas + Ok(SizeEstimatingCompoundSerializer( + CompoundType::Seq, + 2 + len.saturating_sub(1), + )) + } + + fn serialize_tuple(self, _len: usize) -> Result { + unreachable!() + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + unreachable!() + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + unreachable!() + } + + /// maps are only used for the _finalIvs field which is not encrypted. + fn serialize_map(self, len: Option) -> Result { + let Some(len) = len else { + return Err(SizeEstimationError("serialize_map".into())); + }; + // starting with the braces + colons + one comma for each field after the first + Ok(SizeEstimatingCompoundSerializer( + CompoundType::Map, + 2 + (len + len).saturating_sub(1), + )) + } + + fn serialize_struct( + self, + name: &'static str, + len: usize, + ) -> Result { + use crate::id::id_tuple::{ID_TUPLE_CUSTOM_NAME, ID_TUPLE_GENERATED_NAME}; + use CompoundType::*; + match name { + // braces + one comma + ID_TUPLE_GENERATED_NAME | ID_TUPLE_CUSTOM_NAME => { + Ok(SizeEstimatingCompoundSerializer(IdTuple, 3)) + } + // braces + colons + one comma for each field after the first + _ => Ok(SizeEstimatingCompoundSerializer( + Struct, + 2 + (len + len).saturating_sub(1), + )), + } + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + unreachable!() + } } struct SizeEstimatingPlaintextSerializer; @@ -256,238 +253,238 @@ struct SizeEstimatingPlaintextSerializer; /// to be used for objects that we know will not be encrypted or encoded, /// eg struct field names, ids. impl<'a> Serializer for &'a mut SizeEstimatingPlaintextSerializer { - type Ok = usize; - type Error = SizeEstimationError; - type SerializeSeq = ser::Impossible; - type SerializeTuple = ser::Impossible; - type SerializeTupleStruct = ser::Impossible; - type SerializeTupleVariant = ser::Impossible; - type SerializeMap = ser::Impossible; - type SerializeStruct = ser::Impossible; - type SerializeStructVariant = ser::Impossible; - - fn serialize_bool(self, _v: bool) -> Result { - unimplemented!("serialize_bool") - } - - fn serialize_i8(self, _v: i8) -> Result { - unimplemented!("serialize_i8") - } - - fn serialize_i16(self, _v: i16) -> Result { - unimplemented!("serialize_i16") - } - - fn serialize_i32(self, _v: i32) -> Result { - unimplemented!("serialize_i32") - } - - fn serialize_i64(self, _v: i64) -> Result { - unimplemented!("serialize_i64") - } - - fn serialize_u8(self, _v: u8) -> Result { - unimplemented!("serialize_u8") - } - - fn serialize_u16(self, _v: u16) -> Result { - unimplemented!("serialize_u16") - } - - fn serialize_u32(self, _v: u32) -> Result { - unimplemented!("serialize_iu32") - } - - fn serialize_u64(self, _v: u64) -> Result { - unimplemented!("serialize_u64") - } - - fn serialize_f32(self, _v: f32) -> Result { - unimplemented!("serialize_f32") - } - - fn serialize_f64(self, _v: f64) -> Result { - unimplemented!("serialize_f64") - } - - fn serialize_char(self, _v: char) -> Result { - unimplemented!("serialize_char") - } - - fn serialize_str(self, v: &str) -> Result { - Ok(v.len() + 2) - } - - fn serialize_bytes(self, v: &[u8]) -> Result { - Ok(plain_base64_size_with_pad(v) + 2) - } - - fn serialize_none(self) -> Result { - unimplemented!("serialize_none") - } - - fn serialize_some(self, _value: &T) -> Result - where - T: ?Sized + Serialize, - { - unimplemented!("serialize_some") - } - - fn serialize_unit(self) -> Result { - unimplemented!("serialize_unit") - } - - fn serialize_unit_struct(self, _name: &'static str) -> Result { - unimplemented!("serialize_unit_struct") - } - - fn serialize_unit_variant( - self, - _name: &'static str, - _variant_index: u32, - _variant: &'static str, - ) -> Result { - unimplemented!("serialize_unit_variant") - } - - fn serialize_newtype_struct( - self, - _name: &'static str, - value: &T, - ) -> Result - where - T: ?Sized + Serialize, - { - value.serialize(self) - } - - fn serialize_newtype_variant( - self, - _name: &'static str, - _variant_index: u32, - _variant: &'static str, - _value: &T, - ) -> Result - where - T: ?Sized + Serialize, - { - unimplemented!("serialize_newtype_variant") - } - - fn serialize_seq(self, _len: Option) -> Result { - unimplemented!("serialize_seq") - } - - fn serialize_tuple(self, _len: usize) -> Result { - unimplemented!("serialize_tuple") - } - - fn serialize_tuple_struct( - self, - _name: &'static str, - _len: usize, - ) -> Result { - unimplemented!("serialize_tuple_struct") - } - - fn serialize_tuple_variant( - self, - _name: &'static str, - _variant_index: u32, - _variant: &'static str, - _len: usize, - ) -> Result { - unimplemented!("serialize_tuple_variant") - } - - fn serialize_map(self, _len: Option) -> Result { - unimplemented!("serialize_map") - } - - fn serialize_struct( - self, - _name: &'static str, - _len: usize, - ) -> Result { - unimplemented!("serialize_struct") - } - - fn serialize_struct_variant( - self, - _name: &'static str, - _variant_index: u32, - _variant: &'static str, - _len: usize, - ) -> Result { - unimplemented!("serialize_struct_variant") - } + type Ok = usize; + type Error = SizeEstimationError; + type SerializeSeq = ser::Impossible; + type SerializeTuple = ser::Impossible; + type SerializeTupleStruct = ser::Impossible; + type SerializeTupleVariant = ser::Impossible; + type SerializeMap = ser::Impossible; + type SerializeStruct = ser::Impossible; + type SerializeStructVariant = ser::Impossible; + + fn serialize_bool(self, _v: bool) -> Result { + unimplemented!("serialize_bool") + } + + fn serialize_i8(self, _v: i8) -> Result { + unimplemented!("serialize_i8") + } + + fn serialize_i16(self, _v: i16) -> Result { + unimplemented!("serialize_i16") + } + + fn serialize_i32(self, _v: i32) -> Result { + unimplemented!("serialize_i32") + } + + fn serialize_i64(self, _v: i64) -> Result { + unimplemented!("serialize_i64") + } + + fn serialize_u8(self, _v: u8) -> Result { + unimplemented!("serialize_u8") + } + + fn serialize_u16(self, _v: u16) -> Result { + unimplemented!("serialize_u16") + } + + fn serialize_u32(self, _v: u32) -> Result { + unimplemented!("serialize_iu32") + } + + fn serialize_u64(self, _v: u64) -> Result { + unimplemented!("serialize_u64") + } + + fn serialize_f32(self, _v: f32) -> Result { + unimplemented!("serialize_f32") + } + + fn serialize_f64(self, _v: f64) -> Result { + unimplemented!("serialize_f64") + } + + fn serialize_char(self, _v: char) -> Result { + unimplemented!("serialize_char") + } + + fn serialize_str(self, v: &str) -> Result { + Ok(v.len() + 2) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + Ok(plain_base64_size_with_pad(v) + 2) + } + + fn serialize_none(self) -> Result { + unimplemented!("serialize_none") + } + + fn serialize_some(self, _value: &T) -> Result + where + T: ?Sized + Serialize, + { + unimplemented!("serialize_some") + } + + fn serialize_unit(self) -> Result { + unimplemented!("serialize_unit") + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + unimplemented!("serialize_unit_struct") + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result { + unimplemented!("serialize_unit_variant") + } + + fn serialize_newtype_struct( + self, + _name: &'static str, + value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + unimplemented!("serialize_newtype_variant") + } + + fn serialize_seq(self, _len: Option) -> Result { + unimplemented!("serialize_seq") + } + + fn serialize_tuple(self, _len: usize) -> Result { + unimplemented!("serialize_tuple") + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + unimplemented!("serialize_tuple_struct") + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + unimplemented!("serialize_tuple_variant") + } + + fn serialize_map(self, _len: Option) -> Result { + unimplemented!("serialize_map") + } + + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + unimplemented!("serialize_struct") + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + unimplemented!("serialize_struct_variant") + } } /// the compound types that are handled by the serialization. /// some types are special-cased. enum CompoundType { - /// id tuples are strings, but serialize as a sequence. - IdTuple, - Struct, - Map, - Seq, + /// id tuples are strings, but serialize as a sequence. + IdTuple, + Struct, + Map, + Seq, } /// struct SizeEstimatingCompoundSerializer(CompoundType, usize); impl<'a> SerializeSeq for SizeEstimatingCompoundSerializer { - type Ok = usize; - type Error = SizeEstimationError; - - fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> - where - T: ?Sized + Serialize, - { - self.1 += value.serialize(&mut SizeEstimatingSerializer)?; - Ok(()) - } - - fn end(self) -> Result { - Ok(self.1) - } + type Ok = usize; + type Error = SizeEstimationError; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + self.1 += value.serialize(&mut SizeEstimatingSerializer)?; + Ok(()) + } + + fn end(self) -> Result { + Ok(self.1) + } } /// maps are only used for the _finalIvs fields which are not encrypted. impl<'a> SerializeMap for SizeEstimatingCompoundSerializer { - type Ok = usize; - type Error = SizeEstimationError; - - fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> - where - T: ?Sized + Serialize, - { - self.1 += key.serialize(&mut SizeEstimatingPlaintextSerializer)?; - Ok(()) - } - - fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> - where - T: ?Sized + Serialize, - { - self.1 += value.serialize(&mut SizeEstimatingPlaintextSerializer)?; - Ok(()) - } - - fn end(self) -> Result { - Ok(self.1) - } + type Ok = usize; + type Error = SizeEstimationError; + + fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + self.1 += key.serialize(&mut SizeEstimatingPlaintextSerializer)?; + Ok(()) + } + + fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + self.1 += value.serialize(&mut SizeEstimatingPlaintextSerializer)?; + Ok(()) + } + + fn end(self) -> Result { + Ok(self.1) + } } impl<'a> SerializeStruct for SizeEstimatingCompoundSerializer { - type Ok = usize; - type Error = SizeEstimationError; - - fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error> - where - T: ?Sized + Serialize, - { - match self.0 { + type Ok = usize; + type Error = SizeEstimationError; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + match self.0 { CompoundType::IdTuple => { match key { "list_id" | "element_id" => self.1 += value.serialize(&mut SizeEstimatingPlaintextSerializer)?, @@ -498,203 +495,203 @@ impl<'a> SerializeStruct for SizeEstimatingCompoundSerializer { _ => unreachable!("shouldn't call SerializeStruct::serialize_field while not serializing an IdTuple or struct") } - Ok(()) - } + Ok(()) + } - fn end(self) -> Result { - Ok(self.1) - } + fn end(self) -> Result { + Ok(self.1) + } } fn enc_base64_size_with_pad(bytes: &[u8]) -> usize { - // b64 encodes 3 bytes with 4 ascii chars, rounded up. - // since we're encrypting and padding to the block size, - // we also need to add more overhead for that. - (bytes.len() + MINIMUM_ENCRYPTED_SLICE_SIZE) - .div_ceil(3) - .saturating_mul(4) + // b64 encodes 3 bytes with 4 ascii chars, rounded up. + // since we're encrypting and padding to the block size, + // we also need to add more overhead for that. + (bytes.len() + MINIMUM_ENCRYPTED_SLICE_SIZE) + .div_ceil(3) + .saturating_mul(4) } fn plain_base64_size_with_pad(bytes: &[u8]) -> usize { - // b64 encodes 3 bytes with 4 ascii chars, rounded up. - // since we're padding to the block size, we also need to add more overhead for that. - bytes.len().div_ceil(3).saturating_mul(4) + // b64 encodes 3 bytes with 4 ascii chars, rounded up. + // since we're padding to the block size, we also need to add more overhead for that. + bytes.len().div_ceil(3).saturating_mul(4) } #[cfg(test)] mod tests { - use super::{enc_base64_size_with_pad, estimate_json_size, SizeEstimatingSerializer}; - use crate::date::DateTime; - use crate::entities::FinalIv; - use crate::{CustomId, GeneratedId, IdTupleCustom, IdTupleGenerated, TypeRef}; - use serde::Serialize; - use std::collections::HashMap; - - #[derive(Serialize)] - struct FooBarBaz { - pub field_a: A, - pub field_b: B, - pub field_c: C, - } - - impl crate::entities::Entity for FooBarBaz { - fn type_ref() -> TypeRef { - unreachable!() - } - } - - #[test] - fn estimate_struct_size() { - let foo: FooBarBaz = FooBarBaz { - field_a: 0, - field_b: 234, - field_c: true, - }; - assert_eq!( - r#"{"field_a":0,"field_b":234,"field_c":true}"#.len(), - estimate_json_size(&foo) - ); - - let foo2: FooBarBaz = FooBarBaz { - field_a: IdTupleGenerated { - list_id: GeneratedId("moo".to_string()), - element_id: GeneratedId("wuff".to_string()), - }, - field_b: IdTupleCustom { - list_id: GeneratedId("meow".to_string()), - element_id: CustomId("123".to_string()), - }, - field_c: DateTime::from_millis(1753355555555), - }; - - assert_eq!( - r#"{"field_a":["moo","wuff"],"field_b":["meow","123"],"field_c":1753355555555}"#.len(), - estimate_json_size(&foo2) - ); - - let foo3: FooBarBaz, Option<()>, ()> = FooBarBaz { - field_a: None, - field_b: Some(()), - field_c: (), - }; - assert_eq!( - r#"{"field_a":null,"field_b":null,"field_c":null}"#.len(), - estimate_json_size(&foo3) - ); - } - - #[test] - fn estimate_map_size() { - let value = HashMap::from([ - ("some", FinalIv(Vec::from(b"0"))), - ("other", FinalIv(Vec::from(b"234"))), - ]); - assert_eq!( - r#"{"some":"MAo=","other":"===="}"#.len(), - // maps are only used for the _finalIvs fields which are not encrypted. - value.serialize(&mut SizeEstimatingSerializer).unwrap() - ); - } - - #[test] - fn estimate_bool_size() { - assert_eq!(4, true.serialize(&mut SizeEstimatingSerializer).unwrap()); - assert_eq!(5, false.serialize(&mut SizeEstimatingSerializer).unwrap()); - } - - #[test] - fn estimate_str_size() { - assert_eq!( - enc_base64_size_with_pad(b"foo") + 2, - "foo".serialize(&mut SizeEstimatingSerializer).unwrap() - ); - assert_eq!( - enc_base64_size_with_pad(b"") + 2, - "".serialize(&mut SizeEstimatingSerializer).unwrap() - ); - } - - #[test] - fn estimate_num_size() { - assert_eq!(1, 0_u32.serialize(&mut SizeEstimatingSerializer).unwrap()); - assert_eq!(2, 10_u32.serialize(&mut SizeEstimatingSerializer).unwrap()); - } - - #[test] - fn estimate_vec_size() { - assert_eq!( - 2, - Vec::::new() - .serialize(&mut SizeEstimatingSerializer) - .unwrap() - ); - assert_eq!( - "[0,10]".len(), - vec![0_u32, 10_u32] - .serialize(&mut SizeEstimatingSerializer) - .unwrap() - ); - assert_eq!( - "[true,false]".len(), - vec![true, false] - .serialize(&mut SizeEstimatingSerializer) - .unwrap() - ); - assert_eq!( - "[true]".len(), - vec![true].serialize(&mut SizeEstimatingSerializer).unwrap() - ); - assert_eq!( - r#"["",""]"#.len() + enc_base64_size_with_pad(b"0") + enc_base64_size_with_pad(b"10"), - vec!["0", "10"] - .serialize(&mut SizeEstimatingSerializer) - .unwrap() - ); - } - - #[test] - fn estimate_bytes_size() { - // using FinalIv because it's annotated to use serde_bytes for the byte vector serialization. - // serde serializes a bare &[u8] as a sequence or tuple by default - assert_eq!( - enc_base64_size_with_pad(b"") + 2, - FinalIv(b"".as_slice().to_owned()) - .serialize(&mut SizeEstimatingSerializer) - .unwrap() - ); - assert_eq!( - enc_base64_size_with_pad(b"0") + 2, - FinalIv(b"0".as_slice().to_owned()) - .serialize(&mut SizeEstimatingSerializer) - .unwrap() - ); - assert_eq!( - enc_base64_size_with_pad(b"hello") + 2, - FinalIv(b"hello".as_slice().to_owned()) - .serialize(&mut SizeEstimatingSerializer) - .unwrap() - ); - } - - #[test] - fn estimate_date_size() { - assert_eq!( - "123456".len(), - DateTime::from_millis(123456) - .serialize(&mut SizeEstimatingSerializer) - .unwrap() - ); - } - - #[test] - fn estimate_id_tuple() { - let id = IdTupleGenerated::new( - GeneratedId("abc".to_string()), - GeneratedId("defg".to_string()), - ); - assert_eq!( - r#"["abc","defg"]"#.len(), - id.serialize(&mut SizeEstimatingSerializer).unwrap() - ); - } + use super::{enc_base64_size_with_pad, estimate_json_size, SizeEstimatingSerializer}; + use crate::date::DateTime; + use crate::entities::FinalIv; + use crate::{CustomId, GeneratedId, IdTupleCustom, IdTupleGenerated, TypeRef}; + use serde::Serialize; + use std::collections::HashMap; + + #[derive(Serialize)] + struct FooBarBaz { + pub field_a: A, + pub field_b: B, + pub field_c: C, + } + + impl crate::entities::Entity for FooBarBaz { + fn type_ref() -> TypeRef { + unreachable!() + } + } + + #[test] + fn estimate_struct_size() { + let foo: FooBarBaz = FooBarBaz { + field_a: 0, + field_b: 234, + field_c: true, + }; + assert_eq!( + r#"{"field_a":0,"field_b":234,"field_c":true}"#.len(), + estimate_json_size(&foo) + ); + + let foo2: FooBarBaz = FooBarBaz { + field_a: IdTupleGenerated { + list_id: GeneratedId("moo".to_string()), + element_id: GeneratedId("wuff".to_string()), + }, + field_b: IdTupleCustom { + list_id: GeneratedId("meow".to_string()), + element_id: CustomId("123".to_string()), + }, + field_c: DateTime::from_millis(1753355555555), + }; + + assert_eq!( + r#"{"field_a":["moo","wuff"],"field_b":["meow","123"],"field_c":1753355555555}"#.len(), + estimate_json_size(&foo2) + ); + + let foo3: FooBarBaz, Option<()>, ()> = FooBarBaz { + field_a: None, + field_b: Some(()), + field_c: (), + }; + assert_eq!( + r#"{"field_a":null,"field_b":null,"field_c":null}"#.len(), + estimate_json_size(&foo3) + ); + } + + #[test] + fn estimate_map_size() { + let value = HashMap::from([ + ("some", FinalIv(Vec::from(b"0"))), + ("other", FinalIv(Vec::from(b"234"))), + ]); + assert_eq!( + r#"{"some":"MAo=","other":"===="}"#.len(), + // maps are only used for the _finalIvs fields which are not encrypted. + value.serialize(&mut SizeEstimatingSerializer).unwrap() + ); + } + + #[test] + fn estimate_bool_size() { + assert_eq!(4, true.serialize(&mut SizeEstimatingSerializer).unwrap()); + assert_eq!(5, false.serialize(&mut SizeEstimatingSerializer).unwrap()); + } + + #[test] + fn estimate_str_size() { + assert_eq!( + enc_base64_size_with_pad(b"foo") + 2, + "foo".serialize(&mut SizeEstimatingSerializer).unwrap() + ); + assert_eq!( + enc_base64_size_with_pad(b"") + 2, + "".serialize(&mut SizeEstimatingSerializer).unwrap() + ); + } + + #[test] + fn estimate_num_size() { + assert_eq!(1, 0_u32.serialize(&mut SizeEstimatingSerializer).unwrap()); + assert_eq!(2, 10_u32.serialize(&mut SizeEstimatingSerializer).unwrap()); + } + + #[test] + fn estimate_vec_size() { + assert_eq!( + 2, + Vec::::new() + .serialize(&mut SizeEstimatingSerializer) + .unwrap() + ); + assert_eq!( + "[0,10]".len(), + vec![0_u32, 10_u32] + .serialize(&mut SizeEstimatingSerializer) + .unwrap() + ); + assert_eq!( + "[true,false]".len(), + vec![true, false] + .serialize(&mut SizeEstimatingSerializer) + .unwrap() + ); + assert_eq!( + "[true]".len(), + vec![true].serialize(&mut SizeEstimatingSerializer).unwrap() + ); + assert_eq!( + r#"["",""]"#.len() + enc_base64_size_with_pad(b"0") + enc_base64_size_with_pad(b"10"), + vec!["0", "10"] + .serialize(&mut SizeEstimatingSerializer) + .unwrap() + ); + } + + #[test] + fn estimate_bytes_size() { + // using FinalIv because it's annotated to use serde_bytes for the byte vector serialization. + // serde serializes a bare &[u8] as a sequence or tuple by default + assert_eq!( + enc_base64_size_with_pad(b"") + 2, + FinalIv(b"".as_slice().to_owned()) + .serialize(&mut SizeEstimatingSerializer) + .unwrap() + ); + assert_eq!( + enc_base64_size_with_pad(b"0") + 2, + FinalIv(b"0".as_slice().to_owned()) + .serialize(&mut SizeEstimatingSerializer) + .unwrap() + ); + assert_eq!( + enc_base64_size_with_pad(b"hello") + 2, + FinalIv(b"hello".as_slice().to_owned()) + .serialize(&mut SizeEstimatingSerializer) + .unwrap() + ); + } + + #[test] + fn estimate_date_size() { + assert_eq!( + "123456".len(), + DateTime::from_millis(123456) + .serialize(&mut SizeEstimatingSerializer) + .unwrap() + ); + } + + #[test] + fn estimate_id_tuple() { + let id = IdTupleGenerated::new( + GeneratedId("abc".to_string()), + GeneratedId("defg".to_string()), + ); + assert_eq!( + r#"["abc","defg"]"#.len(), + id.serialize(&mut SizeEstimatingSerializer).unwrap() + ); + } } diff --git a/tuta-sdk/rust/sdk/src/key_loader_facade.rs b/tuta-sdk/rust/sdk/src/key_loader_facade.rs index 5df9cb4154c..f400c749cc4 100644 --- a/tuta-sdk/rust/sdk/src/key_loader_facade.rs +++ b/tuta-sdk/rust/sdk/src/key_loader_facade.rs @@ -5,7 +5,6 @@ use crate::entities::generated::sys::{Group, GroupKey}; use crate::typed_entity_client::TypedEntityClient; #[cfg_attr(test, mockall_double::double)] use crate::user_facade::UserFacade; -use crate::util::Versioned; use crate::CustomId; use crate::GeneratedId; use crate::ListLoadDirection; From 6075ec761704ba77b2da8999f1150d10605e36b0 Mon Sep 17 00:00:00 2001 From: map Date: Wed, 13 Nov 2024 09:42:49 +0100 Subject: [PATCH 14/32] improved mime handling --- packages/node-mimimi/Cargo.toml | 1 + packages/node-mimimi/src/importer.rs | 658 +++++++++--------- .../src/importer/file_reader/import_client.rs | 2 +- .../src/importer/importable_mail.rs | 267 +++---- .../mime_string_to_importable_mail_test.rs | 315 ++++----- .../msg_file_compatibility_test.rs | 6 +- 6 files changed, 623 insertions(+), 626 deletions(-) diff --git a/packages/node-mimimi/Cargo.toml b/packages/node-mimimi/Cargo.toml index 0e32997708a..5b6522f833d 100644 --- a/packages/node-mimimi/Cargo.toml +++ b/packages/node-mimimi/Cargo.toml @@ -39,6 +39,7 @@ features = ["serde", "starttls", "ext_id", "ext_metadata"] napi-build = { version = "2.1.3" } [dev-dependencies] +base64 = "0.22.1" tokio = { version = "1", features = ["full"] } serde_json = "1" diff --git a/packages/node-mimimi/src/importer.rs b/packages/node-mimimi/src/importer.rs index 51177048cf8..7af673344ec 100644 --- a/packages/node-mimimi/src/importer.rs +++ b/packages/node-mimimi/src/importer.rs @@ -26,13 +26,13 @@ mod importable_mail; #[derive(Clone, PartialEq)] pub enum ImportParams { - Imap { - imap_import_config: ImapImportConfig, - }, - LocalFile { - file_path: String, - is_mbox: bool, - }, + Imap { + imap_import_config: ImapImportConfig, + }, + LocalFile { + file_path: String, + is_mbox: bool, + }, } /// current state of the imap_reader import for this tuta account @@ -42,51 +42,51 @@ pub enum ImportParams { #[derive(PartialEq, Default)] #[cfg_attr(test, derive(Debug))] pub enum ImportState { - #[default] - NotInitialized, - Paused, - Running, - Postponed, - Finished, + #[default] + NotInitialized, + Paused, + Running, + Postponed, + Finished, } #[cfg_attr(feature = "javascript", napi_derive::napi(object))] #[derive(PartialEq, Clone, Default)] #[cfg_attr(test, derive(Debug))] pub struct ImportStatus { - pub state: ImportState, - pub imported_mails: u32, + pub state: ImportState, + pub imported_mails: u32, } struct Importer { - status: ImportStatus, - logged_in_sdk: Arc, - target_owner_group: GeneratedId, - target_mail_folder: IdTupleGenerated, - import_source: Arc>, - randomizer_facade: RandomizerFacade, + status: ImportStatus, + logged_in_sdk: Arc, + target_owner_group: GeneratedId, + target_mail_folder: IdTupleGenerated, + import_source: Arc>, + randomizer_facade: RandomizerFacade, } pub enum ImportSource { - RemoteImap { imap_import_client: ImapImport }, - LocalFile { fs_email_client: FileImport }, + RemoteImap { imap_import_client: ImapImport }, + LocalFile { fs_email_client: FileImport }, } /// Wrapper for `Importer` to be used from napi-rs interface #[cfg_attr(feature = "javascript", napi_derive::napi)] pub struct ImporterApi { - inner: Arc>, + inner: Arc>, } #[derive(Debug, PartialEq, Clone)] pub enum IterationError { - Imap(ImapIterationError), - File(FileIterationError), + Imap(ImapIterationError), + File(FileIterationError), } struct ImportSourceIterator { // it would be nice to not need the mutex, but when the importer continues the import, - // it mutates its own state and also calls mutating functions on the source. solving this + // it mutates its own state and also calls mutating functions on the source. solving this // probably requires a bigger restructure of the code (it's very OOP atm) source: Arc>, } @@ -113,7 +113,7 @@ impl Iterator for ImportSourceIterator { Err(IterationError::File(FileIterationError::SourceEnd)) | Err(IterationError::Imap(ImapIterationError::SourceEnd)) => { None - } + }, Err(e) => { // once we handle this case we will need another iterator that filters (and logs) the @@ -131,30 +131,30 @@ impl Importer { Ok(self.status.clone()) } - /// once we get the ImportableMail from either of source, - /// continue to the uploading counterpart - async fn import_all_mail( - &mut self, + /// once we get the ImportableMail from either of source, + /// continue to the uploading counterpart + async fn import_all_mail( + &mut self, importable_mails: Iter, ) -> Result, ()> where Iter: Iterator, { - let new_aes_256_key = GenericAesKey::from_bytes( - self.randomizer_facade - .generate_random_array::<{ tutasdk::crypto::aes::AES_256_KEY_SIZE }>() - .as_slice(), - ) - .unwrap(); - let mail_group_key = self - .logged_in_sdk - .get_current_sym_group_key(&self.target_owner_group) - .await - .map_err(|_e| ())?; - let owner_enc_session_key = - mail_group_key.encrypt_key(&new_aes_256_key, Iv::generate(&self.randomizer_facade)); - - const MAX_REQUEST_SIZE: usize = 1024 * 1024 * 10; + let new_aes_256_key = GenericAesKey::from_bytes( + self.randomizer_facade + .generate_random_array::<{ tutasdk::crypto::aes::AES_256_KEY_SIZE }>() + .as_slice(), + ) + .unwrap(); + let mail_group_key = self + .logged_in_sdk + .get_current_sym_group_key(&self.target_owner_group) + .await + .map_err(|_e| ())?; + let owner_enc_session_key = + mail_group_key.encrypt_key(&new_aes_256_key, Iv::generate(&self.randomizer_facade)); + + const MAX_REQUEST_SIZE: usize = 1024 * 1024 * 10; let import_chunks: Vec> = reduce_to_chunks( importable_mails.map(ImportMailData::from), MAX_REQUEST_SIZE, @@ -214,305 +214,305 @@ impl Importer { self.status = new_status; Ok(mails) - } + } } impl ImporterApi { - pub fn new( - logged_in_sdk: Arc, - target_owner_group: GeneratedId, - target_mail_folder: IdTupleGenerated, - import_source: Arc>, - ) -> Self { - let import_inner = Importer { - logged_in_sdk, - target_owner_group, - target_mail_folder, - import_source, - status: ImportStatus::default(), - randomizer_facade: RandomizerFacade::from_core(rand::rngs::OsRng), - }; - Self { - inner: Arc::new(NapiTokioMutex::new(import_inner)), - } - } - - pub async fn continue_import_inner(&mut self) -> Result { - self.inner.lock().await.continue_import().await - } - - pub async fn delete_import_inner(&mut self) -> Result { - todo!() - } - - pub async fn pause_import_inner(&mut self) -> Result { - todo!() - } - - pub async fn create_file_importer_inner( - tuta_credentials: TutaCredentials, - target_owner_group: String, - target_mail_folder: (String, String), - source_paths: Vec, - ) -> napi::Result { - let logged_in_sdk_future = Self::create_sdk(tuta_credentials); - - let fs_email_client = FileImport::new(source_paths) - .map_err(|_e| NapiError::from_reason("Cannot create file import"))?; - let import_source = Arc::new(Mutex::new(ImportSource::LocalFile { fs_email_client })); - let logged_in_sdk = logged_in_sdk_future - .await - .map_err(|_e| NapiError::from_reason("Cannot create logged in sdk"))?; - - Ok(ImporterApi::new( - logged_in_sdk, - GeneratedId(target_owner_group), - IdTupleGenerated::new( - GeneratedId(target_mail_folder.0), - GeneratedId(target_mail_folder.1), - ), - import_source, - )) - } - - async fn create_sdk(tuta_credentials: TutaCredentials) -> Result, String> { - let rest_client = Arc::new( - NativeRestClient::try_new() - .map_err(|e| format!("Cannot build native rest client: {e}"))?, - ); - - let logged_in_sdk = { - let sdk = Sdk::new(tuta_credentials.api_url.clone(), rest_client); - - let sdk_credentials: Credentials = tuta_credentials - .clone() - .try_into() - .map_err(|_| "Cannot convert to valid credentials".to_string())?; - sdk.login(sdk_credentials) - .await - .map_err(|e| format!("Cannot login to sdk. Error: {:?}", e))? - }; - - Ok(logged_in_sdk) - } + pub fn new( + logged_in_sdk: Arc, + target_owner_group: GeneratedId, + target_mail_folder: IdTupleGenerated, + import_source: Arc>, + ) -> Self { + let import_inner = Importer { + logged_in_sdk, + target_owner_group, + target_mail_folder, + import_source, + status: ImportStatus::default(), + randomizer_facade: RandomizerFacade::from_core(rand::rngs::OsRng), + }; + Self { + inner: Arc::new(NapiTokioMutex::new(import_inner)), + } + } + + pub async fn continue_import_inner(&mut self) -> Result { + self.inner.lock().await.continue_import().await + } + + pub async fn delete_import_inner(&mut self) -> Result { + todo!() + } + + pub async fn pause_import_inner(&mut self) -> Result { + todo!() + } + + pub async fn create_file_importer_inner( + tuta_credentials: TutaCredentials, + target_owner_group: String, + target_mail_folder: (String, String), + source_paths: Vec, + ) -> napi::Result { + let logged_in_sdk_future = Self::create_sdk(tuta_credentials); + + let fs_email_client = FileImport::new(source_paths) + .map_err(|_e| NapiError::from_reason("Cannot create file import"))?; + let import_source = Arc::new(Mutex::new(ImportSource::LocalFile { fs_email_client })); + let logged_in_sdk = logged_in_sdk_future + .await + .map_err(|_e| NapiError::from_reason("Cannot create logged in sdk"))?; + + Ok(ImporterApi::new( + logged_in_sdk, + GeneratedId(target_owner_group), + IdTupleGenerated::new( + GeneratedId(target_mail_folder.0), + GeneratedId(target_mail_folder.1), + ), + import_source, + )) + } + + async fn create_sdk(tuta_credentials: TutaCredentials) -> Result, String> { + let rest_client = Arc::new( + NativeRestClient::try_new() + .map_err(|e| format!("Cannot build native rest client: {e}"))?, + ); + + let logged_in_sdk = { + let sdk = Sdk::new(tuta_credentials.api_url.clone(), rest_client); + + let sdk_credentials: Credentials = tuta_credentials + .clone() + .try_into() + .map_err(|_| "Cannot convert to valid credentials".to_string())?; + sdk.login(sdk_credentials) + .await + .map_err(|e| format!("Cannot login to sdk. Error: {:?}", e))? + }; + + Ok(logged_in_sdk) + } } // Wrapper for napi #[cfg(feature = "javascript")] #[napi_derive::napi] impl ImporterApi { - // once Self::continue_import return custom error, - // do the error conversion here, or in trait - fn error_conversion(_err: E) -> napi::Error { - todo!() - } - - #[napi] - pub async unsafe fn continue_import(&mut self) -> napi::Result { - self.continue_import_inner() - .await - .map_err(Self::error_conversion) - } - - #[napi] - pub async unsafe fn delete_import(&mut self) -> napi::Result { - self.delete_import_inner() - .await - .map_err(Self::error_conversion) - } - - #[napi] - pub async unsafe fn pause_import(&mut self) -> napi::Result { - self.pause_import_inner() - .await - .map_err(Self::error_conversion) - } - - #[napi] - pub async fn create_file_importer( - tuta_credentials: TutaCredentials, - target_owner_group: String, - target_mail_folder: (String, String), - source_paths: Vec, - ) -> napi::Result { - Self::create_file_importer_inner( - tuta_credentials, - target_owner_group, - target_mail_folder, - source_paths, - ) - .await - } + // once Self::continue_import return custom error, + // do the error conversion here, or in trait + fn error_conversion(_err: E) -> napi::Error { + todo!() + } + + #[napi] + pub async unsafe fn continue_import(&mut self) -> napi::Result { + self.continue_import_inner() + .await + .map_err(Self::error_conversion) + } + + #[napi] + pub async unsafe fn delete_import(&mut self) -> napi::Result { + self.delete_import_inner() + .await + .map_err(Self::error_conversion) + } + + #[napi] + pub async unsafe fn pause_import(&mut self) -> napi::Result { + self.pause_import_inner() + .await + .map_err(Self::error_conversion) + } + + #[napi] + pub async fn create_file_importer( + tuta_credentials: TutaCredentials, + target_owner_group: String, + target_mail_folder: (String, String), + source_paths: Vec, + ) -> napi::Result { + Self::create_file_importer_inner( + tuta_credentials, + target_owner_group, + target_mail_folder, + source_paths, + ) + .await + } } #[cfg(test)] mod tests { - use super::*; - use crate::importer::imap_reader::{ImapCredentials, LoginMechanism}; - use crate::tuta_imap::testing::GreenMailTestServer; - use mail_builder::MessageBuilder; - use tutasdk::entities::generated::tutanota::MailFolder; - use tutasdk::folder_system::MailSetKind; - use tutasdk::net::native_rest_client::NativeRestClient; - use tutasdk::Sdk; - - fn sample_email(subject: String) -> String { - let email = MessageBuilder::new() + use super::*; + use crate::importer::imap_reader::{ImapCredentials, LoginMechanism}; + use crate::tuta_imap::testing::GreenMailTestServer; + use mail_builder::MessageBuilder; + use tutasdk::entities::generated::tutanota::MailFolder; + use tutasdk::folder_system::MailSetKind; + use tutasdk::net::native_rest_client::NativeRestClient; + use tutasdk::Sdk; + + fn sample_email(subject: String) -> String { + let email = MessageBuilder::new() .from(("Matthias", "map@example.org")) .to(("Johannes", "jhm@example.org")) .subject(subject) .text_body("Hello tutao! this is the first step to have email import.Want to see html πŸ˜€?

red

") .write_to_string() .unwrap(); - email - } - - async fn get_test_import_folder_id( - logged_in_sdk: &Arc, - kind: MailSetKind, - ) -> MailFolder { - let mail_facade = logged_in_sdk.mail_facade(); - let mailbox = mail_facade.load_user_mailbox().await.unwrap(); - let folders = mail_facade - .load_folders_for_mailbox(&mailbox) - .await - .unwrap(); - folders - .system_folder_by_type(kind) - .expect("inbox should exist") - .clone() - } - - async fn init_imap_importer() -> (Importer, GreenMailTestServer) { - let importer_mail_address = "map-free@tutanota.de".to_string(); - let logged_in_sdk = Sdk::new( - "http://localhost:9000".to_string(), - Arc::new(NativeRestClient::try_new().unwrap()), - ) - .create_session(importer_mail_address.as_str(), "map") - .await - .unwrap(); - let greenmail = GreenMailTestServer::new(); - let imap_import_config = ImapImportConfig { - root_import_mail_folder_name: "/".to_string(), - credentials: ImapCredentials { - host: "127.0.0.1".to_string(), - port: greenmail.imaps_port.try_into().unwrap(), - login_mechanism: LoginMechanism::Plain { - username: "sug@example.org".to_string(), - password: "sug".to_string(), - }, - }, - }; - - let import_source = Arc::new(Mutex::new(ImportSource::RemoteImap { - imap_import_client: ImapImport::new(imap_import_config), - })); - let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng); - let target_mail_folder = get_test_import_folder_id(&logged_in_sdk, MailSetKind::Archive) - .await - ._id - .unwrap(); - - let target_owner_group = logged_in_sdk - .mail_facade() - .get_group_id_for_mail_address(importer_mail_address.as_str()) - .await - .unwrap(); - - let importer = Importer { - target_owner_group, - target_mail_folder, - logged_in_sdk, - import_source, - randomizer_facade, - status: ImportStatus::default(), - }; - - (importer, greenmail) - } - - pub async fn init_file_importer(source_paths: Vec) -> Importer { - let logged_in_sdk = Sdk::new( - "http://localhost:9000".to_string(), - Arc::new(NativeRestClient::try_new().unwrap()), - ) - .create_session("map-free@tutanota.de", "map") - .await - .unwrap(); - - let import_source = Arc::new(Mutex::new(ImportSource::LocalFile { - fs_email_client: FileImport::new(source_paths).unwrap(), - })); - let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng); - let target_mail_folder = get_test_import_folder_id(&logged_in_sdk, MailSetKind::Archive) - .await - ._id - .unwrap(); - - let target_owner_group = logged_in_sdk - .mail_facade() - .get_group_id_for_mail_address("map-free@tutanota.de") - .await - .unwrap(); - - Importer { - status: ImportStatus::default(), - target_owner_group, - target_mail_folder, - logged_in_sdk, - import_source, - randomizer_facade, - } - } - - #[tokio::test] - pub async fn import_multiple_from_imap_default_folder() { - let (mut importer, greenmail) = init_imap_importer().await; - - let email_first = sample_email("Hello from imap πŸ˜€! -- Бписок.doc".to_string()); - let email_second = sample_email("Second time: hello".to_string()); - greenmail.store_mail("sug@example.org", email_first.as_str()); - greenmail.store_mail("sug@example.org", email_second.as_str()); - - let import_res = importer.continue_import().await.map_err(|_| ()); - assert_eq!( - Ok(ImportStatus { - state: ImportState::Finished, - imported_mails: 2, - }), - import_res - ); - } - - #[tokio::test] - pub async fn import_single_from_imap_default_folder() { - let (mut importer, greenmail) = init_imap_importer().await; - - let email = sample_email("Single email".to_string()); - greenmail.store_mail("sug@example.org", email.as_str()); - - let import_res = importer.continue_import().await.map_err(|_| ()); - assert_eq!( - Ok(ImportStatus { - state: ImportState::Finished, - imported_mails: 1, - }), - import_res - ); - } - - #[tokio::test] - async fn can_import_single_eml_file() { - let mut importer = init_file_importer(vec!["./test/sample.eml".to_string()]).await; - - let import_res = importer.continue_import().await.map_err(|_| ()); - assert_eq!( - Ok(ImportStatus { - state: ImportState::Finished, - imported_mails: 1, - }), - import_res - ); - } + email + } + + async fn get_test_import_folder_id( + logged_in_sdk: &Arc, + kind: MailSetKind, + ) -> MailFolder { + let mail_facade = logged_in_sdk.mail_facade(); + let mailbox = mail_facade.load_user_mailbox().await.unwrap(); + let folders = mail_facade + .load_folders_for_mailbox(&mailbox) + .await + .unwrap(); + folders + .system_folder_by_type(kind) + .expect("inbox should exist") + .clone() + } + + async fn init_imap_importer() -> (Importer, GreenMailTestServer) { + let importer_mail_address = "map-free@tutanota.de".to_string(); + let logged_in_sdk = Sdk::new( + "http://localhost:9000".to_string(), + Arc::new(NativeRestClient::try_new().unwrap()), + ) + .create_session(importer_mail_address.as_str(), "map") + .await + .unwrap(); + let greenmail = GreenMailTestServer::new(); + let imap_import_config = ImapImportConfig { + root_import_mail_folder_name: "/".to_string(), + credentials: ImapCredentials { + host: "127.0.0.1".to_string(), + port: greenmail.imaps_port.try_into().unwrap(), + login_mechanism: LoginMechanism::Plain { + username: "sug@example.org".to_string(), + password: "sug".to_string(), + }, + }, + }; + + let import_source = Arc::new(Mutex::new(ImportSource::RemoteImap { + imap_import_client: ImapImport::new(imap_import_config), + })); + let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng); + let target_mail_folder = get_test_import_folder_id(&logged_in_sdk, MailSetKind::Archive) + .await + ._id + .unwrap(); + + let target_owner_group = logged_in_sdk + .mail_facade() + .get_group_id_for_mail_address(importer_mail_address.as_str()) + .await + .unwrap(); + + let importer = Importer { + target_owner_group, + target_mail_folder, + logged_in_sdk, + import_source, + randomizer_facade, + status: ImportStatus::default(), + }; + + (importer, greenmail) + } + + pub async fn init_file_importer(source_paths: Vec) -> Importer { + let logged_in_sdk = Sdk::new( + "http://localhost:9000".to_string(), + Arc::new(NativeRestClient::try_new().unwrap()), + ) + .create_session("map-free@tutanota.de", "map") + .await + .unwrap(); + + let import_source = Arc::new(Mutex::new(ImportSource::LocalFile { + fs_email_client: FileImport::new(source_paths).unwrap(), + })); + let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng); + let target_mail_folder = get_test_import_folder_id(&logged_in_sdk, MailSetKind::Archive) + .await + ._id + .unwrap(); + + let target_owner_group = logged_in_sdk + .mail_facade() + .get_group_id_for_mail_address("map-free@tutanota.de") + .await + .unwrap(); + + Importer { + status: ImportStatus::default(), + target_owner_group, + target_mail_folder, + logged_in_sdk, + import_source, + randomizer_facade, + } + } + + #[tokio::test] + pub async fn import_multiple_from_imap_default_folder() { + let (mut importer, greenmail) = init_imap_importer().await; + + let email_first = sample_email("Hello from imap πŸ˜€! -- Бписок.doc".to_string()); + let email_second = sample_email("Second time: hello".to_string()); + greenmail.store_mail("sug@example.org", email_first.as_str()); + greenmail.store_mail("sug@example.org", email_second.as_str()); + + let import_res = importer.continue_import().await.map_err(|_| ()); + assert_eq!( + Ok(ImportStatus { + state: ImportState::Finished, + imported_mails: 2, + }), + import_res + ); + } + + #[tokio::test] + pub async fn import_single_from_imap_default_folder() { + let (mut importer, greenmail) = init_imap_importer().await; + + let email = sample_email("Single email".to_string()); + greenmail.store_mail("sug@example.org", email.as_str()); + + let import_res = importer.continue_import().await.map_err(|_| ()); + assert_eq!( + Ok(ImportStatus { + state: ImportState::Finished, + imported_mails: 1, + }), + import_res + ); + } + + #[tokio::test] + async fn can_import_single_eml_file() { + let mut importer = init_file_importer(vec!["./test/sample.eml".to_string()]).await; + + let import_res = importer.continue_import().await.map_err(|_| ()); + assert_eq!( + Ok(ImportStatus { + state: ImportState::Finished, + imported_mails: 1, + }), + import_res + ); + } } diff --git a/packages/node-mimimi/src/importer/file_reader/import_client.rs b/packages/node-mimimi/src/importer/file_reader/import_client.rs index 6ae0bb9f476..c6a512fb028 100644 --- a/packages/node-mimimi/src/importer/file_reader/import_client.rs +++ b/packages/node-mimimi/src/importer/file_reader/import_client.rs @@ -105,7 +105,7 @@ impl FileImport { .message_parser .parse(email_contents.as_slice()) .ok_or_else(|| FileIterationError::MessageParseError("todo1".to_string()))?; - let importable_mail = ImportableMail::try_from(parsed_message) + let importable_mail = ImportableMail::try_from(&parsed_message) .map_err(|e| FileIterationError::MessageParseError(format!("{e:?}")))?; Ok(importable_mail) } diff --git a/packages/node-mimimi/src/importer/importable_mail.rs b/packages/node-mimimi/src/importer/importable_mail.rs index 492eda3936c..0cca75cac21 100644 --- a/packages/node-mimimi/src/importer/importable_mail.rs +++ b/packages/node-mimimi/src/importer/importable_mail.rs @@ -1,9 +1,11 @@ use crate::tuta_imap::client::types::ImapMail; use extend_mail_parser::MakeString; +use mail_builder::headers::Header; use mail_parser::{ - Address, GetHeader, HeaderName, HeaderValue, MessageParser, MessagePart, MessagePartId, - MimeHeaders, PartType, + Address, ContentType, GetHeader, HeaderValue, Message, MessageParser, MessagePart, + MessagePartId, MimeHeaders, PartType, }; +use regex::Regex; use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use std::time::SystemTime; @@ -52,18 +54,13 @@ pub(super) enum ReplyType { ReplyForward = 3, } -#[cfg_attr(test, derive(PartialEq, Debug))] -pub(super) enum ImportableMailAttachment { - Attachment { - filename: Option, - content_type: String, - content_id: String, - content: Vec, - is_inline: bool, - }, - AttachedMessage { - message: ImportableMail, - }, +#[cfg_attr(test, derive(PartialEq, Debug, Clone))] +pub(super) struct ImportableMailAttachment { + filename: String, + content_id: Option, + content_type: String, + content: Vec, + is_inline: bool, } #[cfg_attr(test, derive(PartialEq, Debug))] @@ -171,18 +168,6 @@ impl ImportableMail { email_body_as_html.push_str(html_text); } - fn handle_attached_message( - attachments: &mut Vec, - attached_message: mail_parser::Message, - ) -> Result<(), MailParseError> { - let importable_mail = ImportableMail::try_from(attached_message)?; - let this_attachment = ImportableMailAttachment::AttachedMessage { - message: importable_mail, - }; - attachments.push(this_attachment); - Ok(()) - } - // from the parsed message // return : // .0 a single string that ca be display as email in html format @@ -201,73 +186,62 @@ impl ImportableMail { continue; } - // if not boundary attribute is defined in Content-Type, then the text is treated as comment. - // see: russian.msg - let probably_unbounded_message = - parsed_message.attachments.contains(&part_id) && part_id == 0; - match &part.body { - PartType::Html(_) | PartType::Text(_) if probably_unbounded_message => { - // is this a comment in mime? - continue; + PartType::Binary(binary_content) => { + Self::handle_binary(part, &mut attachments, binary_content.to_vec(), false); }, - PartType::Binary(binary_content) | PartType::InlineBinary(binary_content) => { - let is_inline = if matches!(part.body, PartType::InlineBinary(_)) { - true - } else if matches!(part.body, PartType::Binary(_)) { - false - } else { - unreachable!(); - }; - Self::handle_binary( - &mut attachments, - &part.headers, - binary_content.to_vec(), - is_inline, - ); + PartType::InlineBinary(binary_content) => { + Self::handle_binary(part, &mut attachments, binary_content.to_vec(), true); }, // todo: of it is PartType::Text & PartType::Html, check for ConentDisposition Header // and if it is attachment, treat it as attachment PartType::Text(text) => { - let is_text_plain = part - .content_type() - .map(|content_type| { - let subtype = content_type.subtype().unwrap_or({ - // what do we do with the content-type: text - // with no subtype - // for now assume plain - if content_type.c_type == "text" { - "plain" - } else { - "" - } + let has_attachment_content_disposition = part + .content_disposition() + .map(|content_disposition| content_disposition.c_type == "attachment") + .unwrap_or_default(); + + let is_text_plain = !has_attachment_content_disposition + && part + .content_type() + .map(|content_type| { + let subtype = content_type.subtype().unwrap_or({ + // what do we do with the content-type: text + // with no subtype + // for now assume plain + if content_type.c_type == "text" { + "plain" + } else { + "" + } + }); + + let is_text_plain = + content_type.c_type == "text" && subtype == "plain"; + // edu: https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html + // subtype of the multipart Content-Type. + // This type is syntactically identical to multipart/mixed, but the + // semantics are different. In particular, in a digest, the default + // Content-Type value for a body part is changed from "text/plain" to "message/rfc822". + let is_message_rfc822 = + content_type.c_type == "message" && subtype == "rfc833"; + + is_text_plain || is_message_rfc822 + }) + .unwrap_or({ + // what should we treat text that is not content-Type: text? + // fow now let's assume it's content-type: text/plain + true }); - let is_text_plain = content_type.c_type == "text" && subtype == "plain"; - // edu: https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html - // subtype of the multipart Content-Type. - // This type is syntactically identical to multipart/mixed, but the - // semantics are different. In particular, in a digest, the default - // Content-Type value for a body part is changed from "text/plain" to "message/rfc822". - let is_message_rfc822 = - content_type.c_type == "message" && subtype == "rfc833"; - - is_text_plain || is_message_rfc822 - }) - .unwrap_or({ - // what should we treat text that is not content-Type: text? - // fow now let's assume it's content-type: text/plain - true - }); - - if is_text_plain { + if is_text_plain && !has_attachment_content_disposition { Self::handle_plain_text(&mut email_body_as_html, text.as_ref()); } else { Self::handle_binary( + part, &mut attachments, - &part.headers, text.as_bytes().to_vec(), false, ); @@ -279,7 +253,7 @@ impl ImportableMail { }, PartType::Message(attached_message) => { - Self::handle_attached_message(&mut attachments, attached_message.to_owned())?; + let ignored_result = Self::handle_message(&mut attachments, attached_message); }, PartType::Multipart(multi_part_ids) => { @@ -296,6 +270,53 @@ impl ImportableMail { Ok((email_body_as_html, attachments)) } + fn get_filename(part: &MessagePart, fallback_name: &str) -> String { + let content_disposition_filename = part + .content_disposition() + .map(|c| c.attribute("filename").map(ToString::to_string)) + .flatten(); + let content_type_filename = part + .content_type() + .map(|c| c.attribute("name").map(ToString::to_string)) + .flatten(); + + let file_name = content_disposition_filename.unwrap_or_else(|| { + content_type_filename.unwrap_or_else(|| { + let filename_suffix = part + .content_type() + .map(Self::get_suffix_from_content_type) + .unwrap_or_default(); + fallback_name.to_string() + filename_suffix + }) + }); + Self::escape_filename(&file_name).to_string() + } + + /// Creates a filename from the given filename that is valid on Linux and Windows. Invalid + /// characters are replaced by "_" + fn escape_filename(file_name: &str) -> Cow { + let regex = Regex::new("[\\\\/:*?<>\"|]").unwrap(); + regex.replace(file_name, "_") + } + + fn get_suffix_from_content_type(content_type: &ContentType) -> &'static str { + if content_type.c_type == "message" { + if content_type.subtype() == Some("rfc822") { + ".eml" + } else { + ".txt" + } + } else if content_type.c_type == "text" { + if content_type.subtype() == Some("calendar") { + ".ics" + } else { + ".txt" + } + } else { + "" + } + } + fn handle_multipart( parsed_message: &mail_parser::Message, multipart_ignored_alternative: &mut HashSet, @@ -391,53 +412,65 @@ impl ImportableMail { } fn handle_binary( + part: &MessagePart, attachments: &mut Vec, - header_values: &Vec>, - binary_content: Vec, + content: Vec, is_inline: bool, ) { - let content_type = header_values.header(HeaderName::ContentType).map(|c| { - c.value - .as_content_type() - .expect("Content type should be of type ContentType") - }); - let content_type_attributes = content_type - // get attributes_of_content_type if content-type is there - .and_then(mail_parser::ContentType::attributes) - // if can-not get attributes, default to empty list of attributes - .unwrap_or_default(); - let filename = content_type_attributes - .iter() - // find a attribute name filename - .filter(|(attribute_name, _)| attribute_name == "filename") - .map(|(_, file_name)| file_name.to_string()) - // first attribute called 'filename' - .next(); - - let content_id = header_values - .header_value(&HeaderName::ContentId) - .map(|content_type_header| { - content_type_header - .as_text() - .expect("Content-Id header should be of type text") - }) - .unwrap_or("binary") + let content_id = part.content_id().map(ToString::to_string); + let filename = Self::get_filename(part, "unknown"); + let content_type = part + .content_type() + .map(MakeString::make_string) + .map(Cow::into_owned) + .unwrap_or_else(|| Self::default_content_type().make_string().into_owned()) .to_string(); - let content_type = content_type + let content = content.to_vec(); + let attachment = ImportableMailAttachment { + filename, + content_type, + content_id, + is_inline, + content, + }; + + attachments.push(attachment); + } + + fn handle_message( + attachments: &mut Vec, + message: &Message, + ) -> Result<(), MailParseError> { + let filename = + Self::get_filename(&message.parts[0], &message.subject().unwrap_or("unknown")); + let content_type = message + .content_type() .map(MakeString::make_string) .unwrap_or_default() .to_string(); - let content = binary_content.to_vec(); - let this_attachment = ImportableMailAttachment::Attachment { + let nested_part = &message.parts[0]; + let content = + message.raw_message[nested_part.offset_header..nested_part.offset_end].to_vec(); + let attachment = ImportableMailAttachment { filename, content_type, - content_id, - is_inline, content, + is_inline: false, + content_id: None, + }; + attachments.push(attachment); + Ok(()) + } + + fn default_content_type() -> ContentType<'static> { + let default_content_type = ContentType { + c_type: Cow::Borrowed("text"), + c_subtype: Some(Cow::Borrowed("plain")), + attributes: Some(vec![(Cow::Borrowed("charset"), Cow::Borrowed("us-ascii"))]), }; - attachments.push(this_attachment); + default_content_type } } @@ -533,7 +566,7 @@ impl TryFrom for ImportableMail { .parse(rfc822_full.as_slice()) .ok_or(MailParseError::InvalidMimeMessage)?; - let mut importable_mail = Self::try_from(imap_mail)?; + let mut importable_mail = Self::try_from(&imap_mail).unwrap(); // example: // add more details from imap if given, @@ -559,10 +592,10 @@ pub enum MailParseError { } /// allow to convert from parsed message -impl<'x> TryFrom> for ImportableMail { +impl<'x> TryFrom<&mail_parser::Message<'x>> for ImportableMail { type Error = MailParseError; - fn try_from(parsed_message: mail_parser::Message) -> Result { + fn try_from(parsed_message: &mail_parser::Message) -> Result { let subject = parsed_message.subject().unwrap_or_default().to_string(); let (html_body_text, attachments) = ImportableMail::process_all_parts(&parsed_message)?; diff --git a/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs b/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs index e41707c9526..d785a24348e 100644 --- a/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs +++ b/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs @@ -1,13 +1,12 @@ //! Keep in sync with MimeStringToSmtpMessageConverterTest ! -use crate::importer::importable_mail::{ImportableMail, ImportableMailAttachment, MailContact}; +use crate::importer::importable_mail::{ImportableMail, MailContact}; +use mail_parser::decoders::base64::base64_decode; use mail_parser::{MessageParser, MimeHeaders}; use tutasdk::date::DateTime; fn parse_mail(msg: &str) -> ImportableMail { - MessageParser::default() - .parse(msg) - .unwrap() + (&MessageParser::default().parse(msg).unwrap()) .try_into() .unwrap() } @@ -113,7 +112,7 @@ Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\{Β³|@"##; let m = parse_mail(msg); assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("b", "b@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); assert_eq!("Hello", m.subject); assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); @@ -128,12 +127,12 @@ Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@"##; +Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\{Β³|@"##; let m = parse_mail(msg); assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("b", "b@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); assert_eq!("Hello", m.subject); assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); @@ -457,45 +456,20 @@ Hello Àâüß assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); assert_eq!(m.attachments.len(), 1); - for attachment in m.attachments { - match attachment { - ImportableMailAttachment::Attachment { .. } => { - // todo: - // should we have got the attachment here or in AttachedMessage? - } - ImportableMailAttachment::AttachedMessage { message: attached } => { - assert_eq!(attached.from_addresses, vec![("D", "d@tutanota.de").into()]); - assert_eq!(attached.to_addresses, vec![("E", "e@tutanota.de").into()]); - assert_eq!(attached.subject, "attached message"); - assert_eq!(attached.html_body_text, "Hello Àâüß"); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date) - } - } - } - -// assertEquals(new SmtpMailContact("A", "a@tutanota.de"), m.getSender()); - // assertEquals(List.of(new SmtpMailContact("B", "b@tutanota.de")), m.getToRecipients()); - // assertEquals("parent message", m.getSubject()); - // assertEquals("normal message", m.getPlainBodyText()); - // assertEquals(null, m.getHtmlBodyText()); - // assertEquals(date, m.getSentDate()); - // - // assertEquals(0, m.getAttachedMessages().size()); - // assertEquals(1, m.getAttachedFiles().size()); - // SmtpAttachment attachement = m.getAttachedFiles().get(0); - // SmtpMessage attached = mimeStringToSmtpMessageConverter.mimeToSmtpMessage(mimeStringToSmtpMessageConverter.dataToMimeMessage(attachement.getData()), - // null); - // - // assertEquals(new SmtpMailContact("D", "d@tutanota.de"), attached.getSender()); - // assertEquals(List.of(new SmtpMailContact("E", "e@tutanota.de")), attached.getToRecipients()); - // assertEquals("attached message", attached.getSubject()); - // assertEquals("Hello Àâüß", attached.getPlainBodyText()); - // assertEquals(null, attached.getHtmlBodyText()); - // assertEquals(yesterday, attached.getSentDate()); + let attachment = m.attachments.first().unwrap(); + let attached = parse_mail( + String::from_utf8(attachment.content.to_vec()) + .unwrap() + .as_str(), + ); + assert_eq!(attached.from_addresses, vec![("D", "d@tutanota.de").into()]); + assert_eq!(attached.to_addresses, vec![("E", "e@tutanota.de").into()]); + assert_eq!(attached.subject, "attached message"); + assert_eq!(attached.html_body_text, "
Hello Àâüß
"); } #[test] -fn attachments() { +fn multiple_attachments() { let msg = r#"Subject: multiple attachments From: A To: B @@ -531,38 +505,31 @@ c2Vjb25kIGF0dGFjaG1lbnQ= assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); assert_eq!(m.subject, "multiple attachments"); + assert_eq!("Hello Àâüß", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); assert_eq!(m.attachments.len(), 3); - todo!() + let [a1, a2, a3] = m.attachments.try_into().unwrap(); -// assertEquals("a@tutanota.de", m.getSender().getMailAddress()); - // assertEquals("A", m.getSender().getName()); - // assertEquals(1, m.getToRecipients().size()); - // assertEquals("b@tutanota.de", m.getToRecipients().get(0).getMailAddress()); - // assertEquals("B", m.getToRecipients().get(0).getName()); - // assertEquals("multiple attachments", m.getSubject()); - // assertEquals("Hello Àâüß", m.getPlainBodyText()); - // assertEquals(null, m.getHtmlBodyText()); - // assertEquals(date, m.getSentDate()); - // - // assertEquals(3, m.getAttachedFiles().size()); - // SmtpAttachment a1 = m.getAttachedFiles().get(0); - // SmtpAttachment a2 = m.getAttachedFiles().get(1); - // SmtpAttachment a3 = m.getAttachedFiles().get(2); - // - // assertEquals("a1.txt", a1.getName()); - // assertArrayEquals(EncodingConverter.base64ToBytes("Zmlyc3QgYXR0YWNobWVudA=="), a1.getData()); - // assertEquals("application/octet-stream", a1.getMimeType()); - // assertNull(a1.getCharset()); - // - // assertEquals("a2.pdf", a2.getName()); - // assertArrayEquals(EncodingConverter.base64ToBytes("c2Vjb25kIGF0dGFjaG1lbnQ="), a2.getData()); - // assertEquals("application/pdf", a2.getMimeType()); - // assertNull(a1.getCharset()); - // - // assertEquals("withoutContentType.pdf", a3.getName()); - // assertArrayEquals(EncodingConverter.base64ToBytes("c2Vjb25kIGF0dGFjaG1lbnQ="), a3.getData()); - // assertEquals("text/plain", a3.getMimeType()); - // assertNull(a1.getCharset()); + assert_eq!("a1.txt", a1.filename); + assert_eq!( + String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), + String::from_utf8(a1.content.to_vec()).unwrap() + ); + assert_eq!("application/octet-stream", a1.content_type); + + assert_eq!("a2.pdf", a2.filename); + assert_eq!( + String::from_utf8(base64_decode(b"c2Vjb25kIGF0dGFjaG1lbnQ=").unwrap()).unwrap(), + String::from_utf8(a2.content.to_vec()).unwrap() + ); + assert_eq!("application/pdf", a2.content_type); + + assert_eq!("withoutContentType.pdf", a3.filename); + assert_eq!( + String::from_utf8(base64_decode(b"c2Vjb25kIGF0dGFjaG1lbnQ=").unwrap()).unwrap(), + String::from_utf8(a3.content.to_vec()).unwrap() + ); + assert_eq!(r#"text/plain; charset="us-ascii""#, a3.content_type); } #[test] @@ -594,7 +561,7 @@ Zmlyc3QgYXR0YWNobWVudA== todo!() -// assertEquals(1, m.getAttachedFiles().size()); + // assertEquals(1, m.getAttachedFiles().size()); // SmtpAttachment a1 = m.getAttachedFiles().get(0); // // assertEquals("a1.png", a1.getName()); @@ -610,7 +577,7 @@ fn attachment_to_attached_message() { From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 -Content-Type: message/rfc822; charset=UTF-8; +Content-Type: message/rfc822; charset=UTF-8 Subject: attached message From: D @@ -627,28 +594,26 @@ Zmlyc3QgYXR0YWNobWVudA== assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - todo!() + assert_eq!(1, m.attachments.len()); -// assertEquals(null, m.getPlainBodyText()); - // assertEquals(null, m.getHtmlBodyText()); - // - // // messages are handled as attached files. - // assertEquals(0, m.getAttachedMessages().size()); - // assertEquals(1, m.getAttachedFiles().size()); - // - // SmtpAttachment attachedFile = m.getAttachedFiles().get(0); - // SmtpMessage attached = mimeStringToSmtpMessageConverter.mimeToSmtpMessage(mimeStringToSmtpMessageConverter.dataToMimeMessage(attachedFile.getData()), - // null); - // - // assertEquals("attached message", attached.getSubject()); - // assertEquals(null, attached.getHtmlBodyText()); - // - // assertEquals(0, attached.getAttachedMessages().size()); - // assertEquals(1, attached.getAttachedFiles().size()); - // - // SmtpAttachment indirectAttachment = attached.getAttachedFiles().get(0); - // assertEquals("indirectly_attached.txt", indirectAttachment.getName()); - // assertArrayEquals(EncodingConverter.base64ToBytes("Zmlyc3QgYXR0YWNobWVudA=="), indirectAttachment.getData()); + let attached = parse_mail( + String::from_utf8(m.attachments.first().unwrap().content.clone()) + .unwrap() + .as_str(), + ); + + assert_eq!(attached.subject, "attached message"); + + assert_eq!("", attached.html_body_text); + assert_eq!(1, attached.attachments.len()); + + assert_eq!(1, attached.attachments.len()); + let indirect_attachment = attached.attachments.first().unwrap(); + assert_eq!("indirectly_attached.txt", indirect_attachment.filename); + assert_eq!( + String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), + String::from_utf8(indirect_attachment.content.to_vec()).unwrap() + ); } #[test] @@ -677,7 +642,7 @@ Abc, die Katze liegt im Schnee ! Àâü?ß ! assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); todo!() -// assertEquals("text attachment", m.getSubject()); + // assertEquals("text attachment", m.getSubject()); // assertEquals("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.getPlainBodyText()); // assertEquals(null, m.getHtmlBodyText()); // assertEquals(date, m.getSentDate()); @@ -715,7 +680,7 @@ Content-Disposition: attachment; filename=a1.html; assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); todo!() -// assertEquals("html attachment", m.getSubject()); + // assertEquals("html attachment", m.getSubject()); // assertEquals("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.getPlainBodyText()); // assertEquals(null, m.getHtmlBodyText()); // assertEquals(date, m.getSentDate()); @@ -761,7 +726,7 @@ second plain text in body text_contents ); -// String firstPlainBodyText = "Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@"; + // String firstPlainBodyText = "Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@"; // String secondPlainBodyText = "Abc, die Katze liegt im Schnee ! Àâü?ß !"; // StringBuilder b = new StringBuilder() // .append("Subject: multiple text/plain parts concatenated\n") @@ -842,7 +807,7 @@ Content-Type: text/html; charset=UTF-8 --line-- "#; - let _m = parse_mail(msg); + let m = parse_mail(msg); todo!() } @@ -869,7 +834,7 @@ first plain text in body --line-- "#; - let _m = parse_mail(msg); + let m = parse_mail(msg); todo!() } @@ -904,7 +869,7 @@ Abc, die Katze liegt im Schnee ! Àâü?ß ! ); todo!() -// assertEquals("multiple text/html and text/plain parts concatenated to single text/html", m.getSubject()); + // assertEquals("multiple text/html and text/plain parts concatenated to single text/html", m.getSubject()); // String concatenatedHtmlBodyText = firstHtmlBodyText.concat(secondHtmlBodyText).concat(PlainTextHtmlConverter.plainTextToHtml(firstPlainBodyText)); // assertEquals(null, m.getPlainBodyText()); // assertEquals(concatenatedHtmlBodyText, m.getHtmlBodyText()); @@ -922,9 +887,9 @@ fn plain_body_text_parts_are_converted_to_html_body_parts_if_html_body_parts_fol assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - todo!() + todo!() -// assertEquals("multiple plain/text and text/html parts concatenated to single text/html", m.getSubject()); + // assertEquals("multiple plain/text and text/html parts concatenated to single text/html", m.getSubject()); // String concatenatedHtmlBodyText = PlainTextHtmlConverter.plainTextToHtml(firstPlainBodyText).concat(firstHtmlBodyText).concat(secondHtmlBodyText); // assertEquals(null, m.getPlainBodyText()); // assertEquals(concatenatedHtmlBodyText, m.getHtmlBodyText()); @@ -937,7 +902,7 @@ fn text_attachment_with_disposition() { let msg = r#"Subject: text attachment From: A To: B -Date: " + new MailDateFormat().format(date) + " +Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/plain; charset=UTF-8 Content-Disposition: attachment; filename=a1.txt; @@ -948,9 +913,9 @@ Abc, die Katze liegt im Schnee ! Àâü?ß ! assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - todo!() + todo!() -// assertEquals("text attachment", m.getSubject()); + // assertEquals("text attachment", m.getSubject()); // assertNull(m.getPlainBodyText()); // assertNull(m.getHtmlBodyText()); // assertEquals(date, m.getSentDate()); @@ -964,10 +929,10 @@ Abc, die Katze liegt im Schnee ! Àâü?ß ! #[test] fn attachment_with_non_ascii_name() { - let msg = r#"Subject: text attachment + let msg = r#"Subject: text attachment From: A To: B -Date: " + new MailDateFormat().format(date) + " +Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/plain; charset=UTF-8; name=\"=?ISO-8859-1?Q?a=F6i=2Epdf?=\" Content-Disposition: attachment; filename*=ISO-8859-1''%61%F6%69%2E%70%64%66 @@ -977,15 +942,13 @@ Abc, die Katze liegt im Schnee ! Àâü?ß ! "#; assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - todo!() - -// SmtpAttachment a1 = m.getAttachedFiles().get(0); - // assertEquals("aΓΆi.pdf", a1.getName()); + assert_eq!(1, m.attachments.len()); + assert_eq!("aΓΆi.pdf", m.attachments.first().unwrap().filename); } #[test] fn attachment_filename_in_content_type() { - let msg = r#"Subject: message with named file attachment + let msg = r#"Subject: message with named file attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -998,18 +961,22 @@ Zmlyc3QgYXR0YWNobWVudA=="#; assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - todo!() -// SmtpAttachment indirectAttachment = m.getAttachedFiles().get(0); - // assertEquals("indirectly_attached.txt", indirectAttachment.getName()); - // assertArrayEquals(EncodingConverter.base64ToBytes("Zmlyc3QgYXR0YWNobWVudA=="), indirectAttachment.getData()); + + assert_eq!(1, m.attachments.len()); + let indirect_attachment = m.attachments.first().unwrap(); + assert_eq!("indirectly_attached.txt", indirect_attachment.filename); + assert_eq!( + String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), + String::from_utf8(indirect_attachment.content.to_vec()).unwrap() + ); } #[test] fn attachment_filename_qencoding() { - let msg = r#"Subject: message with named file attachment + let msg = r#"Subject: message with named file attachment From: A To: B -Date: " + new MailDateFormat().format(date) + " +Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: application/octet-stream; name==?utf-8?Q?=C3=A4=C3=B6=C3=9F=E2=82=AC.txt?=; Content-Transfer-Encoding: base64 @@ -1019,16 +986,19 @@ Zmlyc3QgYXR0YWNobWVudA=="#; assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - todo!() -// SmtpAttachment indirectAttachment = m.getAttachedFiles().get(0); - // assertEquals("Γ€ΓΆΓŸβ‚¬.txt", indirectAttachment.getName()); - // assertArrayEquals(EncodingConverter.base64ToBytes("Zmlyc3QgYXR0YWNobWVudA=="), indirectAttachment.getData()); + assert_eq!(1, m.attachments.len()); + let indirect_attachment = m.attachments.first().unwrap(); + assert_eq!("Γ€ΓΆΓŸβ‚¬.txt", indirect_attachment.filename); + assert_eq!( + String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), + String::from_utf8(indirect_attachment.content.to_vec()).unwrap() + ); } #[test] fn encrypted() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -1045,41 +1015,43 @@ SGFsbG8= assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - todo!() + assert_eq!(1, m.attachments.len()); -// assertEquals(1, m.getAttachedFiles().size()); + // assertEquals(1, m.getAttachedFiles().size()); } #[test] fn recipient_groups() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: foo:a@b.example.de,c@d.example.de,e@f.example.de; Reply-To: ??? Date: Thu, 7 Nov 2024 15:54:04 +0100"#; - let m: ImportableMail = parse_mail(msg); + let m = parse_mail(msg); assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!( + m.to_addresses, + vec![ + ("", "a@b.example.de").into(), + ("", "c@d.example.de").into(), + ("", "e@f.example.de").into() + ] + ); assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - todo!() - -// assertEquals("a@b.example.de", m.getToRecipients().get(0).getMailAddress()); - // assertEquals("c@d.example.de", m.getToRecipients().get(1).getMailAddress()); - // assertEquals("e@f.example.de", m.getToRecipients().get(2).getMailAddress()); } #[test] fn undisclosed_recipients() { - let msg = r#"To: undisclosed-recipients:;"#; - let m: ImportableMail = parse_mail(msg); + let msg = r#"To: undisclosed-recipients:;"#; + let m = parse_mail(msg); - assert!(m.to_addresses.is_empty()); + assert_eq!(0, m.to_addresses.len()); } #[test] fn long_content_type() { - let msg = r#"From: A + let msg = r#"From: A Content-type: multipart/mixed; boundary=frontier --frontier @@ -1089,18 +1061,19 @@ Content-Disposition: attachment; filename=withoutContentType.pdf; Message --frontier-- "#; - let _m: ImportableMail = parse_mail(msg); + let m = parse_mail(msg); - todo!() -// assertEquals("text/plain", m.getAttachedFiles().get(0).getMimeType()); - // assertEquals("us-ascii", m.getAttachedFiles().get(0).getCharset()); - // assertEquals("withoutContentType.pdf", m.getAttachedFiles().get(0).getName()); + let attachment = m.attachments.first().unwrap(); + assert_eq!("withoutContentType.pdf", attachment.filename); + assert_eq!("text/plain", attachment.content_type); + todo!() + // assertEquals("us-ascii", m.getAttachedFiles().get(0).getCharset()); } #[test] fn normalize_header_value() { todo!() -// // trim and remove LF and CR + // // trim and remove LF and CR // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" ").toArray(new String[0])); // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" ").toArray(new String[0])); // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" \r \n ").toArray(new String[0])); @@ -1133,8 +1106,7 @@ fn get_spf_result() { #[test] fn mail_from_with_delemiter() { - let msg = r#" -Message-ID: 123456 + let msg = r#"Message-ID: 123456 Subject: Hello From: A,B To: B @@ -1144,37 +1116,31 @@ Content-Type: multipart/mixed; boundary=frontier --frontier "#; - let _m: ImportableMail = parse_mail(msg); + let m = parse_mail(msg); - todo!() -// assertEquals("A, B ", m.getSender().getMailAddress()); + todo!() + // assertEquals("A, B ", m.getSender().getMailAddress()); // assertEquals("", m.getSender().getName()); // assertFalse(m.getSender().isValid()); } #[test] fn incomplete_text_content_type() { - let msg = r#" -Subject: Hello + let msg = r#"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text -any body text - - ---frontier -"#; - let _m: ImportableMail = parse_mail(msg); +any body text"#; + let m = parse_mail(msg); - todo!() -// assertEquals("any body text", m.getPlainBodyText()); + assert_eq!("any body text", m.html_body_text); } #[test] fn calendar_content_type() { - let msg = r#"Message-ID: 123456 + let msg = r#"Message-ID: 123456 Subject: Hello From: A To: B @@ -1182,13 +1148,11 @@ References: <1234564@web.de> Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-Type: text/calendar; charset=\"UTF-8\"; method=REQUEST "#; - let _m: ImportableMail = parse_mail(msg); + let m = parse_mail(msg); - todo!() -// assertEquals("a@tutanota.de", m.getSender().getMailAddress()); - // assertEquals("A", m.getSender().getName()); - // assertEquals("text/calendar", m.getAttachedFiles().get(0).getMimeType()); - // assertEquals("REQUEST", m.getAttachedFiles().get(0).getCalendarMethod()); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!("text/calendar", m.attachments.first().unwrap().content_type); + // assertEquals("REQUEST", m.getAttachedFiles().get(0).getCalendarMethod()); } #[test] @@ -1204,13 +1168,9 @@ Content-Type: text/calendar; charset="UTF-8"; method=request; let m: ImportableMail = parse_mail(msg); assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - match m.attachments.first().unwrap() { - ImportableMailAttachment::Attachment { content_type, .. } => { - assert_eq!("text/calendar", content_type); - // todo! assert_eq!("REQUEST", calendar_method) - } - ImportableMailAttachment::AttachedMessage { .. } => { panic!("") } - }; + let attachment = m.attachments.first().unwrap(); + assert_eq!("text/calendar", attachment.content_type); + // todo! assert_eq!("REQUEST", calendar_method) } #[test] @@ -1221,12 +1181,15 @@ fn invalid_content_types_default_to_text_plain() { "Content-Type: text", "Content-Type; text/html", "Content-Type; invalid/type", - "Content-Type: application/pdf; no_parameter_name.pdf" + "Content-Type: application/pdf; no_parameter_name.pdf", ]; for invalid_content_type in invalid_content_types { let parsed = MessageParser::default() .parse(invalid_content_type) .unwrap(); - assert_eq!("text/plain", parsed.content_type().unwrap().c_type.to_string()); + assert_eq!( + "text/plain", + parsed.content_type().unwrap().c_type.to_string() + ); } } diff --git a/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs b/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs index 1d68c08acd2..8d0417e7863 100644 --- a/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs +++ b/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs @@ -1,6 +1,6 @@ //! keep in sync with MimeToolsTestMessages.java -use crate::importer::importable_mail::{ImportableMail, MailContact}; +use crate::importer::importable_mail::{ImportableMail, ImportableMailAttachment, MailContact}; use serde::Deserialize; use std::borrow::Cow; use std::io::Read; @@ -66,7 +66,7 @@ fn mime_tools_test_messages() { result: expected_result, exception: expected_exception, } = FileContent::read_from_file(expected_json_file_name.as_str()).unwrap(); - let parsed_message_result = ImportableMail::try_from(parsed_message.clone()); + let parsed_message_result = ImportableMail::try_from(&parsed_message); if expected_result.is_some() && expected_exception.is_none() { let mut importable_mail = parsed_message_result.unwrap(); @@ -205,7 +205,7 @@ impl From for ImportableMail { raw_message: Cow::Owned(headers_string_clone.as_bytes().to_vec()), }; - ImportableMail::try_from(parsed_mail).unwrap() + ImportableMail::try_from(&parsed_mail).unwrap() } } From 0f74d046db59fab1df88c29ebfd0c508ffb22c45 Mon Sep 17 00:00:00 2001 From: nig Date: Wed, 13 Nov 2024 11:45:40 +0100 Subject: [PATCH 15/32] fmt --- packages/node-mimimi/src/importer.rs | 212 +-- .../src/importer/imap_reader/import_client.rs | 262 ++-- .../src/importer/importable_mail.rs | 3 +- .../importable_mail/extend_mail_parser.rs | 8 +- .../mime_string_to_importable_mail_test.rs | 1072 +++++++------- .../plain_text_to_html_converter.rs | 229 ++- packages/node-mimimi/src/lib.rs | 2 +- packages/node-mimimi/src/reduce_to_chunks.rs | 163 +- .../src/tuta_imap/client/tls_stream.rs | 154 +- .../rust/sdk/src/services/service_executor.rs | 1318 ++++++++--------- 10 files changed, 1725 insertions(+), 1698 deletions(-) diff --git a/packages/node-mimimi/src/importer.rs b/packages/node-mimimi/src/importer.rs index 7af673344ec..54f78c7753c 100644 --- a/packages/node-mimimi/src/importer.rs +++ b/packages/node-mimimi/src/importer.rs @@ -85,61 +85,61 @@ pub enum IterationError { } struct ImportSourceIterator { - // it would be nice to not need the mutex, but when the importer continues the import, + // it would be nice to not need the mutex, but when the importer continues the import, // it mutates its own state and also calls mutating functions on the source. solving this - // probably requires a bigger restructure of the code (it's very OOP atm) - source: Arc>, + // probably requires a bigger restructure of the code (it's very OOP atm) + source: Arc>, } impl Iterator for ImportSourceIterator { - type Item = ImportableMail; - - fn next(&mut self) -> Option { - let mut source = self.source.lock().unwrap(); - let next_importable_mail = match &mut *source { - // the other way (converting fs_source to an async_iterator) would be nicer, but that's a nightly feature - ImportSource::RemoteImap { imap_import_client } => imap_import_client - .fetch_next_mail() - .map_err(IterationError::Imap), - ImportSource::LocalFile { fs_email_client } => fs_email_client - .get_next_importable_mail() - .map_err(IterationError::File), - }; - - match next_importable_mail { - Ok(next_importable_mail) => Some(next_importable_mail), - - // source says, all the iteration have ended, - Err(IterationError::File(FileIterationError::SourceEnd)) - | Err(IterationError::Imap(ImapIterationError::SourceEnd)) => { - None - }, - - Err(e) => { - // once we handle this case we will need another iterator that filters (and logs) the - // errors so we don't have to handle the error case during the chunking + upload - panic!("Cannot get next email from source: {e:?}") - } - } - } + type Item = ImportableMail; + + fn next(&mut self) -> Option { + let mut source = self.source.lock().unwrap(); + let next_importable_mail = match &mut *source { + // the other way (converting fs_source to an async_iterator) would be nicer, but that's a nightly feature + ImportSource::RemoteImap { imap_import_client } => imap_import_client + .fetch_next_mail() + .map_err(IterationError::Imap), + ImportSource::LocalFile { fs_email_client } => fs_email_client + .get_next_importable_mail() + .map_err(IterationError::File), + }; + + match next_importable_mail { + Ok(next_importable_mail) => Some(next_importable_mail), + + // source says, all the iteration have ended, + Err(IterationError::File(FileIterationError::SourceEnd)) + | Err(IterationError::Imap(ImapIterationError::SourceEnd)) => None, + + Err(e) => { + // once we handle this case we will need another iterator that filters (and logs) the + // errors so we don't have to handle the error case during the chunking + upload + panic!("Cannot get next email from source: {e:?}") + }, + } + } } impl Importer { - pub async fn continue_import(&mut self) -> Result { - let source_iterator = ImportSourceIterator { source: Arc::clone(&self.import_source) }; - let _ = self.import_all_mail(source_iterator).await; - Ok(self.status.clone()) - } + pub async fn continue_import(&mut self) -> Result { + let source_iterator = ImportSourceIterator { + source: Arc::clone(&self.import_source), + }; + let _ = self.import_all_mail(source_iterator).await; + Ok(self.status.clone()) + } /// once we get the ImportableMail from either of source, /// continue to the uploading counterpart async fn import_all_mail( &mut self, - importable_mails: Iter, - ) -> Result, ()> - where - Iter: Iterator, - { + importable_mails: Iter, + ) -> Result, ()> + where + Iter: Iterator, + { let new_aes_256_key = GenericAesKey::from_bytes( self.randomizer_facade .generate_random_array::<{ tutasdk::crypto::aes::AES_256_KEY_SIZE }>() @@ -155,65 +155,73 @@ impl Importer { mail_group_key.encrypt_key(&new_aes_256_key, Iv::generate(&self.randomizer_facade)); const MAX_REQUEST_SIZE: usize = 1024 * 1024 * 10; - let import_chunks: Vec> = reduce_to_chunks( - importable_mails.map(ImportMailData::from), - MAX_REQUEST_SIZE, - Box::new(estimate_json_size), - ).collect(); - - let mut mails: Vec = Vec::new(); - let mut new_status = ImportStatus { - state: ImportState::Running, - imported_mails: 0, - }; - for imports in import_chunks { - let import_len = imports.len(); - let import_mail_post_in = ImportMailPostIn { - ownerEncSessionKey: owner_enc_session_key.object.clone(), - ownerGroup: self.target_owner_group.clone(), - ownerKeyVersion: owner_enc_session_key.version, - imports, - targetMailFolder: self.target_mail_folder.clone(), - _format: 0, - _errors: None, - _finalIvs: Default::default(), - }; - - let service_params = ExtraServiceParams { - session_key: Some(new_aes_256_key.clone()), - ..Default::default() - }; - - let response = self - .logged_in_sdk - .get_service_executor() - .post::(import_mail_post_in, service_params) - .await; - - match response { - // this import has been success, - Ok(mut imported_post_out) => { - mails.append(&mut imported_post_out.mails); - new_status = ImportStatus { - state: ImportState::Running, - imported_mails: self.status.imported_mails.saturating_add(u32::try_from(import_len).unwrap_or(u32::MAX)), - }; - } - - Err(_) => { - // todo: save the ImportableMails to some fail list, - // since, in this iteration the source will not give these mail again, - new_status = ImportStatus { - state: ImportState::Postponed, - imported_mails: self.status.imported_mails, - }; - } - } - } - new_status.state = if new_status.state == ImportState::Postponed { ImportState::Postponed } else { ImportState::Finished }; - - self.status = new_status; - Ok(mails) + let import_chunks: Vec> = reduce_to_chunks( + importable_mails.map(ImportMailData::from), + MAX_REQUEST_SIZE, + Box::new(estimate_json_size), + ) + .collect(); + + let mut mails: Vec = Vec::new(); + let mut new_status = ImportStatus { + state: ImportState::Running, + imported_mails: 0, + }; + for imports in import_chunks { + let import_len = imports.len(); + let import_mail_post_in = ImportMailPostIn { + ownerEncSessionKey: owner_enc_session_key.object.clone(), + ownerGroup: self.target_owner_group.clone(), + ownerKeyVersion: owner_enc_session_key.version, + imports, + targetMailFolder: self.target_mail_folder.clone(), + _format: 0, + _errors: None, + _finalIvs: Default::default(), + }; + + let service_params = ExtraServiceParams { + session_key: Some(new_aes_256_key.clone()), + ..Default::default() + }; + + let response = self + .logged_in_sdk + .get_service_executor() + .post::(import_mail_post_in, service_params) + .await; + + match response { + // this import has been success, + Ok(mut imported_post_out) => { + mails.append(&mut imported_post_out.mails); + new_status = ImportStatus { + state: ImportState::Running, + imported_mails: self + .status + .imported_mails + .saturating_add(u32::try_from(import_len).unwrap_or(u32::MAX)), + }; + }, + + Err(_) => { + // todo: save the ImportableMails to some fail list, + // since, in this iteration the source will not give these mail again, + new_status = ImportStatus { + state: ImportState::Postponed, + imported_mails: self.status.imported_mails, + }; + }, + } + } + new_status.state = if new_status.state == ImportState::Postponed { + ImportState::Postponed + } else { + ImportState::Finished + }; + + self.status = new_status; + Ok(mails) } } diff --git a/packages/node-mimimi/src/importer/imap_reader/import_client.rs b/packages/node-mimimi/src/importer/imap_reader/import_client.rs index 5ca9f71ebe1..9d21a2a86b5 100644 --- a/packages/node-mimimi/src/importer/imap_reader/import_client.rs +++ b/packages/node-mimimi/src/importer/imap_reader/import_client.rs @@ -6,153 +6,153 @@ use imap_codec::imap_types::response::StatusKind; use std::num::NonZeroU32; pub struct ImapImport { - import_config: ImapImportConfig, - imap_client: TutaImapClient, + import_config: ImapImportConfig, + imap_client: TutaImapClient, - import_state: ImapImportState, + import_state: ImapImportState, } pub struct ImapImportState { - done_fetched_mailbox: Vec>, - current_mailbox: Option>, - next_target_mailbox: Vec>, + done_fetched_mailbox: Vec>, + current_mailbox: Option>, + next_target_mailbox: Vec>, - /// List of mail id that have been fetched from current_mailbox, - fetched_from_current_mailbox: Vec, + /// List of mail id that have been fetched from current_mailbox, + fetched_from_current_mailbox: Vec, } impl ImapImportState { - /// add currently selected mailbox to done, - /// and pop one from next target mailbox - pub fn finish_current_mailbox(&mut self) { - let current_mailbox = self.current_mailbox.as_mut().expect("No current mailbox"); - self.done_fetched_mailbox.push(current_mailbox.clone()); - self.current_mailbox = self.next_target_mailbox.pop(); - self.fetched_from_current_mailbox.clear(); - } - - /// this id of current mailbox was fetched - pub fn fetched_from_current_mailbox(&mut self, id: NonZeroU32) { - assert!(self.current_mailbox.is_some(), "No current mailbox"); - self.fetched_from_current_mailbox.push(id); - } + /// add currently selected mailbox to done, + /// and pop one from next target mailbox + pub fn finish_current_mailbox(&mut self) { + let current_mailbox = self.current_mailbox.as_mut().expect("No current mailbox"); + self.done_fetched_mailbox.push(current_mailbox.clone()); + self.current_mailbox = self.next_target_mailbox.pop(); + self.fetched_from_current_mailbox.clear(); + } + + /// this id of current mailbox was fetched + pub fn fetched_from_current_mailbox(&mut self, id: NonZeroU32) { + assert!(self.current_mailbox.is_some(), "No current mailbox"); + self.fetched_from_current_mailbox.push(id); + } } #[derive(Debug, PartialEq, Clone)] pub enum ImapIterationError { - /// All mail form remote server have been visited at least once, - SourceEnd, + /// All mail form remote server have been visited at least once, + SourceEnd, - /// when executing a command, received a non-ok status, - NonOkCommandStatus, + /// when executing a command, received a non-ok status, + NonOkCommandStatus, - /// Can not convert ImapMail to ConvertableMail - MailParseError(MailParseError), + /// Can not convert ImapMail to ConvertableMail + MailParseError(MailParseError), - /// Can not login to imap server - NoLogin, + /// Can not login to imap server + NoLogin, } impl ImapImport { - pub fn new(import_config: ImapImportConfig) -> Self { - let imap_client = TutaImapClient::new( - import_config.credentials.host.as_str(), - import_config.credentials.port, - ); - - Self { - imap_client, - import_config, - - import_state: ImapImportState { - done_fetched_mailbox: vec![], - next_target_mailbox: vec![Mailbox::Inbox], - current_mailbox: None, - fetched_from_current_mailbox: vec![], - }, - } - } - - /// High level abstraction to read next mail from imap, - /// will switch to next mailbox, if everything from current mailbox is fetched, - pub fn fetch_next_mail(&mut self) -> Result { - self.ensure_logged_in()?; - - while self.imap_client.latest_search_results.is_empty() { - if self.import_state.current_mailbox.is_some() { - self.import_state.finish_current_mailbox(); - } - - // select next mailbox - // and search for all available mails - self.ensure_mailbox_selected()?; - - self.imap_client - .search_all_uid() - .eq(&StatusKind::Ok) - .then_some(()) - .ok_or(ImapIterationError::NonOkCommandStatus)?; - } - - // search for the last ( oldest ? ) mail - let next_mail_id = self.imap_client.latest_search_results.pop().unwrap(); - self.imap_client - .fetch_mail_by_uid(next_mail_id) - .eq(&StatusKind::Ok) - .then_some(()) - .ok_or(ImapIterationError::NonOkCommandStatus)?; - let next_mail_imap = self.imap_client.latest_mails.remove(&next_mail_id).unwrap(); - - // mark this id have been fetched - self.import_state.fetched_from_current_mailbox(next_mail_id); - - ImportableMail::try_from(next_mail_imap).map_err(ImapIterationError::MailParseError) - } - - fn ensure_mailbox_selected(&mut self) -> Result<(), ImapIterationError> { - if self.import_state.current_mailbox.is_none() { - let next_mailbox_to_select = self - .import_state - .next_target_mailbox - .pop() - .ok_or(ImapIterationError::SourceEnd)?; - self.import_state.current_mailbox = Some(next_mailbox_to_select); - self.import_state.fetched_from_current_mailbox = vec![]; - } - - // if something from current mailbox is selected, it means we are already in selected state - if !self.import_state.fetched_from_current_mailbox.is_empty() { - return Ok(()); - } - - let target_mailbox = self - .import_state - .current_mailbox - .as_ref() - .ok_or(ImapIterationError::SourceEnd)?; - self.imap_client - .select_mailbox(target_mailbox.clone()) - .eq(&StatusKind::Ok) - .then_some(()) - .ok_or(ImapIterationError::NonOkCommandStatus) - } - - fn ensure_logged_in(&mut self) -> Result<(), ImapIterationError> { - if self.imap_client.is_logged_in() { - return Ok(()); - } - - match &self.import_config.credentials.login_mechanism { - LoginMechanism::Plain { username, password } => self - .imap_client - .plain_login(username.as_str(), password.as_str()) - .eq(&StatusKind::Ok) - .then_some(()) - .ok_or(ImapIterationError::NoLogin), - - LoginMechanism::OAuth { access_token: _ } => { - unimplemented!() - } - } - } + pub fn new(import_config: ImapImportConfig) -> Self { + let imap_client = TutaImapClient::new( + import_config.credentials.host.as_str(), + import_config.credentials.port, + ); + + Self { + imap_client, + import_config, + + import_state: ImapImportState { + done_fetched_mailbox: vec![], + next_target_mailbox: vec![Mailbox::Inbox], + current_mailbox: None, + fetched_from_current_mailbox: vec![], + }, + } + } + + /// High level abstraction to read next mail from imap, + /// will switch to next mailbox, if everything from current mailbox is fetched, + pub fn fetch_next_mail(&mut self) -> Result { + self.ensure_logged_in()?; + + while self.imap_client.latest_search_results.is_empty() { + if self.import_state.current_mailbox.is_some() { + self.import_state.finish_current_mailbox(); + } + + // select next mailbox + // and search for all available mails + self.ensure_mailbox_selected()?; + + self.imap_client + .search_all_uid() + .eq(&StatusKind::Ok) + .then_some(()) + .ok_or(ImapIterationError::NonOkCommandStatus)?; + } + + // search for the last ( oldest ? ) mail + let next_mail_id = self.imap_client.latest_search_results.pop().unwrap(); + self.imap_client + .fetch_mail_by_uid(next_mail_id) + .eq(&StatusKind::Ok) + .then_some(()) + .ok_or(ImapIterationError::NonOkCommandStatus)?; + let next_mail_imap = self.imap_client.latest_mails.remove(&next_mail_id).unwrap(); + + // mark this id have been fetched + self.import_state.fetched_from_current_mailbox(next_mail_id); + + ImportableMail::try_from(next_mail_imap).map_err(ImapIterationError::MailParseError) + } + + fn ensure_mailbox_selected(&mut self) -> Result<(), ImapIterationError> { + if self.import_state.current_mailbox.is_none() { + let next_mailbox_to_select = self + .import_state + .next_target_mailbox + .pop() + .ok_or(ImapIterationError::SourceEnd)?; + self.import_state.current_mailbox = Some(next_mailbox_to_select); + self.import_state.fetched_from_current_mailbox = vec![]; + } + + // if something from current mailbox is selected, it means we are already in selected state + if !self.import_state.fetched_from_current_mailbox.is_empty() { + return Ok(()); + } + + let target_mailbox = self + .import_state + .current_mailbox + .as_ref() + .ok_or(ImapIterationError::SourceEnd)?; + self.imap_client + .select_mailbox(target_mailbox.clone()) + .eq(&StatusKind::Ok) + .then_some(()) + .ok_or(ImapIterationError::NonOkCommandStatus) + } + + fn ensure_logged_in(&mut self) -> Result<(), ImapIterationError> { + if self.imap_client.is_logged_in() { + return Ok(()); + } + + match &self.import_config.credentials.login_mechanism { + LoginMechanism::Plain { username, password } => self + .imap_client + .plain_login(username.as_str(), password.as_str()) + .eq(&StatusKind::Ok) + .then_some(()) + .ok_or(ImapIterationError::NoLogin), + + LoginMechanism::OAuth { access_token: _ } => { + unimplemented!() + }, + } + } } diff --git a/packages/node-mimimi/src/importer/importable_mail.rs b/packages/node-mimimi/src/importer/importable_mail.rs index 0cca75cac21..6ae1f4b7538 100644 --- a/packages/node-mimimi/src/importer/importable_mail.rs +++ b/packages/node-mimimi/src/importer/importable_mail.rs @@ -642,7 +642,8 @@ impl<'x> TryFrom<&mail_parser::Message<'x>> for ImportableMail { // different envelope sender should not contain address listed in from_addresses; .filter(|diff_sender| { from_addresses - .iter().any(|from| from.mail_address != diff_sender.mail_address) + .iter() + .any(|from| from.mail_address != diff_sender.mail_address) }) .map(|mail_address| mail_address.mail_address); diff --git a/packages/node-mimimi/src/importer/importable_mail/extend_mail_parser.rs b/packages/node-mimimi/src/importer/importable_mail/extend_mail_parser.rs index 45a1077da22..da02a0abbff 100644 --- a/packages/node-mimimi/src/importer/importable_mail/extend_mail_parser.rs +++ b/packages/node-mimimi/src/importer/importable_mail/extend_mail_parser.rs @@ -12,9 +12,11 @@ pub(super) fn get_reply_type_from_headers<'a>(headers: &'a [mail_parser::Header< if header.value().make_string().trim().is_empty() { is_forward = true; } - } else if header.name == HeaderName::References && header.value().make_string().trim().is_empty() { - is_reply = true; - } + } else if header.name == HeaderName::References + && header.value().make_string().trim().is_empty() + { + is_reply = true; + } if is_reply && is_forward { break; } diff --git a/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs b/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs index d785a24348e..1a20570741a 100644 --- a/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs +++ b/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs @@ -6,28 +6,28 @@ use mail_parser::{MessageParser, MimeHeaders}; use tutasdk::date::DateTime; fn parse_mail(msg: &str) -> ImportableMail { - (&MessageParser::default().parse(msg).unwrap()) - .try_into() - .unwrap() + (&MessageParser::default().parse(msg).unwrap()) + .try_into() + .unwrap() } // to be able to convert any (str/string, str/string).into() => MailContact impl From<(N, A)> for MailContact where - N: ToString, - A: ToString, + N: ToString, + A: ToString, { - fn from((name, address): (N, A)) -> Self { - Self { - mail_address: address.to_string(), - name: name.to_string(), - } - } + fn from((name, address): (N, A)) -> Self { + Self { + mail_address: address.to_string(), + name: name.to_string(), + } + } } #[test] fn headers() { - let msg = r#"Message-ID: 123456 + let msg = r#"Message-ID: 123456 Subject: Hello From: A To: B @@ -37,90 +37,90 @@ In-Reply-To: <1234564@web.de> Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-Type: multipart/mixed; boundary=frontier "#; - println!("{}", msg); - let m = parse_mail(msg); - assert_eq!("123456", m.message_id.unwrap()); - assert_eq!( + println!("{}", msg); + let m = parse_mail(msg); + assert_eq!("123456", m.message_id.unwrap()); + assert_eq!( m.reply_to_addresses, vec![ ("Reply", "reply@tutanota.de").into(), ("Reply2", "reply2@tutanota.de").into(), ], ); - assert_eq!( + assert_eq!( m.references, vec!["sadf@tutanota.de".to_string(), "1234564@web.de".to_string()], ); - assert_eq!(Some("1234564@web.de".to_string()), m.in_reply_to); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(msg, m.headers_string); + assert_eq!(Some("1234564@web.de".to_string()), m.in_reply_to); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(msg, m.headers_string); } #[test] fn bad_frontier() { - let msg = "Content-Type: multipart/mixed; boundary=komma;ist;nicht;erlaubt\n"; - let parsed_message = MessageParser::default().parse(msg).unwrap(); - let attributes = parsed_message - .content_type() - .unwrap() - .attributes - .as_ref() - .unwrap(); - assert_eq!(attributes.as_slice(), [("boundary".into(), "komma".into())]); + let msg = "Content-Type: multipart/mixed; boundary=komma;ist;nicht;erlaubt\n"; + let parsed_message = MessageParser::default().parse(msg).unwrap(); + let attributes = parsed_message + .content_type() + .unwrap() + .attributes + .as_ref() + .unwrap(); + assert_eq!(attributes.as_slice(), [("boundary".into(), "komma".into())]); } #[test] fn empty_references() { - let msg = "Subject: Hello"; - let m = parse_mail(msg); - assert!(m.references.is_empty()); + let msg = "Subject: Hello"; + let m = parse_mail(msg); + assert!(m.references.is_empty()); } #[test] fn empty_in_reply_to() { - let msg = "Subject: Hello"; - let m = parse_mail(msg); - assert_eq!(None, m.in_reply_to); + let msg = "Subject: Hello"; + let m = parse_mail(msg); + assert_eq!(None, m.in_reply_to); } #[test] fn text_plain_us_ascii_7bit() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 US-ASCII: !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~"##; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(m.subject, "Hello",); - assert_eq!("US-ASCII: !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", m.html_body_text); - assert_eq!(m.date, Some(DateTime::from_millis(1730991244000))); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(m.subject, "Hello",); + assert_eq!("US-ASCII: !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", m.html_body_text); + assert_eq!(m.date, Some(DateTime::from_millis(1730991244000))); } #[test] fn text_plain_utf8bit() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/plain; charset=UTF-8 Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\{Β³|@"##; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!("Hello", m.subject); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Hello", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_utf_explicit_8bit() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -129,18 +129,18 @@ Content-Transfer-Encoding: 8bit Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\{Β³|@"##; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!("Hello", m.subject); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Hello", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_utf_quoted_printable() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -148,18 +148,18 @@ Content-type: text/plain; charset=UTF-8 Content-Transfer-Encoding: quoted-printable Tutanota: =C3=A4=C3=BC=C3=B6=C3=9F=E2=82=AC*#\{=C2=B3|@"##; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); - assert_eq!("Hello", m.subject); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!("Hello", m.subject); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_utf_base64() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -167,18 +167,18 @@ Content-type: text/plain; charset=UTF-8 Content-Transfer-Encoding: base64 VHV0YW5vdGE6IMOkw7zDtsOf4oKsKiNce8KzfEA="##; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!("Hello", m.subject); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Hello", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_utf_invalid_base64() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -186,176 +186,176 @@ Content-type: text/plain; charset=UTF-8 Content-Transfer-Encoding: base64 VHV0YW5vdGE6IMOkw7zDtsOf4oKsKiNce8KzfEA"##; // skip the padding "=" to force an exception - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!("Hello", m.subject); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Hello", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_format_flowed() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/plain; charset=UTF-8; format=flowed Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es \r\neinen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!"#; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!("Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es einen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!", m.html_body_text); + assert_eq!("Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es einen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!", m.html_body_text); } #[test] fn text_plain_format_flowed_del_sp() { - let msg = r#"From: A + let msg = r#"From: A To: B Subject: Hello Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/plain; charset=UTF-8; format=flowed; DelSp=yes Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es \r\neinen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!"#; - let m = parse_mail(msg); - assert_eq!( + let m = parse_mail(msg); + assert_eq!( "Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt eseinen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!", m.html_body_text); } #[test] fn text_plain_subject_encoded_word_qencoding() { - let msg = r#"Subject: Hello =?ISO-8859-1?Q?=E4=F6=FC=DF?=abc + let msg = r#"Subject: Hello =?ISO-8859-1?Q?=E4=F6=FC=DF?=abc From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 "#; - let m = parse_mail(msg); - assert_eq!("Hello Àâüßabc", m.subject); + let m = parse_mail(msg); + assert_eq!("Hello Àâüßabc", m.subject); } #[test] fn text_plain_subject_encoded_word_qencoding_turkish() { - let msg = r#"Subject: =?iso-8859-9?Q?Paracard Hesap =D6zeti?= + let msg = r#"Subject: =?iso-8859-9?Q?Paracard Hesap =D6zeti?= From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 "#; - let m = parse_mail(msg); - assert_eq!("Paracard Hesap Γ–zeti", m.subject); + let m = parse_mail(msg); + assert_eq!("Paracard Hesap Γ–zeti", m.subject); } #[test] fn from_encoded_word_qencoding() { - let msg = r#"Subject: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0?==?utf-8?Q?=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= + let msg = r#"Subject: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0?==?utf-8?Q?=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= From: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0?==?utf-8?Q?=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= To: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0?==?utf-8?Q?=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= Date: Thu, 7 Nov 2024 15:54:04 +0100 "#; - let m = parse_mail(msg); - assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.subject); - assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.from_addresses[0].name); - assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.to_addresses[0].name); + let m = parse_mail(msg); + assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.subject); + assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.from_addresses[0].name); + assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.to_addresses[0].name); } #[test] fn from_encoded_word_qencoding_colon() { - let msg = r#"Subject: Hi + let msg = r#"Subject: Hi From: =?utf-8?Q?=D0=9B=D0=B8=D1=82=D1=80=D0=B5=D1=81=3A=20=D0=A1=D0=B0=D0=BC=D0=B8=D0=B7=D0=B4=D0=B0=D1=82?= "#; - let m = parse_mail(msg); - assert_eq!( - m.from_addresses[0], - ("ЛитрСс: Π‘Π°ΠΌΠΈΠ·Π΄Π°Ρ‚", "mail@selfpub.ru").into() - ); + let m = parse_mail(msg); + assert_eq!( + m.from_addresses[0], + ("ЛитрСс: Π‘Π°ΠΌΠΈΠ·Π΄Π°Ρ‚", "mail@selfpub.ru").into() + ); } #[test] fn recipients_encoded_word_qencoding_colon() { - let msg = "To: =?utf-8?Q?=D0=9B=D0=B8=D1=82=D1=80=D0=B5=D1=81=3A=20=D0=A1=D0=B0=D0=BC=D0=B8=D0=B7=D0=B4=D0=B0=D1=82?= \n"; - let m = parse_mail(msg); - assert_eq!( - m.from_addresses[0], - ("ЛитрСс: Π‘Π°ΠΌΠΈΠ·Π΄Π°Ρ‚", "mail@selfpub.ru").into() - ); + let msg = "To: =?utf-8?Q?=D0=9B=D0=B8=D1=82=D1=80=D0=B5=D1=81=3A=20=D0=A1=D0=B0=D0=BC=D0=B8=D0=B7=D0=B4=D0=B0=D1=82?= \n"; + let m = parse_mail(msg); + assert_eq!( + m.from_addresses[0], + ("ЛитрСс: Π‘Π°ΠΌΠΈΠ·Π΄Π°Ρ‚", "mail@selfpub.ru").into() + ); } #[test] fn recipients_encoded_word_qencoding_partly() { - let msg = "To: =?ISO-8859-1?Q?Andr=E9?= Pirard \n"; - let m = parse_mail(msg); - assert_eq!( - m.from_addresses[0], - ("AndrΓ© Pirard", "PIRARD@vm1.ulg.ac.be").into() - ); + let msg = "To: =?ISO-8859-1?Q?Andr=E9?= Pirard \n"; + let m = parse_mail(msg); + assert_eq!( + m.from_addresses[0], + ("AndrΓ© Pirard", "PIRARD@vm1.ulg.ac.be").into() + ); } #[test] fn text_plain_subject_encoded_word_base64() { - let msg = r#"Subject: =?utf-8?B?SGVsbG8gw6TDtsO8w58=?= + let msg = r#"Subject: =?utf-8?B?SGVsbG8gw6TDtsO8w58=?= From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 "#; - let m = parse_mail(msg); - assert_eq!("Hello Àâüß", m.subject); + let m = parse_mail(msg); + assert_eq!("Hello Àâüß", m.subject); } #[test] fn text_html_only() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/html; charset=UTF-8 Hello Àâüß
"#; - let m = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!( - "Hello Àâüß
", - m.html_body_text - ); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!( + "Hello Àâüß
", + m.html_body_text + ); } #[test] fn charset() { - // todo!() + // todo!() } #[test] fn text_html_inline_charset_definition_utf8() { - let msg = r#"Content-type: text/html + let msg = r#"Content-type: text/html Content-Transfer-Encoding: 8bit

Π‘Π»Π°Π³ΠΎΠ΄Π°Ρ€ΠΈΠΌ Π’ΠΈ

"#; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!( - "

Π‘Π»Π°Π³ΠΎΠ΄Π°Ρ€ΠΈΠΌ Π’ΠΈ

", - m.html_body_text - ); + assert_eq!( + "

Π‘Π»Π°Π³ΠΎΠ΄Π°Ρ€ΠΈΠΌ Π’ΠΈ

", + m.html_body_text + ); } #[test] fn text_html_inline_charset_definition_western() { - let msg = r#"Content-type: text/html + let msg = r#"Content-type: text/html Content-Transfer-Encoding: base64 PGh0bWw+PGhlYWQ+PG1ldGEgY2hhcnNldD0iSVNPLTg4NTktMTUiPjwvaGVhZD48Ym9keT48cD6kIPbkPC9wPjwvYm9keT48L2h0bWw+"#; - let m = parse_mail(msg); - assert_eq!( - "

€ ΓΆΓ€

", - m.html_body_text - ); + let m = parse_mail(msg); + assert_eq!( + "

€ ΓΆΓ€

", + m.html_body_text + ); } #[test] fn text_alternative() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 (CET) @@ -371,60 +371,60 @@ Content-type: text/html; charset=UTF-8; Hello Àâüß
--frontier-- "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!("Hello", m.subject); - assert_eq!( - "Hello Àâüß
", - m.html_body_text - ); + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!("Hello", m.subject); + assert_eq!( + "Hello Àâüß
", + m.html_body_text + ); } #[test] fn invalid_domains_in_mail_addresses() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B , C , D "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!( - m.to_addresses, - vec![ - ("B", "b@a.example").into(), - ("C", "c@c.com").into(), - ("D", "d@d.invalid").into() - ] - ); + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!( + m.to_addresses, + vec![ + ("B", "b@a.example").into(), + ("C", "c@c.com").into(), + ("D", "d@d.invalid").into() + ] + ); } #[test] fn multiple_to_headers() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B , C To: D "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!( - m.to_addresses, - vec![ - ("B", "b@b.org").into(), - ("C", "c@c.com").into(), - ("D", "d@d.net").into() - ] - ); + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!( + m.to_addresses, + vec![ + ("B", "b@b.org").into(), + ("C", "c@c.com").into(), + ("D", "d@d.net").into() + ] + ); } #[test] fn attached_message() { - let msg = r#"Subject: parent message + let msg = r#"Subject: parent message From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -447,30 +447,30 @@ Content-type: text/plain; charset=UTF-8; Hello Àâüß "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(m.subject, "parent message"); - assert_eq!(m.html_body_text, "normal message"); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - - assert_eq!(m.attachments.len(), 1); - let attachment = m.attachments.first().unwrap(); - let attached = parse_mail( - String::from_utf8(attachment.content.to_vec()) - .unwrap() - .as_str(), - ); - assert_eq!(attached.from_addresses, vec![("D", "d@tutanota.de").into()]); - assert_eq!(attached.to_addresses, vec![("E", "e@tutanota.de").into()]); - assert_eq!(attached.subject, "attached message"); - assert_eq!(attached.html_body_text, "
Hello Àâüß
"); + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(m.subject, "parent message"); + assert_eq!(m.html_body_text, "normal message"); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + + assert_eq!(m.attachments.len(), 1); + let attachment = m.attachments.first().unwrap(); + let attached = parse_mail( + String::from_utf8(attachment.content.to_vec()) + .unwrap() + .as_str(), + ); + assert_eq!(attached.from_addresses, vec![("D", "d@tutanota.de").into()]); + assert_eq!(attached.to_addresses, vec![("E", "e@tutanota.de").into()]); + assert_eq!(attached.subject, "attached message"); + assert_eq!(attached.html_body_text, "
Hello Àâüß
"); } #[test] fn multiple_attachments() { - let msg = r#"Subject: multiple attachments + let msg = r#"Subject: multiple attachments From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -499,42 +499,42 @@ Content-Disposition: attachment; filename=withoutContentType.pdf; c2Vjb25kIGF0dGFjaG1lbnQ= --frontier-- "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(m.subject, "multiple attachments"); - - assert_eq!("Hello Àâüß", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(m.attachments.len(), 3); - let [a1, a2, a3] = m.attachments.try_into().unwrap(); - - assert_eq!("a1.txt", a1.filename); - assert_eq!( - String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), - String::from_utf8(a1.content.to_vec()).unwrap() - ); - assert_eq!("application/octet-stream", a1.content_type); - - assert_eq!("a2.pdf", a2.filename); - assert_eq!( - String::from_utf8(base64_decode(b"c2Vjb25kIGF0dGFjaG1lbnQ=").unwrap()).unwrap(), - String::from_utf8(a2.content.to_vec()).unwrap() - ); - assert_eq!("application/pdf", a2.content_type); - - assert_eq!("withoutContentType.pdf", a3.filename); - assert_eq!( - String::from_utf8(base64_decode(b"c2Vjb25kIGF0dGFjaG1lbnQ=").unwrap()).unwrap(), - String::from_utf8(a3.content.to_vec()).unwrap() - ); - assert_eq!(r#"text/plain; charset="us-ascii""#, a3.content_type); + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(m.subject, "multiple attachments"); + + assert_eq!("Hello Àâüß", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.attachments.len(), 3); + let [a1, a2, a3] = m.attachments.try_into().unwrap(); + + assert_eq!("a1.txt", a1.filename); + assert_eq!( + String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), + String::from_utf8(a1.content.to_vec()).unwrap() + ); + assert_eq!("application/octet-stream", a1.content_type); + + assert_eq!("a2.pdf", a2.filename); + assert_eq!( + String::from_utf8(base64_decode(b"c2Vjb25kIGF0dGFjaG1lbnQ=").unwrap()).unwrap(), + String::from_utf8(a2.content.to_vec()).unwrap() + ); + assert_eq!("application/pdf", a2.content_type); + + assert_eq!("withoutContentType.pdf", a3.filename); + assert_eq!( + String::from_utf8(base64_decode(b"c2Vjb25kIGF0dGFjaG1lbnQ=").unwrap()).unwrap(), + String::from_utf8(a3.content.to_vec()).unwrap() + ); + assert_eq!(r#"text/plain; charset="us-ascii""#, a3.content_type); } #[test] fn inline_attachment() { - let msg = r#"Subject: inline attachment + let msg = r#"Subject: inline attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -553,27 +553,27 @@ Content-ID: <123@tutanota.de>; Zmlyc3QgYXR0YWNobWVudA== --frontier-- "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - - todo!() - - // assertEquals(1, m.getAttachedFiles().size()); - // SmtpAttachment a1 = m.getAttachedFiles().get(0); - // - // assertEquals("a1.png", a1.getName()); - // assertArrayEquals(EncodingConverter.base64ToBytes("Zmlyc3QgYXR0YWNobWVudA=="), a1.getData()); - // assertEquals("application/octet-stream", a1.getMimeType()); - // assertEquals("123@tutanota.de", a1.getContentId()); - // assertNull(a1.getCharset()); + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + + todo!() + + // assertEquals(1, m.getAttachedFiles().size()); + // SmtpAttachment a1 = m.getAttachedFiles().get(0); + // + // assertEquals("a1.png", a1.getName()); + // assertArrayEquals(EncodingConverter.base64ToBytes("Zmlyc3QgYXR0YWNobWVudA=="), a1.getData()); + // assertEquals("application/octet-stream", a1.getMimeType()); + // assertEquals("123@tutanota.de", a1.getContentId()); + // assertNull(a1.getCharset()); } #[test] fn attachment_to_attached_message() { - let msg = r#"Subject: parent message + let msg = r#"Subject: parent message From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -589,36 +589,36 @@ Content-Disposition: attachment; filename=indirectly_attached.txt; Zmlyc3QgYXR0YWNobWVudA== "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(1, m.attachments.len()); - - let attached = parse_mail( - String::from_utf8(m.attachments.first().unwrap().content.clone()) - .unwrap() - .as_str(), - ); - - assert_eq!(attached.subject, "attached message"); - - assert_eq!("", attached.html_body_text); - assert_eq!(1, attached.attachments.len()); - - assert_eq!(1, attached.attachments.len()); - let indirect_attachment = attached.attachments.first().unwrap(); - assert_eq!("indirectly_attached.txt", indirect_attachment.filename); - assert_eq!( - String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), - String::from_utf8(indirect_attachment.content.to_vec()).unwrap() - ); + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(1, m.attachments.len()); + + let attached = parse_mail( + String::from_utf8(m.attachments.first().unwrap().content.clone()) + .unwrap() + .as_str(), + ); + + assert_eq!(attached.subject, "attached message"); + + assert_eq!("", attached.html_body_text); + assert_eq!(1, attached.attachments.len()); + + assert_eq!(1, attached.attachments.len()); + let indirect_attachment = attached.attachments.first().unwrap(); + assert_eq!("indirectly_attached.txt", indirect_attachment.filename); + assert_eq!( + String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), + String::from_utf8(indirect_attachment.content.to_vec()).unwrap() + ); } #[test] fn text_attachment() { - let msg = r#"Subject: text attachment + let msg = r#"Subject: text attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -635,28 +635,28 @@ Content-Disposition: attachment; filename=a1.txt; Abc, die Katze liegt im Schnee ! Àâü?ß ! --frontier-- "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - todo!() - - // assertEquals("text attachment", m.getSubject()); - // assertEquals("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.getPlainBodyText()); - // assertEquals(null, m.getHtmlBodyText()); - // assertEquals(date, m.getSentDate()); - // - // assertEquals(1, m.getAttachedFiles().size()); - // SmtpAttachment a1 = m.getAttachedFiles().get(0); - // - // assertEquals("a1.txt", a1.getName()); - // assertArrayEquals("Abc, die Katze liegt im Schnee ! Àâü?ß ! ".getBytes("UTF-8"), a1.getData()); + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() + + // assertEquals("text attachment", m.getSubject()); + // assertEquals("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.getPlainBodyText()); + // assertEquals(null, m.getHtmlBodyText()); + // assertEquals(date, m.getSentDate()); + // + // assertEquals(1, m.getAttachedFiles().size()); + // SmtpAttachment a1 = m.getAttachedFiles().get(0); + // + // assertEquals("a1.txt", a1.getName()); + // assertArrayEquals("Abc, die Katze liegt im Schnee ! Àâü?ß ! ".getBytes("UTF-8"), a1.getData()); } #[test] fn html_attachment() { - let msg = r#"Subject: html attachment + let msg = r#"Subject: html attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -673,28 +673,28 @@ Content-Disposition: attachment; filename=a1.html; Hello Àâüß
--frontier-- "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - todo!() - - // assertEquals("html attachment", m.getSubject()); - // assertEquals("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.getPlainBodyText()); - // assertEquals(null, m.getHtmlBodyText()); - // assertEquals(date, m.getSentDate()); - // - // assertEquals(1, m.getAttachedFiles().size()); - // SmtpAttachment a1 = m.getAttachedFiles().get(0); - // - // assertEquals("a1.html", a1.getName()); - // assertArrayEquals("Hello Àâüß
".getBytes("UTF-8"), a1.getData()); + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() + + // assertEquals("html attachment", m.getSubject()); + // assertEquals("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.getPlainBodyText()); + // assertEquals(null, m.getHtmlBodyText()); + // assertEquals(date, m.getSentDate()); + // + // assertEquals(1, m.getAttachedFiles().size()); + // SmtpAttachment a1 = m.getAttachedFiles().get(0); + // + // assertEquals("a1.html", a1.getName()); + // assertArrayEquals("Hello Àâüß
".getBytes("UTF-8"), a1.getData()); } #[test] fn multiple_plain_body_text_parts_are_concatenated() { - let eml_contents = r#"Message-Id: some-id + let eml_contents = r#"Message-Id: some-id From: A To: B Date: Tue, 5 Nov 2024 13:18:59 +0000 @@ -712,55 +712,55 @@ second plain text in body --line-- "#; - let parsed_message = MessageParser::default() - .with_mime_headers() - .parse(eml_contents) - .unwrap(); - let text_contents = parsed_message - .text_bodies() - .map(|a| a.text_contents().unwrap()) - .collect::>() - .join(""); - assert_eq!( - "first plain text in body\nsecond plain text in body", - text_contents - ); - - // String firstPlainBodyText = "Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@"; - // String secondPlainBodyText = "Abc, die Katze liegt im Schnee ! Àâü?ß !"; - // StringBuilder b = new StringBuilder() - // .append("Subject: multiple text/plain parts concatenated\n") - // .append("From: A \n") - // .append("To: B \n") - // .append("Date: " + new MailDateFormat().format(date) + "\n") - // .append("Content-Type: multipart/mixed; boundary=frontier\n") - // .append("\n") - // .append("--frontier\n") - // .append("Content-type: text/plain; charset=UTF-8\n") - // .append("\n") - // .append(firstPlainBodyText + "\n") - // .append("--frontier\n") - // .append("Content-type: text/plain; charset=UTF-8\n") - // .append("\n") - // .append(secondPlainBodyText + "\n") - // .append("--frontier--"); - // - // SmtpMessage m = mimeStringToSmtpMessageConverter.mimeToSmtpMessage( - // mimeStringToSmtpMessageConverter.dataToMimeMessage(b.toString().getBytes(StandardCharsets.UTF_8)), - // null - // ); - // - // assertEquals("multiple text/plain parts concatenated", m.getSubject()); - // String concatenatedPlainBodyText = firstPlainBodyText.concat(secondPlainBodyText); - // assertEquals(concatenatedPlainBodyText, m.getPlainBodyText()); - // assertEquals(null, m.getHtmlBodyText()); - // assertEquals(date, m.getSentDate()); - // assertEquals(0, m.getAttachedFiles().size()); + let parsed_message = MessageParser::default() + .with_mime_headers() + .parse(eml_contents) + .unwrap(); + let text_contents = parsed_message + .text_bodies() + .map(|a| a.text_contents().unwrap()) + .collect::>() + .join(""); + assert_eq!( + "first plain text in body\nsecond plain text in body", + text_contents + ); + + // String firstPlainBodyText = "Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@"; + // String secondPlainBodyText = "Abc, die Katze liegt im Schnee ! Àâü?ß !"; + // StringBuilder b = new StringBuilder() + // .append("Subject: multiple text/plain parts concatenated\n") + // .append("From: A \n") + // .append("To: B \n") + // .append("Date: " + new MailDateFormat().format(date) + "\n") + // .append("Content-Type: multipart/mixed; boundary=frontier\n") + // .append("\n") + // .append("--frontier\n") + // .append("Content-type: text/plain; charset=UTF-8\n") + // .append("\n") + // .append(firstPlainBodyText + "\n") + // .append("--frontier\n") + // .append("Content-type: text/plain; charset=UTF-8\n") + // .append("\n") + // .append(secondPlainBodyText + "\n") + // .append("--frontier--"); + // + // SmtpMessage m = mimeStringToSmtpMessageConverter.mimeToSmtpMessage( + // mimeStringToSmtpMessageConverter.dataToMimeMessage(b.toString().getBytes(StandardCharsets.UTF_8)), + // null + // ); + // + // assertEquals("multiple text/plain parts concatenated", m.getSubject()); + // String concatenatedPlainBodyText = firstPlainBodyText.concat(secondPlainBodyText); + // assertEquals(concatenatedPlainBodyText, m.getPlainBodyText()); + // assertEquals(null, m.getHtmlBodyText()); + // assertEquals(date, m.getSentDate()); + // assertEquals(0, m.getAttachedFiles().size()); } #[test] fn multiple_html_body_text_parts_are_concatenated() { - let msg = r#"Message-Id: some-id + let msg = r#"Message-Id: some-id Subject: multiple text/html parts concatenated From: A To: B @@ -778,18 +778,18 @@ Content-type: text/html; charset=UTF-8 --frontier-- "#; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!("multiple text/html parts concatenated", m.subject); - assert_eq!("Hello Àâüß
Test Test
", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(0, m.attachments.len()); + assert_eq!("multiple text/html parts concatenated", m.subject); + assert_eq!("Hello Àâüß
Test Test
", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(0, m.attachments.len()); } #[test] // todo! what does this test (map) fn concatenate_alternative_html_text_parts() { - let msg = r#"Message-Id: some-id + let msg = r#"Message-Id: some-id From: A To: B Date: Tue, 5 Nov 2024 13:18:59 +0000 @@ -807,14 +807,14 @@ Content-Type: text/html; charset=UTF-8 --line-- "#; - let m = parse_mail(msg); - todo!() + let m = parse_mail(msg); + todo!() } #[test] // todo! what does this test (map) fn concatenate_multiple_html_and_plain_text_parts() { - let msg = r#"Message-Id: some-id + let msg = r#"Message-Id: some-id From: A To: B Date: Tue, 5 Nov 2024 13:18:59 +0000 @@ -834,14 +834,14 @@ first plain text in body --line-- "#; - let m = parse_mail(msg); - todo!() + let m = parse_mail(msg); + todo!() } #[test] fn plain_body_text_parts_are_concatenated_with_html_body_parts_if_html_body_parts_already_existing() { - let msg = r#"Subject: multiple text/html and text/plain parts concatenated to single text/html + let msg = r#"Subject: multiple text/html and text/plain parts concatenated to single text/html From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -861,45 +861,45 @@ Content-type: text/plain; charset=UTF-8 Abc, die Katze liegt im Schnee ! Àâü?ß ! --frontier- "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!( - "multiple text/html and text/plain parts concatenated to single text/html", - m.subject - ); - todo!() - - // assertEquals("multiple text/html and text/plain parts concatenated to single text/html", m.getSubject()); - // String concatenatedHtmlBodyText = firstHtmlBodyText.concat(secondHtmlBodyText).concat(PlainTextHtmlConverter.plainTextToHtml(firstPlainBodyText)); - // assertEquals(null, m.getPlainBodyText()); - // assertEquals(concatenatedHtmlBodyText, m.getHtmlBodyText()); - // assertEquals(date, m.getSentDate()); - // assertEquals(0, m.getAttachedFiles().size()); + let m: ImportableMail = parse_mail(msg); + + assert_eq!( + "multiple text/html and text/plain parts concatenated to single text/html", + m.subject + ); + todo!() + + // assertEquals("multiple text/html and text/plain parts concatenated to single text/html", m.getSubject()); + // String concatenatedHtmlBodyText = firstHtmlBodyText.concat(secondHtmlBodyText).concat(PlainTextHtmlConverter.plainTextToHtml(firstPlainBodyText)); + // assertEquals(null, m.getPlainBodyText()); + // assertEquals(concatenatedHtmlBodyText, m.getHtmlBodyText()); + // assertEquals(date, m.getSentDate()); + // assertEquals(0, m.getAttachedFiles().size()); } #[test] fn plain_body_text_parts_are_converted_to_html_body_parts_if_html_body_parts_follow_afterwards() { - let msg = r#" + let msg = r#" "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - todo!() - - // assertEquals("multiple plain/text and text/html parts concatenated to single text/html", m.getSubject()); - // String concatenatedHtmlBodyText = PlainTextHtmlConverter.plainTextToHtml(firstPlainBodyText).concat(firstHtmlBodyText).concat(secondHtmlBodyText); - // assertEquals(null, m.getPlainBodyText()); - // assertEquals(concatenatedHtmlBodyText, m.getHtmlBodyText()); - // assertEquals(date, m.getSentDate()); - // assertEquals(0, m.getAttachedFiles().size()); + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() + + // assertEquals("multiple plain/text and text/html parts concatenated to single text/html", m.getSubject()); + // String concatenatedHtmlBodyText = PlainTextHtmlConverter.plainTextToHtml(firstPlainBodyText).concat(firstHtmlBodyText).concat(secondHtmlBodyText); + // assertEquals(null, m.getPlainBodyText()); + // assertEquals(concatenatedHtmlBodyText, m.getHtmlBodyText()); + // assertEquals(date, m.getSentDate()); + // assertEquals(0, m.getAttachedFiles().size()); } #[test] fn text_attachment_with_disposition() { - let msg = r#"Subject: text attachment + let msg = r#"Subject: text attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -908,28 +908,28 @@ Content-Disposition: attachment; filename=a1.txt; Abc, die Katze liegt im Schnee ! Àâü?ß ! "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - todo!() - - // assertEquals("text attachment", m.getSubject()); - // assertNull(m.getPlainBodyText()); - // assertNull(m.getHtmlBodyText()); - // assertEquals(date, m.getSentDate()); - // - // assertEquals(1, m.getAttachedFiles().size()); - // SmtpAttachment a1 = m.getAttachedFiles().get(0); - // - // assertEquals("a1.txt", a1.getName()); - // assertArrayEquals("Abc, die Katze liegt im Schnee ! Àâü?ß ! ".getBytes("UTF-8"), a1.getData()); + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + todo!() + + // assertEquals("text attachment", m.getSubject()); + // assertNull(m.getPlainBodyText()); + // assertNull(m.getHtmlBodyText()); + // assertEquals(date, m.getSentDate()); + // + // assertEquals(1, m.getAttachedFiles().size()); + // SmtpAttachment a1 = m.getAttachedFiles().get(0); + // + // assertEquals("a1.txt", a1.getName()); + // assertArrayEquals("Abc, die Katze liegt im Schnee ! Àâü?ß ! ".getBytes("UTF-8"), a1.getData()); } #[test] fn attachment_with_non_ascii_name() { - let msg = r#"Subject: text attachment + let msg = r#"Subject: text attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -937,18 +937,18 @@ Content-type: text/plain; charset=UTF-8; name=\"=?ISO-8859-1?Q?a=F6i=2Epdf?=\" Content-Disposition: attachment; filename*=ISO-8859-1''%61%F6%69%2E%70%64%66 Abc, die Katze liegt im Schnee ! Àâü?ß ! "#; - let m: ImportableMail = parse_mail(msg); + let m: ImportableMail = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(1, m.attachments.len()); - assert_eq!("aΓΆi.pdf", m.attachments.first().unwrap().filename); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(1, m.attachments.len()); + assert_eq!("aΓΆi.pdf", m.attachments.first().unwrap().filename); } #[test] fn attachment_filename_in_content_type() { - let msg = r#"Subject: message with named file attachment + let msg = r#"Subject: message with named file attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -956,24 +956,24 @@ Content-type: application/octet-stream; name=indirectly_attached.txt; Content-Transfer-Encoding: base64 Zmlyc3QgYXR0YWNobWVudA=="#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - - assert_eq!(1, m.attachments.len()); - let indirect_attachment = m.attachments.first().unwrap(); - assert_eq!("indirectly_attached.txt", indirect_attachment.filename); - assert_eq!( - String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), - String::from_utf8(indirect_attachment.content.to_vec()).unwrap() - ); + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + + assert_eq!(1, m.attachments.len()); + let indirect_attachment = m.attachments.first().unwrap(); + assert_eq!("indirectly_attached.txt", indirect_attachment.filename); + assert_eq!( + String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), + String::from_utf8(indirect_attachment.content.to_vec()).unwrap() + ); } #[test] fn attachment_filename_qencoding() { - let msg = r#"Subject: message with named file attachment + let msg = r#"Subject: message with named file attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -981,24 +981,24 @@ Content-type: application/octet-stream; name==?utf-8?Q?=C3=A4=C3=B6=C3=9F=E2=82= Content-Transfer-Encoding: base64 Zmlyc3QgYXR0YWNobWVudA=="#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - - assert_eq!(1, m.attachments.len()); - let indirect_attachment = m.attachments.first().unwrap(); - assert_eq!("Γ€ΓΆΓŸβ‚¬.txt", indirect_attachment.filename); - assert_eq!( - String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), - String::from_utf8(indirect_attachment.content.to_vec()).unwrap() - ); + let m: ImportableMail = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + + assert_eq!(1, m.attachments.len()); + let indirect_attachment = m.attachments.first().unwrap(); + assert_eq!("Γ€ΓΆΓŸβ‚¬.txt", indirect_attachment.filename); + assert_eq!( + String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), + String::from_utf8(indirect_attachment.content.to_vec()).unwrap() + ); } #[test] fn encrypted() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -1010,48 +1010,48 @@ Content-Transfer-Encoding: base64 SGFsbG8= --frontier--"#; - let m: ImportableMail = parse_mail(msg); + let m: ImportableMail = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(1, m.attachments.len()); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(1, m.attachments.len()); - // assertEquals(1, m.getAttachedFiles().size()); + // assertEquals(1, m.getAttachedFiles().size()); } #[test] fn recipient_groups() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: foo:a@b.example.de,c@d.example.de,e@f.example.de; Reply-To: ??? Date: Thu, 7 Nov 2024 15:54:04 +0100"#; - let m = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!( - m.to_addresses, - vec![ - ("", "a@b.example.de").into(), - ("", "c@d.example.de").into(), - ("", "e@f.example.de").into() - ] - ); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!( + m.to_addresses, + vec![ + ("", "a@b.example.de").into(), + ("", "c@d.example.de").into(), + ("", "e@f.example.de").into() + ] + ); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn undisclosed_recipients() { - let msg = r#"To: undisclosed-recipients:;"#; - let m = parse_mail(msg); + let msg = r#"To: undisclosed-recipients:;"#; + let m = parse_mail(msg); - assert_eq!(0, m.to_addresses.len()); + assert_eq!(0, m.to_addresses.len()); } #[test] fn long_content_type() { - let msg = r#"From: A + let msg = r#"From: A Content-type: multipart/mixed; boundary=frontier --frontier @@ -1061,52 +1061,52 @@ Content-Disposition: attachment; filename=withoutContentType.pdf; Message --frontier-- "#; - let m = parse_mail(msg); + let m = parse_mail(msg); - let attachment = m.attachments.first().unwrap(); - assert_eq!("withoutContentType.pdf", attachment.filename); - assert_eq!("text/plain", attachment.content_type); - todo!() - // assertEquals("us-ascii", m.getAttachedFiles().get(0).getCharset()); + let attachment = m.attachments.first().unwrap(); + assert_eq!("withoutContentType.pdf", attachment.filename); + assert_eq!("text/plain", attachment.content_type); + todo!() + // assertEquals("us-ascii", m.getAttachedFiles().get(0).getCharset()); } #[test] fn normalize_header_value() { - todo!() - // // trim and remove LF and CR - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" ").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" ").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" \r \n ").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" \n \r ").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds("\n\r \r\n").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" | \n\r | ").toArray(new String[0])); - // assertArrayEquals(new String[0], MimeStringToSmtpMessageConverter.stripMessageIds(" <>").toArray(new String[0])); - // - // // remove comments - // assertArrayEquals(new String[0], MimeStringToSmtpMessageConverter.stripMessageIds(" (abc)").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" (abc) ").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" (abc) (def) ").toArray(new String[0])); - // - // // illegal comments - // assertArrayEquals(new String[0], MimeStringToSmtpMessageConverter.stripMessageIds(" (abc").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" abc) ").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" abc (abc").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" )abc def( ").toArray(new String[0])); - // - // // ids in comments are currently recognized - // assertArrayEquals(new String[]{"a@b", "g@h", "i@d"}, - // MimeStringToSmtpMessageConverter.stripMessageIds(" (abc) (def) ()").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" (abc def) ").toArray(new String[0])); + todo!() + // // trim and remove LF and CR + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" ").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" ").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" \r \n ").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" \n \r ").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds("\n\r \r\n").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" | \n\r | ").toArray(new String[0])); + // assertArrayEquals(new String[0], MimeStringToSmtpMessageConverter.stripMessageIds(" <>").toArray(new String[0])); + // + // // remove comments + // assertArrayEquals(new String[0], MimeStringToSmtpMessageConverter.stripMessageIds(" (abc)").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" (abc) ").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" (abc) (def) ").toArray(new String[0])); + // + // // illegal comments + // assertArrayEquals(new String[0], MimeStringToSmtpMessageConverter.stripMessageIds(" (abc").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" abc) ").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" abc (abc").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" )abc def( ").toArray(new String[0])); + // + // // ids in comments are currently recognized + // assertArrayEquals(new String[]{"a@b", "g@h", "i@d"}, + // MimeStringToSmtpMessageConverter.stripMessageIds(" (abc) (def) ()").toArray(new String[0])); + // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" (abc def) ").toArray(new String[0])); } #[test] fn get_spf_result() { - // net yet used on rust + // net yet used on rust } #[test] fn mail_from_with_delemiter() { - let msg = r#"Message-ID: 123456 + let msg = r#"Message-ID: 123456 Subject: Hello From: A,B To: B @@ -1116,31 +1116,31 @@ Content-Type: multipart/mixed; boundary=frontier --frontier "#; - let m = parse_mail(msg); + let m = parse_mail(msg); - todo!() - // assertEquals("A, B ", m.getSender().getMailAddress()); - // assertEquals("", m.getSender().getName()); - // assertFalse(m.getSender().isValid()); + todo!() + // assertEquals("A, B ", m.getSender().getMailAddress()); + // assertEquals("", m.getSender().getName()); + // assertFalse(m.getSender().isValid()); } #[test] fn incomplete_text_content_type() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text any body text"#; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!("any body text", m.html_body_text); + assert_eq!("any body text", m.html_body_text); } #[test] fn calendar_content_type() { - let msg = r#"Message-ID: 123456 + let msg = r#"Message-ID: 123456 Subject: Hello From: A To: B @@ -1148,16 +1148,16 @@ References: <1234564@web.de> Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-Type: text/calendar; charset=\"UTF-8\"; method=REQUEST "#; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!("text/calendar", m.attachments.first().unwrap().content_type); - // assertEquals("REQUEST", m.getAttachedFiles().get(0).getCalendarMethod()); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!("text/calendar", m.attachments.first().unwrap().content_type); + // assertEquals("REQUEST", m.getAttachedFiles().get(0).getCalendarMethod()); } #[test] fn calendar_content_type_method() { - let msg = r#"Message-ID: 123456 + let msg = r#"Message-ID: 123456 Subject: Hello From: A To: B @@ -1165,31 +1165,31 @@ References: <1234564@web.de> Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-Type: text/calendar; charset="UTF-8"; method=request; "#; - let m: ImportableMail = parse_mail(msg); + let m: ImportableMail = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - let attachment = m.attachments.first().unwrap(); - assert_eq!("text/calendar", attachment.content_type); - // todo! assert_eq!("REQUEST", calendar_method) + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + let attachment = m.attachments.first().unwrap(); + assert_eq!("text/calendar", attachment.content_type); + // todo! assert_eq!("REQUEST", calendar_method) } #[test] fn invalid_content_types_default_to_text_plain() { - let invalid_content_types = vec![ - "Content-Type:", - "Content-Type: _", - "Content-Type: text", - "Content-Type; text/html", - "Content-Type; invalid/type", - "Content-Type: application/pdf; no_parameter_name.pdf", - ]; - for invalid_content_type in invalid_content_types { - let parsed = MessageParser::default() - .parse(invalid_content_type) - .unwrap(); - assert_eq!( - "text/plain", - parsed.content_type().unwrap().c_type.to_string() - ); - } + let invalid_content_types = vec![ + "Content-Type:", + "Content-Type: _", + "Content-Type: text", + "Content-Type; text/html", + "Content-Type; invalid/type", + "Content-Type: application/pdf; no_parameter_name.pdf", + ]; + for invalid_content_type in invalid_content_types { + let parsed = MessageParser::default() + .parse(invalid_content_type) + .unwrap(); + assert_eq!( + "text/plain", + parsed.content_type().unwrap().c_type.to_string() + ); + } } diff --git a/packages/node-mimimi/src/importer/importable_mail/plain_text_to_html_converter.rs b/packages/node-mimimi/src/importer/importable_mail/plain_text_to_html_converter.rs index 51f6811fb24..4b3af701f62 100644 --- a/packages/node-mimimi/src/importer/importable_mail/plain_text_to_html_converter.rs +++ b/packages/node-mimimi/src/importer/importable_mail/plain_text_to_html_converter.rs @@ -9,92 +9,92 @@ use regex::Regex; /// /// This code is ported from tutadb PlainTextToHtmlConverter pub(super) fn plain_text_to_html(plain_text: &str) -> String { - let mut result: String = String::from(""); - let separator: Regex = Regex::new("\r?\n").expect("invalid regex"); // todo! move to const - let lines = separator.split(plain_text); - let mut previous_quote_level = 0; - for (i, line) in lines.enumerate() { - let line_quote_level = get_line_quote_level(line.to_string()); - - if i > 0 && (previous_quote_level == line_quote_level) { - // only append an explicit newline (
) if the quoteLevel does not change (implicit newline in case of
) - result.push_str("
") - } - - result.push_str( - "
" - .repeat( - (previous_quote_level - line_quote_level) - .try_into() - .unwrap_or(0), - ) - .as_str(), - ); - result.push_str( - "
" - .repeat( - (line_quote_level - previous_quote_level) - .try_into() - .unwrap_or(0), - ) - .as_str(), - ); - - if line_quote_level > 0 { - if line.len() > line_quote_level as usize { - let quote_block_start_index: usize = (line_quote_level + 1) as usize; - let indented_line: &str = &line[quote_block_start_index..]; - let escaped_line = escape_plain_text_line(indented_line); - result.push_str(&escaped_line) - } - } else { - let escaped_line = escape_plain_text_line(line); - result.push_str(&escaped_line); // skip '> ', '>> ', ... - } - previous_quote_level = line_quote_level - } - - result.push_str( - "
" - .repeat((previous_quote_level).try_into().unwrap_or(0)) - .as_str(), - ); - - result + let mut result: String = String::from(""); + let separator: Regex = Regex::new("\r?\n").expect("invalid regex"); // todo! move to const + let lines = separator.split(plain_text); + let mut previous_quote_level = 0; + for (i, line) in lines.enumerate() { + let line_quote_level = get_line_quote_level(line.to_string()); + + if i > 0 && (previous_quote_level == line_quote_level) { + // only append an explicit newline (
) if the quoteLevel does not change (implicit newline in case of
) + result.push_str("
") + } + + result.push_str( + "
" + .repeat( + (previous_quote_level - line_quote_level) + .try_into() + .unwrap_or(0), + ) + .as_str(), + ); + result.push_str( + "
" + .repeat( + (line_quote_level - previous_quote_level) + .try_into() + .unwrap_or(0), + ) + .as_str(), + ); + + if line_quote_level > 0 { + if line.len() > line_quote_level as usize { + let quote_block_start_index: usize = (line_quote_level + 1) as usize; + let indented_line: &str = &line[quote_block_start_index..]; + let escaped_line = escape_plain_text_line(indented_line); + result.push_str(&escaped_line) + } + } else { + let escaped_line = escape_plain_text_line(line); + result.push_str(&escaped_line); // skip '> ', '>> ', ... + } + previous_quote_level = line_quote_level + } + + result.push_str( + "
" + .repeat((previous_quote_level).try_into().unwrap_or(0)) + .as_str(), + ); + + result } fn escape_plain_text_line(line: &str) -> String { - let escaped_line = line.replace("&", "&"); - let escaped_line = escaped_line.replace("<", "<"); + let escaped_line = line.replace("&", "&"); + let escaped_line = escaped_line.replace("<", "<"); - escaped_line.replace(">", ">") + escaped_line.replace(">", ">") } fn get_line_quote_level(line: String) -> i32 { - let mut line_open_blockquotes = 0; - for char in line.chars() { - if char == '>' { - line_open_blockquotes += 1; - } else if char == ' ' { - break; - } else { - line_open_blockquotes = 0; - break; - } - } - line_open_blockquotes + let mut line_open_blockquotes = 0; + for char in line.chars() { + if char == '>' { + line_open_blockquotes += 1; + } else if char == ' ' { + break; + } else { + line_open_blockquotes = 0; + break; + } + } + line_open_blockquotes } #[cfg(test)] mod test { - use crate::importer::importable_mail::plain_text_to_html_converter::plain_text_to_html; - - /** - * Adds and tags to the given html - */ - fn add_html_page_tags(html: String) -> String { - format!( - "\r\n\ + use crate::importer::importable_mail::plain_text_to_html_converter::plain_text_to_html; + + /** + * Adds and tags to the given html + */ + fn add_html_page_tags(html: String) -> String { + format!( + "\r\n\ \r\n\ \r\n\ \r\n\ @@ -102,52 +102,51 @@ mod test { {}\ \r\n\ \r\n", - html - ) - } + html + ) + } - - #[test] - pub fn convert_to_html() { - assert_eq!("Test-Mail im Plain-Text und mit komischen Zeichen: & \"~ΓΆΓ€β₯£Τ»Β³@
weiter gehts in der naechsten Zeile", + #[test] + pub fn convert_to_html() { + assert_eq!("Test-Mail im Plain-Text und mit komischen Zeichen: & \"~ΓΆΓ€β₯£Τ»Β³@
weiter gehts in der naechsten Zeile", plain_text_to_html("Test-Mail im Plain-Text und mit komischen Zeichen: & \"~ΓΆΓ€β₯£Τ»Β³@\r\nweiter gehts in der naechsten Zeile")); - assert_eq!( - "
simple blockquote
", - plain_text_to_html("> simple blockquote") - ); + assert_eq!( + "
simple blockquote
", + plain_text_to_html("> simple blockquote") + ); - assert_eq!( - "
blockquote
with line break
", - plain_text_to_html("> blockquote \r\n> with line break") - ); + assert_eq!( + "
blockquote
with line break
", + plain_text_to_html("> blockquote \r\n> with line break") + ); - assert_eq!( - "
blockquote
with line break
", - plain_text_to_html(">> blockquote \r\n> with line break") - ); + assert_eq!( + "
blockquote
with line break
", + plain_text_to_html(">> blockquote \r\n> with line break") + ); - assert_eq!( - "
blockquote
with line break
", - plain_text_to_html("> blockquote \r\n>> with line break") - ); + assert_eq!( + "
blockquote
with line break
", + plain_text_to_html("> blockquote \r\n>> with line break") + ); - assert_eq!("
blockquote
with line break", + assert_eq!("
blockquote
with line break", plain_text_to_html(">>> blockquote \r\n with line break")); - // quote without text - assert_eq!("
", plain_text_to_html(">")); + // quote without text + assert_eq!("
", plain_text_to_html(">")); - // quote without text but newline - assert_eq!( - "

", - plain_text_to_html(">\r\n>") - ); - } + // quote without text but newline + assert_eq!( + "

", + plain_text_to_html(">\r\n>") + ); + } - #[test] - pub fn test_add_html_page_tags() { - let expected = "\r\n\ + #[test] + pub fn test_add_html_page_tags() { + let expected = "\r\n\ \r\n\ \r\n\ \r\n\ @@ -155,9 +154,9 @@ mod test { Test-Mail im Plain-Text\ \r\n\ \r\n"; - assert_eq!( - expected, - add_html_page_tags("Test-Mail im Plain-Text".to_string()) - ); - } + assert_eq!( + expected, + add_html_page_tags("Test-Mail im Plain-Text".to_string()) + ); + } } diff --git a/packages/node-mimimi/src/lib.rs b/packages/node-mimimi/src/lib.rs index 194c50bfb03..e1e09b69552 100644 --- a/packages/node-mimimi/src/lib.rs +++ b/packages/node-mimimi/src/lib.rs @@ -3,6 +3,6 @@ pub mod importer; #[cfg(feature = "javascript")] pub mod logging; +mod reduce_to_chunks; pub mod tuta; mod tuta_imap; -mod reduce_to_chunks; diff --git a/packages/node-mimimi/src/reduce_to_chunks.rs b/packages/node-mimimi/src/reduce_to_chunks.rs index 645f5e83789..fd9f9ca0b02 100644 --- a/packages/node-mimimi/src/reduce_to_chunks.rs +++ b/packages/node-mimimi/src/reduce_to_chunks.rs @@ -3,96 +3,113 @@ use std::ops::Deref; struct ChunkingIterator where - Inner: Iterator, + Inner: Iterator, { - inner: Peekable, - max_size: usize, - sizer: Box usize>, + inner: Peekable, + max_size: usize, + sizer: Box usize>, } impl Iterator for ChunkingIterator where - Inner: Iterator, + Inner: Iterator, { - type Item = Vec; - fn next(&mut self) -> Option { - let seq = &mut self.inner; - let mut element = seq.peek()?; + type Item = Vec; + fn next(&mut self) -> Option { + let seq = &mut self.inner; + let mut element = seq.peek()?; - let mut chunk: Vec = Vec::new(); - let mut current_chunk_size = 0_usize; - loop { - let element_size = self.sizer.deref()(element); - if element_size > self.max_size { - // this element is too big for one chunk. we might just ignore that and make a - // one-element chunk that fails to upload, or we stop iteration here. - // this discards any elements already in the chunk - return None; - } - let new_chunk_size = current_chunk_size.saturating_add(element_size); - if new_chunk_size > self.max_size { - // chunk is full - this element goes into the next chunk. - // because we used peek() it'll still be available for the next call to this function. - return Some(chunk); - } else { - current_chunk_size = new_chunk_size; - chunk.push(seq.next().expect("got None from next even though peek() gave Some")); - element = match seq.peek() { - None => break, - Some(e) => e - }; - } - } - Some(chunk) - } + let mut chunk: Vec = Vec::new(); + let mut current_chunk_size = 0_usize; + loop { + let element_size = self.sizer.deref()(element); + if element_size > self.max_size { + // this element is too big for one chunk. we might just ignore that and make a + // one-element chunk that fails to upload, or we stop iteration here. + // this discards any elements already in the chunk + return None; + } + let new_chunk_size = current_chunk_size.saturating_add(element_size); + if new_chunk_size > self.max_size { + // chunk is full - this element goes into the next chunk. + // because we used peek() it'll still be available for the next call to this function. + return Some(chunk); + } else { + current_chunk_size = new_chunk_size; + chunk.push( + seq.next() + .expect("got None from next even though peek() gave Some"), + ); + element = match seq.peek() { + None => break, + Some(e) => e, + }; + } + } + Some(chunk) + } } /// split a given vector of elements into a vector of chunks not exceeding max_size, where the /// chunks size is calculated by summing up the elements sizes as given by the sizer function. /// /// the number of chunks is not guaranteed to be optimal. -pub fn reduce_to_chunks<'element, Element: 'element>(seq: impl Iterator, max_size: usize, sizer: Box usize>) -> impl Iterator> { - ChunkingIterator { inner: seq.peekable(), max_size, sizer } +pub fn reduce_to_chunks<'element, Element: 'element>( + seq: impl Iterator, + max_size: usize, + sizer: Box usize>, +) -> impl Iterator> { + ChunkingIterator { + inner: seq.peekable(), + max_size, + sizer, + } } - #[cfg(test)] mod tests { - use crate::reduce_to_chunks::reduce_to_chunks; + use crate::reduce_to_chunks::reduce_to_chunks; - #[test] - fn reduce_to_chunks_simple() { - assert_eq!(vec![ - vec![1, 2, 3], - vec![4], - vec![5], - vec![6] - ], - reduce_to_chunks::(vec![1, 2, 3, 4, 5, 6].into_iter(), 6, Box::new(|item| { *item })).collect::>>() - ); - } + #[test] + fn reduce_to_chunks_simple() { + assert_eq!( + vec![vec![1, 2, 3], vec![4], vec![5], vec![6]], + reduce_to_chunks::( + vec![1, 2, 3, 4, 5, 6].into_iter(), + 6, + Box::new(|item| { *item }) + ) + .collect::>>() + ); + } - #[test] - fn reduce_to_chunks_no_split() { - assert_eq!(vec![ - vec![1, 2, 3, 4, 5, 6], - ], - reduce_to_chunks::(vec![1, 2, 3, 4, 5, 6].into_iter(), 21, Box::new(|item| { *item })).collect::>>() - ); - } + #[test] + fn reduce_to_chunks_no_split() { + assert_eq!( + vec![vec![1, 2, 3, 4, 5, 6],], + reduce_to_chunks::( + vec![1, 2, 3, 4, 5, 6].into_iter(), + 21, + Box::new(|item| { *item }) + ) + .collect::>>() + ); + } - #[test] - fn reduce_to_chunks_empty() { - assert_eq!( - Vec::>::new(), - reduce_to_chunks::(vec![].into_iter(), 0, Box::new(|item| { *item })).collect::>>() - ); - } + #[test] + fn reduce_to_chunks_empty() { + assert_eq!( + Vec::>::new(), + reduce_to_chunks::(vec![].into_iter(), 0, Box::new(|item| { *item })) + .collect::>>() + ); + } - #[test] - fn split_too_big() { - assert_eq!( - Vec::>::new(), - reduce_to_chunks::(vec![1, 10, 11].into_iter(), 2, Box::new(|item| { *item })).collect::>>() - ); - } -} \ No newline at end of file + #[test] + fn split_too_big() { + assert_eq!( + Vec::>::new(), + reduce_to_chunks::(vec![1, 10, 11].into_iter(), 2, Box::new(|item| { *item })) + .collect::>>() + ); + } +} diff --git a/packages/node-mimimi/src/tuta_imap/client/tls_stream.rs b/packages/node-mimimi/src/tuta_imap/client/tls_stream.rs index 936337810ea..65e915efd42 100644 --- a/packages/node-mimimi/src/tuta_imap/client/tls_stream.rs +++ b/packages/node-mimimi/src/tuta_imap/client/tls_stream.rs @@ -11,99 +11,99 @@ use std::time::Duration; pub type SecuredStream = rustls::StreamOwned; pub struct TlsStream { - buffer_controller: BufReader, + buffer_controller: BufReader, } impl TlsStream { - pub fn new(address: &str, port: u16) -> Self { - let tcp_address = SocketAddr::V4(SocketAddrV4::new( - std::net::Ipv4Addr::from_str(address).unwrap(), - port, - )); - let tcp_stream = TcpStream::connect_timeout(&tcp_address, Duration::from_secs(10)).unwrap(); + pub fn new(address: &str, port: u16) -> Self { + let tcp_address = SocketAddr::V4(SocketAddrV4::new( + std::net::Ipv4Addr::from_str(address).unwrap(), + port, + )); + let tcp_stream = TcpStream::connect_timeout(&tcp_address, Duration::from_secs(10)).unwrap(); - let dangerous_config = ClientConfig::builder() - .dangerous() - .with_custom_certificate_verifier(Arc::new(MockSsl)) - .with_no_client_auth(); - let client_connection = rustls::ClientConnection::new( - Arc::new(dangerous_config), - address.to_string().try_into().unwrap(), - ) - .unwrap(); + let dangerous_config = ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(MockSsl)) + .with_no_client_auth(); + let client_connection = rustls::ClientConnection::new( + Arc::new(dangerous_config), + address.to_string().try_into().unwrap(), + ) + .unwrap(); - let buffer_controller = BufReader::new(SecuredStream::new(client_connection, tcp_stream)); - TlsStream { buffer_controller } - } + let buffer_controller = BufReader::new(SecuredStream::new(client_connection, tcp_stream)); + TlsStream { buffer_controller } + } - pub fn write_imap_command(&mut self, encoded_command: &[u8]) -> std::io::Result { - let writer = self.buffer_controller.get_mut(); - let written = writer.write(encoded_command)?; - writer.flush()?; - Ok(written) - } + pub fn write_imap_command(&mut self, encoded_command: &[u8]) -> std::io::Result { + let writer = self.buffer_controller.get_mut(); + let written = writer.write(encoded_command)?; + writer.flush()?; + Ok(written) + } - pub fn read_until_crlf(&mut self) -> std::io::Result> { - let mut line_until_crlf = Vec::new(); - self.buffer_controller - .read_until_slice(b"\r\n", &mut line_until_crlf)?; + pub fn read_until_crlf(&mut self) -> std::io::Result> { + let mut line_until_crlf = Vec::new(); + self.buffer_controller + .read_until_slice(b"\r\n", &mut line_until_crlf)?; - Ok(line_until_crlf) - } + Ok(line_until_crlf) + } - pub fn read_exact(&mut self, target: &mut Vec) -> std::io::Result<()> { - self.buffer_controller.read_exact(target) - } + pub fn read_exact(&mut self, target: &mut Vec) -> std::io::Result<()> { + self.buffer_controller.read_exact(target) + } } #[derive(Debug)] pub struct MockSsl; impl ServerCertVerifier for MockSsl { - fn verify_server_cert( - &self, - _end_entity: &CertificateDer<'_>, - _intermediates: &[CertificateDer<'_>], - _server_name: &ServerName<'_>, - _ocsp_response: &[u8], - _now: UnixTime, - ) -> Result { - Ok(ServerCertVerified::assertion()) - } + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp_response: &[u8], + _now: UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } - fn verify_tls12_signature( - &self, - _message: &[u8], - _cert: &CertificateDer<'_>, - _dss: &DigitallySignedStruct, - ) -> Result { - Ok(HandshakeSignatureValid::assertion()) - } + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } - fn verify_tls13_signature( - &self, - _message: &[u8], - _cert: &CertificateDer<'_>, - _dss: &DigitallySignedStruct, - ) -> Result { - Ok(HandshakeSignatureValid::assertion()) - } + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } - fn supported_verify_schemes(&self) -> Vec { - vec![ - SignatureScheme::RSA_PKCS1_SHA1, - SignatureScheme::ECDSA_SHA1_Legacy, - SignatureScheme::RSA_PKCS1_SHA256, - SignatureScheme::ECDSA_NISTP256_SHA256, - SignatureScheme::RSA_PKCS1_SHA384, - SignatureScheme::ECDSA_NISTP384_SHA384, - SignatureScheme::RSA_PKCS1_SHA512, - SignatureScheme::ECDSA_NISTP521_SHA512, - SignatureScheme::RSA_PSS_SHA256, - SignatureScheme::RSA_PSS_SHA384, - SignatureScheme::RSA_PSS_SHA512, - SignatureScheme::ED25519, - SignatureScheme::ED448, - ] - } + fn supported_verify_schemes(&self) -> Vec { + vec![ + SignatureScheme::RSA_PKCS1_SHA1, + SignatureScheme::ECDSA_SHA1_Legacy, + SignatureScheme::RSA_PKCS1_SHA256, + SignatureScheme::ECDSA_NISTP256_SHA256, + SignatureScheme::RSA_PKCS1_SHA384, + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::RSA_PKCS1_SHA512, + SignatureScheme::ECDSA_NISTP521_SHA512, + SignatureScheme::RSA_PSS_SHA256, + SignatureScheme::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA512, + SignatureScheme::ED25519, + SignatureScheme::ED448, + ] + } } diff --git a/tuta-sdk/rust/sdk/src/services/service_executor.rs b/tuta-sdk/rust/sdk/src/services/service_executor.rs index 9e851f09021..2202386345a 100644 --- a/tuta-sdk/rust/sdk/src/services/service_executor.rs +++ b/tuta-sdk/rust/sdk/src/services/service_executor.rs @@ -10,7 +10,7 @@ use crate::rest_client::{HttpMethod, RestClient, RestClientOptions}; use crate::rest_error::HttpError; use crate::services::hidden::Executor; use crate::services::{ - DeleteService, ExtraServiceParams, GetService, PostService, PutService, Service, + DeleteService, ExtraServiceParams, GetService, PostService, PutService, Service, }; use crate::type_model_provider::TypeModelProvider; use crate::{ApiCallError, HeadersProvider}; @@ -24,117 +24,117 @@ use std::sync::Arc; pub struct ResolvingServiceExecutor(ServiceExecutor); impl Deref for ResolvingServiceExecutor { - type Target = ServiceExecutor; + type Target = ServiceExecutor; - fn deref(&self) -> &Self::Target { - &self.0 - } + fn deref(&self) -> &Self::Target { + &self.0 + } } impl ResolvingServiceExecutor { - #[must_use] - pub fn new( - auth_headers_provider: Arc, - crypto_facade: Arc, - entity_facade: Arc, - instance_mapper: Arc, - json_serializer: Arc, - rest_client: Arc, - type_model_provider: Arc, - base_url: String, - ) -> Self { - Self(ServiceExecutor::new( - auth_headers_provider, - Some(crypto_facade), - entity_facade, - instance_mapper, - json_serializer, - rest_client, - type_model_provider, - base_url, - )) - } + #[must_use] + pub fn new( + auth_headers_provider: Arc, + crypto_facade: Arc, + entity_facade: Arc, + instance_mapper: Arc, + json_serializer: Arc, + rest_client: Arc, + type_model_provider: Arc, + base_url: String, + ) -> Self { + Self(ServiceExecutor::new( + auth_headers_provider, + Some(crypto_facade), + entity_facade, + instance_mapper, + json_serializer, + rest_client, + type_model_provider, + base_url, + )) + } } pub struct ServiceExecutor { - auth_headers_provider: Arc, - crypto_facade: Option>, - entity_facade: Arc, - instance_mapper: Arc, - json_serializer: Arc, - rest_client: Arc, - type_model_provider: Arc, - base_url: String, + auth_headers_provider: Arc, + crypto_facade: Option>, + entity_facade: Arc, + instance_mapper: Arc, + json_serializer: Arc, + rest_client: Arc, + type_model_provider: Arc, + base_url: String, } #[cfg_attr(test, mockall::automock)] impl ServiceExecutor { - #[must_use] - pub fn new( - auth_headers_provider: Arc, - crypto_facade: Option>, - entity_facade: Arc, - instance_mapper: Arc, - json_serializer: Arc, - rest_client: Arc, - type_model_provider: Arc, - base_url: String, - ) -> Self { - Self { - auth_headers_provider, - crypto_facade, - entity_facade, - instance_mapper, - json_serializer, - rest_client, - type_model_provider, - base_url, - } - } - - pub async fn get( - &self, - data: S::Input, - params: ExtraServiceParams, - ) -> Result - where - S: GetService, - { - S::GET(self, data, params).await - } - - pub async fn post( - &self, - data: S::Input, - params: ExtraServiceParams, - ) -> Result - where - S: PostService, - { - S::POST(self, data, params).await - } - - pub async fn put( - &self, - data: S::Input, - params: ExtraServiceParams, - ) -> Result - where - S: PutService, - { - S::PUT(self, data, params).await - } - - pub async fn delete( - &self, - data: S::Input, - params: ExtraServiceParams, - ) -> Result - where - S: DeleteService, - { - S::DELETE(self, data, params).await - } + #[must_use] + pub fn new( + auth_headers_provider: Arc, + crypto_facade: Option>, + entity_facade: Arc, + instance_mapper: Arc, + json_serializer: Arc, + rest_client: Arc, + type_model_provider: Arc, + base_url: String, + ) -> Self { + Self { + auth_headers_provider, + crypto_facade, + entity_facade, + instance_mapper, + json_serializer, + rest_client, + type_model_provider, + base_url, + } + } + + pub async fn get( + &self, + data: S::Input, + params: ExtraServiceParams, + ) -> Result + where + S: GetService, + { + S::GET(self, data, params).await + } + + pub async fn post( + &self, + data: S::Input, + params: ExtraServiceParams, + ) -> Result + where + S: PostService, + { + S::POST(self, data, params).await + } + + pub async fn put( + &self, + data: S::Input, + params: ExtraServiceParams, + ) -> Result + where + S: PutService, + { + S::PUT(self, data, params).await + } + + pub async fn delete( + &self, + data: S::Input, + params: ExtraServiceParams, + ) -> Result + where + S: DeleteService, + { + S::DELETE(self, data, params).await + } } // Needed because ResolvingServiceExecutor relies on Deref and doesn't have ServiceExecutor @@ -185,568 +185,568 @@ mockall::mock! { #[async_trait::async_trait] impl Executor for ServiceExecutor { - async fn do_request( - &self, - data: Option, - method: HttpMethod, - extra_service_params: ExtraServiceParams, - ) -> Result>, ApiCallError> - where - S: Service, - I: Entity + Serialize + Send, - { - let url = format!( - "{}/rest/{}", - if let Some(url) = extra_service_params.base_url { - url.clone() - } else { - self.base_url.clone() - }, - S::PATH, - ); - let model_version: u32 = S::VERSION; - - let body: Option> = if let Some(input_entity) = data { - let parsed_entity = self - .instance_mapper - .serialize_entity(input_entity) - .map_err(|e| { - ApiCallError::internal_with_err(e, "failed to convert to ParsedEntity") - })?; - let input_type_ref = I::type_ref(); - let type_model = self - .type_model_provider - .get_type_model(input_type_ref.app, input_type_ref.type_) - .ok_or(ApiCallError::internal(format!( - "type {:?} does not exist", - input_type_ref - )))?; - - let encrypted_parsed_entity = if type_model.is_encrypted() { - match extra_service_params.session_key { - Some(ref sk) => { - self.entity_facade - .encrypt_and_map(type_model, &parsed_entity, sk)? + async fn do_request( + &self, + data: Option, + method: HttpMethod, + extra_service_params: ExtraServiceParams, + ) -> Result>, ApiCallError> + where + S: Service, + I: Entity + Serialize + Send, + { + let url = format!( + "{}/rest/{}", + if let Some(url) = extra_service_params.base_url { + url.clone() + } else { + self.base_url.clone() + }, + S::PATH, + ); + let model_version: u32 = S::VERSION; + + let body: Option> = if let Some(input_entity) = data { + let parsed_entity = self + .instance_mapper + .serialize_entity(input_entity) + .map_err(|e| { + ApiCallError::internal_with_err(e, "failed to convert to ParsedEntity") + })?; + let input_type_ref = I::type_ref(); + let type_model = self + .type_model_provider + .get_type_model(input_type_ref.app, input_type_ref.type_) + .ok_or(ApiCallError::internal(format!( + "type {:?} does not exist", + input_type_ref + )))?; + + let encrypted_parsed_entity = if type_model.is_encrypted() { + match extra_service_params.session_key { + Some(ref sk) => { + self.entity_facade + .encrypt_and_map(type_model, &parsed_entity, sk)? }, - None => Err(ApiCallError::InternalSdkError { - error_message: format!( - "Encrypting {}/{} requires a session key!", - type_model.app, type_model.name - ), - })?, - } - } else { - parsed_entity - }; - - let raw_entity = self - .json_serializer - .serialize(&I::type_ref(), encrypted_parsed_entity)?; - let bytes = serde_json::to_vec::(&raw_entity).map_err(|e| { - ApiCallError::internal_with_err(e, "failed to serialize input to string") - })?; - Some(bytes) - } else { - None - }; - - let mut headers = self.auth_headers_provider.provide_headers(model_version); - if let Some(extra_headers) = extra_service_params.extra_headers { - headers.extend(extra_headers); - } - if body.is_some() { - headers.insert("Content-Type".to_owned(), "application/json".to_owned()); - } - - let response = self - .rest_client - .request_binary(url, method, RestClientOptions { body, headers }) - .await?; - let precondition = response.headers.get("precondition"); - match response.status { - 200 | 201 => Ok(response.body), - _ => Err(ApiCallError::ServerResponseError { - source: HttpError::from_http_response(response.status, precondition)?, - }), - } - } - - async fn handle_response( - &self, - body: Option>, - ) -> Result - where - OutputType: Entity + Deserialize<'static>, - { - let response_bytes = body.expect("no body"); - let response_entity = serde_json::from_slice::(response_bytes.as_slice()) - .map_err(|e| ApiCallError::internal_with_err(e, "Failed to serialize instance"))?; - let output_type_ref = &OutputType::type_ref(); - let mut parsed_entity = self - .json_serializer - .parse(output_type_ref, response_entity)?; - let type_model: &TypeModel = self - .type_model_provider - .get_type_model(output_type_ref.app, output_type_ref.type_) - .expect("invalid type ref!"); - - if type_model.marked_encrypted() { - let session_key = self - .crypto_facade - .as_ref() - .ok_or_else(|| { - ApiCallError::internal(format!( - "got encrypted response, but cannot resolve session keys yet: {}", - type_model.name, - )) - })? - .resolve_session_key(&mut parsed_entity, type_model) - .await - .map_err(|error| { - ApiCallError::internal(format!( - "Failed to resolve session key for service response '{}'; {}", - type_model.name, error - )) - })? - // `resolve_session_key()` only returns none if the entity is unencrypted, so - // no need to handle it - .expect("encrypted entity should resolve a session key"); - - let decrypted_entity = - self.entity_facade - .decrypt_and_map(type_model, parsed_entity, session_key)?; - let typed_entity = self - .instance_mapper - .parse_entity::(decrypted_entity) - .map_err(|e| { - ApiCallError::internal_with_err( - e, - "Failed to parse encrypted entity into proper types", - ) - })?; - Ok(typed_entity) - } else { - let typed_entity = self - .instance_mapper - .parse_entity::(parsed_entity) - .map_err(|error| { - ApiCallError::internal_with_err( - error, - "Failed to parse unencrypted entity into proper types", - ) - })?; - Ok(typed_entity) - } - } + None => Err(ApiCallError::InternalSdkError { + error_message: format!( + "Encrypting {}/{} requires a session key!", + type_model.app, type_model.name + ), + })?, + } + } else { + parsed_entity + }; + + let raw_entity = self + .json_serializer + .serialize(&I::type_ref(), encrypted_parsed_entity)?; + let bytes = serde_json::to_vec::(&raw_entity).map_err(|e| { + ApiCallError::internal_with_err(e, "failed to serialize input to string") + })?; + Some(bytes) + } else { + None + }; + + let mut headers = self.auth_headers_provider.provide_headers(model_version); + if let Some(extra_headers) = extra_service_params.extra_headers { + headers.extend(extra_headers); + } + if body.is_some() { + headers.insert("Content-Type".to_owned(), "application/json".to_owned()); + } + + let response = self + .rest_client + .request_binary(url, method, RestClientOptions { body, headers }) + .await?; + let precondition = response.headers.get("precondition"); + match response.status { + 200 | 201 => Ok(response.body), + _ => Err(ApiCallError::ServerResponseError { + source: HttpError::from_http_response(response.status, precondition)?, + }), + } + } + + async fn handle_response( + &self, + body: Option>, + ) -> Result + where + OutputType: Entity + Deserialize<'static>, + { + let response_bytes = body.expect("no body"); + let response_entity = serde_json::from_slice::(response_bytes.as_slice()) + .map_err(|e| ApiCallError::internal_with_err(e, "Failed to serialize instance"))?; + let output_type_ref = &OutputType::type_ref(); + let mut parsed_entity = self + .json_serializer + .parse(output_type_ref, response_entity)?; + let type_model: &TypeModel = self + .type_model_provider + .get_type_model(output_type_ref.app, output_type_ref.type_) + .expect("invalid type ref!"); + + if type_model.marked_encrypted() { + let session_key = self + .crypto_facade + .as_ref() + .ok_or_else(|| { + ApiCallError::internal(format!( + "got encrypted response, but cannot resolve session keys yet: {}", + type_model.name, + )) + })? + .resolve_session_key(&mut parsed_entity, type_model) + .await + .map_err(|error| { + ApiCallError::internal(format!( + "Failed to resolve session key for service response '{}'; {}", + type_model.name, error + )) + })? + // `resolve_session_key()` only returns none if the entity is unencrypted, so + // no need to handle it + .expect("encrypted entity should resolve a session key"); + + let decrypted_entity = + self.entity_facade + .decrypt_and_map(type_model, parsed_entity, session_key)?; + let typed_entity = self + .instance_mapper + .parse_entity::(decrypted_entity) + .map_err(|e| { + ApiCallError::internal_with_err( + e, + "Failed to parse encrypted entity into proper types", + ) + })?; + Ok(typed_entity) + } else { + let typed_entity = self + .instance_mapper + .parse_entity::(parsed_entity) + .map_err(|error| { + ApiCallError::internal_with_err( + error, + "Failed to parse unencrypted entity into proper types", + ) + })?; + Ok(typed_entity) + } + } } #[cfg(test)] mod tests { - #[mockall_double::double] - use crate::crypto::crypto_facade::CryptoFacade; - use crate::crypto::crypto_facade::ResolvedSessionKey; - use crate::crypto::key::GenericAesKey; - use crate::crypto::AES_256_KEY_SIZE; - use crate::date::DateTime; - use crate::element_value::ElementValue; - use crate::entities::entity_facade::MockEntityFacade; - use crate::instance_mapper::InstanceMapper; - use crate::json_element::RawEntity; - use crate::json_serializer::JsonSerializer; - use crate::rest_client::{HttpMethod, MockRestClient, RestResponse}; - use crate::services::service_executor::ResolvingServiceExecutor; - use crate::services::test_services::{ - HelloEncInput, HelloEncOutput, HelloEncryptedService, HelloUnEncInput, HelloUnEncOutput, - HelloUnEncryptedService, APP_VERSION_STR, - }; - use crate::services::{test_services, ExtraServiceParams}; - use crate::type_model_provider::TypeModelProvider; - use crate::{HeadersProvider, CLIENT_VERSION}; - use base64::prelude::BASE64_STANDARD; - use base64::Engine; - use std::collections::HashMap; - use std::sync::Arc; - - #[tokio::test] - pub async fn post_should_map_unencrypted_data_and_response() { - let hello_input_data = HelloUnEncInput { - message: "Something".to_string(), - }; - let executor = maps_unencrypted_data_and_response(HttpMethod::POST); - let result = executor - .post::(hello_input_data, ExtraServiceParams::default()) - .await; - - assert_eq!( - Ok(HelloUnEncOutput { - timestamp: DateTime::from_millis(3000), - answer: "Response to some request".to_string(), - }), - result - ); - } - - #[tokio::test] - pub async fn put_should_map_unencrypted_data_and_response() { - let hello_input_data = HelloUnEncInput { - message: "Something".to_string(), - }; - let executor = maps_unencrypted_data_and_response(HttpMethod::PUT); - let result = executor - .put::(hello_input_data, ExtraServiceParams::default()) - .await; - - assert_eq!( - Ok(HelloUnEncOutput { - timestamp: DateTime::from_millis(3000), - answer: "Response to some request".to_string(), - }), - result - ); - } - - #[tokio::test] - pub async fn get_should_map_unencrypted_data_and_response() { - let hello_input_data = HelloUnEncInput { - message: "Something".to_string(), - }; - let executor = maps_unencrypted_data_and_response(HttpMethod::GET); - let result = executor - .get::(hello_input_data, ExtraServiceParams::default()) - .await; - - assert_eq!( - Ok(HelloUnEncOutput { - timestamp: DateTime::from_millis(3000), - answer: "Response to some request".to_string(), - }), - result - ); - } - - #[tokio::test] - pub async fn delete_should_map_unencrypted_data_and_response() { - let hello_input_data = HelloUnEncInput { - message: "Something".to_string(), - }; - let executor = maps_unencrypted_data_and_response(HttpMethod::DELETE); - let result = executor - .delete::(hello_input_data, ExtraServiceParams::default()) - .await; - - assert_eq!( - Ok(HelloUnEncOutput { - timestamp: DateTime::from_millis(3000), - answer: "Response to some request".to_string(), - }), - result - ); - } - - #[tokio::test] - pub async fn post_should_decrypt_map_encrypted_data_and_response() { - let session_key = - GenericAesKey::from_bytes(&rand::random::<[u8; AES_256_KEY_SIZE]>()).unwrap(); - let executor = maps_encrypted_data_and_response_data(HttpMethod::POST, session_key.clone()); - - let params = ExtraServiceParams { - session_key: Some(session_key.clone()), - ..ExtraServiceParams::default() - }; - let input_entity = HelloEncInput { - message: "my encrypted request".to_string(), - }; - - let result = executor - .post::(input_entity, params) - .await; - assert_eq!( - Ok(HelloEncOutput { - answer: "my secret response".to_string(), - timestamp: DateTime::from_millis(3000), - _finalIvs: HashMap::new() - }), - result - ); - } - - #[tokio::test] - pub async fn put_should_decrypt_map_encrypted_data_and_response() { - let session_key = - GenericAesKey::from_bytes(&rand::random::<[u8; AES_256_KEY_SIZE]>()).unwrap(); - let executor = maps_encrypted_data_and_response_data(HttpMethod::PUT, session_key.clone()); - - let params = ExtraServiceParams { - session_key: Some(session_key.clone()), - ..ExtraServiceParams::default() - }; - let input_entity = HelloEncInput { - message: "my encrypted request".to_string(), - }; - - let result = executor - .put::(input_entity, params) - .await; - assert_eq!( - Ok(HelloEncOutput { - answer: "my secret response".to_string(), - timestamp: DateTime::from_millis(3000), - _finalIvs: HashMap::new() - }), - result - ); - } - - #[tokio::test] - pub async fn get_should_decrypt_map_encrypted_data_and_response() { - let session_key = - GenericAesKey::from_bytes(&rand::random::<[u8; AES_256_KEY_SIZE]>()).unwrap(); - let executor = maps_encrypted_data_and_response_data(HttpMethod::GET, session_key.clone()); - - let params = ExtraServiceParams { - session_key: Some(session_key.clone()), - ..ExtraServiceParams::default() - }; - let input_entity = HelloEncInput { - message: "my encrypted request".to_string(), - }; - - let result = executor - .get::(input_entity, params) - .await; - assert_eq!( - Ok(HelloEncOutput { - answer: "my secret response".to_string(), - timestamp: DateTime::from_millis(3000), - _finalIvs: HashMap::new() - }), - result - ); - } - - #[tokio::test] - pub async fn delete_should_decrypt_map_encrypted_data_and_response() { - let session_key = - GenericAesKey::from_bytes(&rand::random::<[u8; AES_256_KEY_SIZE]>()).unwrap(); - let executor = - maps_encrypted_data_and_response_data(HttpMethod::DELETE, session_key.clone()); - - let params = ExtraServiceParams { - session_key: Some(session_key.clone()), - ..ExtraServiceParams::default() - }; - let input_entity = HelloEncInput { - message: "my encrypted request".to_string(), - }; - - let result = executor - .delete::(input_entity, params) - .await; - assert_eq!( - Ok(HelloEncOutput { - answer: "my secret response".to_string(), - timestamp: DateTime::from_millis(3000), - _finalIvs: HashMap::new() - }), - result - ); - } - - fn setup() -> ResolvingServiceExecutor { - let mut model_provider_map = HashMap::new(); - test_services::extend_model_resolver(&mut model_provider_map); - let type_model_provider: Arc = - Arc::new(TypeModelProvider::new(model_provider_map)); - - let crypto_facade = Arc::new(CryptoFacade::default()); - let entity_facade = Arc::new(MockEntityFacade::default()); - let auth_headers_provider = - Arc::new(HeadersProvider::new(Some("access_token".to_string()))); - let instance_mapper = Arc::new(InstanceMapper::new()); - let json_serializer = Arc::new(JsonSerializer::new(type_model_provider.clone())); - let rest_client = Arc::new(MockRestClient::new()); - - ResolvingServiceExecutor::new( - auth_headers_provider, - crypto_facade, - entity_facade, - instance_mapper, - json_serializer, - rest_client, - type_model_provider.clone(), - "http://api.tuta.com".to_string(), - ) - } - - fn maps_unencrypted_data_and_response(http_method: HttpMethod) -> ResolvingServiceExecutor { - let executor = setup(); - let rest_client; - let entity_facade; - unsafe { - rest_client = Arc::as_ptr(&executor.rest_client) - .cast::() - .cast_mut() - .as_mut() - .unwrap(); - entity_facade = Arc::as_ptr(&executor.entity_facade) - .cast::() - .cast_mut() - .as_mut() - .unwrap(); - } - - entity_facade.expect_encrypt_and_map().never(); - rest_client - .expect_request_binary() - .return_once(move |url, method, opts| { - assert_eq!( - "http://api.tuta.com/rest/test/unencrypted-hello", - url.as_str() - ); - assert_eq!(http_method, method); - - let expected_headers = [ - ("v", APP_VERSION_STR), - ("accessToken", "access_token"), - ("Content-Type", "application/json"), - ("cv", CLIENT_VERSION), - ] - .into_iter() - .map(|(a, b)| (a.to_string(), b.to_string())) - .collect::>(); - assert_eq!(expected_headers, opts.headers); - let expected_body = - serde_json::from_str::(r#"{"message":"Something"}"#).unwrap(); - let body = - serde_json::from_slice::(opts.body.unwrap().as_slice()).unwrap(); - assert_eq!(expected_body, body); - - Ok(RestResponse { - status: 200, - headers: HashMap::new(), - body: Some( - br#"{"answer":"Response to some request","timestamp":"3000"}"#.to_vec(), - ), - }) - }); - - executor - } - - pub fn maps_encrypted_data_and_response_data( - http_method: HttpMethod, - session_key: GenericAesKey, - ) -> ResolvingServiceExecutor { - let executor = setup(); - let crypto_facade; - let rest_client; - let entity_facade; - unsafe { - crypto_facade = Arc::as_ptr(executor.crypto_facade.as_ref().unwrap()) - .cast::() - .cast_mut() - .as_mut() - .unwrap(); - rest_client = Arc::as_ptr(&executor.rest_client) - .cast::() - .cast_mut() - .as_mut() - .unwrap(); - - entity_facade = Arc::as_ptr(&executor.entity_facade) - .cast::() - .cast_mut() - .as_mut() - .unwrap(); - } - let owner_enc_session_key = [rand::random(); 32].to_vec(); - - rest_client - .expect_request_binary() - .return_once(move |url, method, opts| { - assert_eq!( - "http://api.tuta.com/rest/test/encrypted-hello", - url.as_str() - ); - assert_eq!(http_method, method); - let expected_body = - serde_json::from_str::(r#"{"message": "my encrypted request"}"#) - .unwrap(); - let body = - serde_json::from_slice::(opts.body.unwrap().as_slice()).unwrap(); - assert_eq!(expected_body, body); - let expected_headers = [ - ("accessToken", "access_token"), - ("cv", CLIENT_VERSION), - ("Content-Type", "application/json"), - ("v", APP_VERSION_STR), - ] - .into_iter() - .map(|(a, b)| (a.to_string(), b.to_string())) - .collect::>(); - assert_eq!(expected_headers, opts.headers); - Ok(RestResponse { - status: 200, - headers: HashMap::new(), - body: Some( - br#"{ "answer":"bXkgc2VjcmV0IHJlc3BvbnNl","timestamp":"MzAwMA==" }"# - .to_vec(), - ), - }) - }); - - let session_key_clone = session_key.clone(); - crypto_facade - .expect_resolve_session_key() - .returning(move |_entity, model| { - assert_eq!(("test", "HelloEncOutput"), (model.app, model.name)); - assert!(model.marked_encrypted()); - - Ok(Some(ResolvedSessionKey { - session_key: session_key_clone.clone(), - owner_enc_session_key: owner_enc_session_key.clone(), - })) - }); - - let session_key_clone = session_key.clone(); - entity_facade - .expect_encrypt_and_map() - .return_once(move |_, instance, sk| { - assert_eq!(&session_key_clone, sk); - Ok(instance.clone()) - }); - - let session_key_clone = session_key.clone(); - entity_facade.expect_decrypt_and_map().return_once( - move |_, mut entity, resolved_session_key| { - assert_eq!(session_key_clone, resolved_session_key.session_key); - assert_eq!( - &ElementValue::Bytes(BASE64_STANDARD.decode(r#"MzAwMA=="#).unwrap()), - entity.get("timestamp").unwrap() - ); - assert_eq!( - &ElementValue::Bytes( - BASE64_STANDARD - .decode(r#"bXkgc2VjcmV0IHJlc3BvbnNl"#) - .unwrap() - ), - entity.get("answer").unwrap() - ); - - entity.insert( - "answer".to_string(), - ElementValue::String(String::from("my secret response")), - ); - entity.insert( - "timestamp".to_string(), - ElementValue::Date(DateTime::from_millis(3000)), - ); - entity.insert("_finalIvs".to_string(), ElementValue::Dict(HashMap::new())); - Ok(entity.clone()) - }, - ); - - executor - } + #[mockall_double::double] + use crate::crypto::crypto_facade::CryptoFacade; + use crate::crypto::crypto_facade::ResolvedSessionKey; + use crate::crypto::key::GenericAesKey; + use crate::crypto::AES_256_KEY_SIZE; + use crate::date::DateTime; + use crate::element_value::ElementValue; + use crate::entities::entity_facade::MockEntityFacade; + use crate::instance_mapper::InstanceMapper; + use crate::json_element::RawEntity; + use crate::json_serializer::JsonSerializer; + use crate::rest_client::{HttpMethod, MockRestClient, RestResponse}; + use crate::services::service_executor::ResolvingServiceExecutor; + use crate::services::test_services::{ + HelloEncInput, HelloEncOutput, HelloEncryptedService, HelloUnEncInput, HelloUnEncOutput, + HelloUnEncryptedService, APP_VERSION_STR, + }; + use crate::services::{test_services, ExtraServiceParams}; + use crate::type_model_provider::TypeModelProvider; + use crate::{HeadersProvider, CLIENT_VERSION}; + use base64::prelude::BASE64_STANDARD; + use base64::Engine; + use std::collections::HashMap; + use std::sync::Arc; + + #[tokio::test] + pub async fn post_should_map_unencrypted_data_and_response() { + let hello_input_data = HelloUnEncInput { + message: "Something".to_string(), + }; + let executor = maps_unencrypted_data_and_response(HttpMethod::POST); + let result = executor + .post::(hello_input_data, ExtraServiceParams::default()) + .await; + + assert_eq!( + Ok(HelloUnEncOutput { + timestamp: DateTime::from_millis(3000), + answer: "Response to some request".to_string(), + }), + result + ); + } + + #[tokio::test] + pub async fn put_should_map_unencrypted_data_and_response() { + let hello_input_data = HelloUnEncInput { + message: "Something".to_string(), + }; + let executor = maps_unencrypted_data_and_response(HttpMethod::PUT); + let result = executor + .put::(hello_input_data, ExtraServiceParams::default()) + .await; + + assert_eq!( + Ok(HelloUnEncOutput { + timestamp: DateTime::from_millis(3000), + answer: "Response to some request".to_string(), + }), + result + ); + } + + #[tokio::test] + pub async fn get_should_map_unencrypted_data_and_response() { + let hello_input_data = HelloUnEncInput { + message: "Something".to_string(), + }; + let executor = maps_unencrypted_data_and_response(HttpMethod::GET); + let result = executor + .get::(hello_input_data, ExtraServiceParams::default()) + .await; + + assert_eq!( + Ok(HelloUnEncOutput { + timestamp: DateTime::from_millis(3000), + answer: "Response to some request".to_string(), + }), + result + ); + } + + #[tokio::test] + pub async fn delete_should_map_unencrypted_data_and_response() { + let hello_input_data = HelloUnEncInput { + message: "Something".to_string(), + }; + let executor = maps_unencrypted_data_and_response(HttpMethod::DELETE); + let result = executor + .delete::(hello_input_data, ExtraServiceParams::default()) + .await; + + assert_eq!( + Ok(HelloUnEncOutput { + timestamp: DateTime::from_millis(3000), + answer: "Response to some request".to_string(), + }), + result + ); + } + + #[tokio::test] + pub async fn post_should_decrypt_map_encrypted_data_and_response() { + let session_key = + GenericAesKey::from_bytes(&rand::random::<[u8; AES_256_KEY_SIZE]>()).unwrap(); + let executor = maps_encrypted_data_and_response_data(HttpMethod::POST, session_key.clone()); + + let params = ExtraServiceParams { + session_key: Some(session_key.clone()), + ..ExtraServiceParams::default() + }; + let input_entity = HelloEncInput { + message: "my encrypted request".to_string(), + }; + + let result = executor + .post::(input_entity, params) + .await; + assert_eq!( + Ok(HelloEncOutput { + answer: "my secret response".to_string(), + timestamp: DateTime::from_millis(3000), + _finalIvs: HashMap::new() + }), + result + ); + } + + #[tokio::test] + pub async fn put_should_decrypt_map_encrypted_data_and_response() { + let session_key = + GenericAesKey::from_bytes(&rand::random::<[u8; AES_256_KEY_SIZE]>()).unwrap(); + let executor = maps_encrypted_data_and_response_data(HttpMethod::PUT, session_key.clone()); + + let params = ExtraServiceParams { + session_key: Some(session_key.clone()), + ..ExtraServiceParams::default() + }; + let input_entity = HelloEncInput { + message: "my encrypted request".to_string(), + }; + + let result = executor + .put::(input_entity, params) + .await; + assert_eq!( + Ok(HelloEncOutput { + answer: "my secret response".to_string(), + timestamp: DateTime::from_millis(3000), + _finalIvs: HashMap::new() + }), + result + ); + } + + #[tokio::test] + pub async fn get_should_decrypt_map_encrypted_data_and_response() { + let session_key = + GenericAesKey::from_bytes(&rand::random::<[u8; AES_256_KEY_SIZE]>()).unwrap(); + let executor = maps_encrypted_data_and_response_data(HttpMethod::GET, session_key.clone()); + + let params = ExtraServiceParams { + session_key: Some(session_key.clone()), + ..ExtraServiceParams::default() + }; + let input_entity = HelloEncInput { + message: "my encrypted request".to_string(), + }; + + let result = executor + .get::(input_entity, params) + .await; + assert_eq!( + Ok(HelloEncOutput { + answer: "my secret response".to_string(), + timestamp: DateTime::from_millis(3000), + _finalIvs: HashMap::new() + }), + result + ); + } + + #[tokio::test] + pub async fn delete_should_decrypt_map_encrypted_data_and_response() { + let session_key = + GenericAesKey::from_bytes(&rand::random::<[u8; AES_256_KEY_SIZE]>()).unwrap(); + let executor = + maps_encrypted_data_and_response_data(HttpMethod::DELETE, session_key.clone()); + + let params = ExtraServiceParams { + session_key: Some(session_key.clone()), + ..ExtraServiceParams::default() + }; + let input_entity = HelloEncInput { + message: "my encrypted request".to_string(), + }; + + let result = executor + .delete::(input_entity, params) + .await; + assert_eq!( + Ok(HelloEncOutput { + answer: "my secret response".to_string(), + timestamp: DateTime::from_millis(3000), + _finalIvs: HashMap::new() + }), + result + ); + } + + fn setup() -> ResolvingServiceExecutor { + let mut model_provider_map = HashMap::new(); + test_services::extend_model_resolver(&mut model_provider_map); + let type_model_provider: Arc = + Arc::new(TypeModelProvider::new(model_provider_map)); + + let crypto_facade = Arc::new(CryptoFacade::default()); + let entity_facade = Arc::new(MockEntityFacade::default()); + let auth_headers_provider = + Arc::new(HeadersProvider::new(Some("access_token".to_string()))); + let instance_mapper = Arc::new(InstanceMapper::new()); + let json_serializer = Arc::new(JsonSerializer::new(type_model_provider.clone())); + let rest_client = Arc::new(MockRestClient::new()); + + ResolvingServiceExecutor::new( + auth_headers_provider, + crypto_facade, + entity_facade, + instance_mapper, + json_serializer, + rest_client, + type_model_provider.clone(), + "http://api.tuta.com".to_string(), + ) + } + + fn maps_unencrypted_data_and_response(http_method: HttpMethod) -> ResolvingServiceExecutor { + let executor = setup(); + let rest_client; + let entity_facade; + unsafe { + rest_client = Arc::as_ptr(&executor.rest_client) + .cast::() + .cast_mut() + .as_mut() + .unwrap(); + entity_facade = Arc::as_ptr(&executor.entity_facade) + .cast::() + .cast_mut() + .as_mut() + .unwrap(); + } + + entity_facade.expect_encrypt_and_map().never(); + rest_client + .expect_request_binary() + .return_once(move |url, method, opts| { + assert_eq!( + "http://api.tuta.com/rest/test/unencrypted-hello", + url.as_str() + ); + assert_eq!(http_method, method); + + let expected_headers = [ + ("v", APP_VERSION_STR), + ("accessToken", "access_token"), + ("Content-Type", "application/json"), + ("cv", CLIENT_VERSION), + ] + .into_iter() + .map(|(a, b)| (a.to_string(), b.to_string())) + .collect::>(); + assert_eq!(expected_headers, opts.headers); + let expected_body = + serde_json::from_str::(r#"{"message":"Something"}"#).unwrap(); + let body = + serde_json::from_slice::(opts.body.unwrap().as_slice()).unwrap(); + assert_eq!(expected_body, body); + + Ok(RestResponse { + status: 200, + headers: HashMap::new(), + body: Some( + br#"{"answer":"Response to some request","timestamp":"3000"}"#.to_vec(), + ), + }) + }); + + executor + } + + pub fn maps_encrypted_data_and_response_data( + http_method: HttpMethod, + session_key: GenericAesKey, + ) -> ResolvingServiceExecutor { + let executor = setup(); + let crypto_facade; + let rest_client; + let entity_facade; + unsafe { + crypto_facade = Arc::as_ptr(executor.crypto_facade.as_ref().unwrap()) + .cast::() + .cast_mut() + .as_mut() + .unwrap(); + rest_client = Arc::as_ptr(&executor.rest_client) + .cast::() + .cast_mut() + .as_mut() + .unwrap(); + + entity_facade = Arc::as_ptr(&executor.entity_facade) + .cast::() + .cast_mut() + .as_mut() + .unwrap(); + } + let owner_enc_session_key = [rand::random(); 32].to_vec(); + + rest_client + .expect_request_binary() + .return_once(move |url, method, opts| { + assert_eq!( + "http://api.tuta.com/rest/test/encrypted-hello", + url.as_str() + ); + assert_eq!(http_method, method); + let expected_body = + serde_json::from_str::(r#"{"message": "my encrypted request"}"#) + .unwrap(); + let body = + serde_json::from_slice::(opts.body.unwrap().as_slice()).unwrap(); + assert_eq!(expected_body, body); + let expected_headers = [ + ("accessToken", "access_token"), + ("cv", CLIENT_VERSION), + ("Content-Type", "application/json"), + ("v", APP_VERSION_STR), + ] + .into_iter() + .map(|(a, b)| (a.to_string(), b.to_string())) + .collect::>(); + assert_eq!(expected_headers, opts.headers); + Ok(RestResponse { + status: 200, + headers: HashMap::new(), + body: Some( + br#"{ "answer":"bXkgc2VjcmV0IHJlc3BvbnNl","timestamp":"MzAwMA==" }"# + .to_vec(), + ), + }) + }); + + let session_key_clone = session_key.clone(); + crypto_facade + .expect_resolve_session_key() + .returning(move |_entity, model| { + assert_eq!(("test", "HelloEncOutput"), (model.app, model.name)); + assert!(model.marked_encrypted()); + + Ok(Some(ResolvedSessionKey { + session_key: session_key_clone.clone(), + owner_enc_session_key: owner_enc_session_key.clone(), + })) + }); + + let session_key_clone = session_key.clone(); + entity_facade + .expect_encrypt_and_map() + .return_once(move |_, instance, sk| { + assert_eq!(&session_key_clone, sk); + Ok(instance.clone()) + }); + + let session_key_clone = session_key.clone(); + entity_facade.expect_decrypt_and_map().return_once( + move |_, mut entity, resolved_session_key| { + assert_eq!(session_key_clone, resolved_session_key.session_key); + assert_eq!( + &ElementValue::Bytes(BASE64_STANDARD.decode(r#"MzAwMA=="#).unwrap()), + entity.get("timestamp").unwrap() + ); + assert_eq!( + &ElementValue::Bytes( + BASE64_STANDARD + .decode(r#"bXkgc2VjcmV0IHJlc3BvbnNl"#) + .unwrap() + ), + entity.get("answer").unwrap() + ); + + entity.insert( + "answer".to_string(), + ElementValue::String(String::from("my secret response")), + ); + entity.insert( + "timestamp".to_string(), + ElementValue::Date(DateTime::from_millis(3000)), + ); + entity.insert("_finalIvs".to_string(), ElementValue::Dict(HashMap::new())); + Ok(entity.clone()) + }, + ); + + executor + } } From 94bfe144e5a6dc78f5e71d6d470265fced0a2c6c Mon Sep 17 00:00:00 2001 From: nig Date: Wed, 13 Nov 2024 11:51:28 +0100 Subject: [PATCH 16/32] enable multi mail import --- .../rust/sdk/src/entities/entity_facade.rs | 2512 +++++++++-------- .../sdk/src/entities/json_size_estimator.rs | 4 +- 2 files changed, 1263 insertions(+), 1253 deletions(-) diff --git a/tuta-sdk/rust/sdk/src/entities/entity_facade.rs b/tuta-sdk/rust/sdk/src/entities/entity_facade.rs index ea76c368334..dec73fb0688 100644 --- a/tuta-sdk/rust/sdk/src/entities/entity_facade.rs +++ b/tuta-sdk/rust/sdk/src/entities/entity_facade.rs @@ -6,12 +6,12 @@ use crate::date::DateTime; use crate::element_value::{ElementValue, ParsedEntity}; use crate::entities::Errors; use crate::metamodel::{ - AssociationType, Cardinality, ElementType, ModelAssociation, ModelValue, TypeModel, ValueType, + AssociationType, Cardinality, ElementType, ModelAssociation, ModelValue, TypeModel, ValueType, }; use crate::type_model_provider::TypeModelProvider; use crate::util::array_cast_slice; use crate::ApiCallError; -use base64::prelude::{BASE64_STANDARD, BASE64_URL_SAFE_NO_PAD}; +use base64::prelude::BASE64_URL_SAFE_NO_PAD; use base64::Engine; use lz4_flex::block::DecompressError; use minicbor::Encode; @@ -24,785 +24,795 @@ pub const MAX_UNCOMPRESSED_INPUT_LZ4: usize = 0x7e000000; /// Provides high level functions to handle encryption/decryption of entities #[derive(uniffi::Object)] pub struct EntityFacadeImpl { - type_model_provider: Arc, - randomizer_facade: RandomizerFacade, + type_model_provider: Arc, + randomizer_facade: RandomizerFacade, } /// Value after it has been processed #[cfg_attr(test, derive(Debug, PartialEq))] struct MappedValue { - /// The actual decrypted value that will be written to the field - value: ElementValue, - /// IV that was used for encryption or empty value if the field was default-encrypted (empty) - iv: Option>, - /// Expected encryption errors - error: Option, + /// The actual decrypted value that will be written to the field + value: ElementValue, + /// IV that was used for encryption or empty value if the field was default-encrypted (empty) + iv: Option>, + /// Expected encryption errors + error: Option, } #[cfg_attr(test, mockall::automock)] pub trait EntityFacade: Send + Sync { - fn decrypt_and_map( - &self, - type_model: &TypeModel, - entity: ParsedEntity, - resolved_session_key: ResolvedSessionKey, - ) -> Result; - - fn encrypt_and_map( - &self, - type_model: &TypeModel, - instance: &ParsedEntity, - sk: &GenericAesKey, - ) -> Result; + fn decrypt_and_map( + &self, + type_model: &TypeModel, + entity: ParsedEntity, + resolved_session_key: ResolvedSessionKey, + ) -> Result; + + fn encrypt_and_map( + &self, + type_model: &TypeModel, + instance: &ParsedEntity, + sk: &GenericAesKey, + ) -> Result; } impl EntityFacadeImpl { - #[must_use] - pub fn new( - type_model_provider: Arc, - randomizer_facade: RandomizerFacade, - ) -> Self { - EntityFacadeImpl { - type_model_provider, - randomizer_facade, - } - } - - fn should_restore_default_value( - model_value: &ModelValue, - value: &ElementValue, - instance: &ParsedEntity, - key: &str, - ) -> bool { - if model_value.encrypted { - if let Some(final_ivs) = Self::get_final_iv_for_key(instance, key) { - return final_ivs.assert_bytes().is_empty() - && value == &ValueType::get_default(&model_value.value_type); - } - } - false - } - - fn encrypt_value( - model_value: &ModelValue, - instance_value: &ElementValue, - session_key: &GenericAesKey, - iv: Iv, - ) -> Result { - let value_type = &model_value.value_type; - - let element_is_nil = >::is_nil(instance_value); - if !model_value.encrypted - || (element_is_nil && model_value.cardinality == Cardinality::ZeroOrOne) - { - Ok(instance_value.clone()) - } else if element_is_nil { - Err(ApiCallError::internal(format!( - "Nil encrypted value is not accepted. ModelValue Id: {}", - model_value.id - ))) - } else { - let bytes = Self::map_value_to_binary(value_type, instance_value); - let encrypted_data = session_key - .encrypt_data(bytes.as_slice(), iv) - .expect("Cannot encrypt data"); - Ok(ElementValue::Bytes(encrypted_data)) - } - } - - fn get_final_iv_for_key(instance: &ParsedEntity, key: &str) -> Option { - if let Some(final_ivs) = instance.get("_finalIvs") { - if let Some(array) = final_ivs.assert_dict_ref().get(key) { - return Some(ElementValue::Bytes(array.assert_bytes())); - }; - }; - None - } - - fn map_value_to_binary(value_type: &ValueType, value: &ElementValue) -> Vec { - match value_type { - ValueType::Bytes => value.assert_bytes(), - ValueType::String => value.assert_string().as_bytes().to_vec(), - ValueType::Number => value.assert_number().to_string().as_bytes().to_vec(), - ValueType::Date => value - .assert_date() - .as_millis() - .to_string() - .as_bytes() - .to_vec(), - - ValueType::Boolean => if value.assert_bool() { b"1" } else { b"0" }.to_vec(), - ValueType::GeneratedId => value.assert_generated_id().0.as_bytes().to_vec(), - ValueType::CustomId => value.assert_custom_id().0.as_bytes().to_vec(), - ValueType::CompressedString => { - Self::lz4_compress_plain_bytes(value.assert_string().as_bytes()) - // this limit should be checked before creating the instance/entity itself - // eg: client should not accept more input in mail editor once limit is reached - .expect("Unchecked input data? received too large byte for lz4_compression") - }, - } - } - - fn encrypt_and_map_inner( - &self, - type_model: &TypeModel, - instance: &ParsedEntity, - sk: &GenericAesKey, - ) -> Result { - let mut encrypted = ParsedEntity::new(); - - for (key, model_value) in &type_model.values { - let instance_value = instance.get(&key.to_string()).ok_or_else(|| { - ApiCallError::internal(format!("Can not find key: {key} in instance: {instance:?}")) - })?; - - let encrypted_value: ElementValue; - - if Self::should_restore_default_value(model_value, instance_value, instance, key) { - // restore the default encrypted value because it has not changed - // note: this branch must be checked *before* the one which reuses IVs as this one checks - // the length. - encrypted_value = ElementValue::String("".to_string()); - } else if model_value.encrypted - && model_value.is_final - && Self::get_final_iv_for_key(instance, key).is_some() - { - let final_iv = Iv::from_bytes( - Self::get_final_iv_for_key(instance, key) - .unwrap() - .assert_bytes() - .as_slice(), - ) - .map_err(|err| ApiCallError::internal(format!("iv of illegal size {:?}", err)))?; - - encrypted_value = Self::encrypt_value(model_value, instance_value, sk, final_iv)? - } else { - encrypted_value = Self::encrypt_value( - model_value, - instance_value, - sk, - Iv::generate(&self.randomizer_facade), - )? - } - encrypted.insert(key.to_string(), encrypted_value); - } - - if type_model.element_type == ElementType::Aggregated && !encrypted.contains_key("_id") { - let new_id = self.randomizer_facade.generate_random_array::<4>(); - - encrypted.insert( - String::from("_id"), - ElementValue::String(BASE64_URL_SAFE_NO_PAD.encode(BASE64_STANDARD.encode(new_id))), - ); - } - - for (association_name, association) in &type_model.associations { - let encrypted_association = match association.association_type { - AssociationType::Aggregation => { - self.encrypt_aggregate(type_model, association_name, association, instance, sk)? - }, - AssociationType::ElementAssociation - | AssociationType::ListAssociation - | AssociationType::ListElementAssociationCustom - | AssociationType::ListElementAssociationGenerated - | AssociationType::BlobElementAssociation => instance - .get(&association_name.to_string()) - .cloned() - .ok_or(ApiCallError::internal(format!( - "could not find association {association_name} on type {}", - type_model.name - )))?, - }; - encrypted.insert(association_name.to_string(), encrypted_association); - } - - Ok(encrypted) - } - - fn encrypt_aggregate( - &self, - type_model: &TypeModel, - association_name: &str, - association: &ModelAssociation, - instance: &ParsedEntity, - sk: &GenericAesKey, - ) -> Result { - let dependency = association.dependency.unwrap_or(type_model.app); - let aggregated_type_model = self - .type_model_provider - .get_type_model(dependency, association.ref_type) - .ok_or_else(|| { - ApiCallError::internal(format!( - "unknown type model: {:?}", - (dependency, association.ref_type) - )) - })?; - let instance_association = instance.get(&association_name.to_string()).unwrap(); - - match (&association.cardinality, instance_association) { - (Cardinality::ZeroOrOne, ElementValue::Null) => Ok(ElementValue::Null), - - (_, ElementValue::Null) => Err(ApiCallError::internal(format!( - "Undefined attribute {}:{association_name}", - type_model.name - ))), - - (Cardinality::Any, _) => { - let aggregates = instance_association.assert_array(); - let mut encrypted_aggregates = Vec::with_capacity(aggregates.len()); - for aggregate in &aggregates { - let parsed_entity = self.encrypt_and_map_inner( - aggregated_type_model, - &aggregate.assert_dict(), - sk, - )?; - encrypted_aggregates.push(ElementValue::Dict(parsed_entity)); - } - - Ok(ElementValue::Array(encrypted_aggregates)) - }, - - (Cardinality::One | Cardinality::ZeroOrOne, _) => { - let parsed_entity = self.encrypt_and_map_inner( - aggregated_type_model, - &instance_association.assert_dict(), - sk, - )?; - Ok(ElementValue::Dict(parsed_entity)) - }, - } - } - - fn decrypt_and_map_inner( - &self, - type_model: &TypeModel, - mut entity: ParsedEntity, - session_key: &GenericAesKey, - ) -> Result { - let mut mapped_decrypted: HashMap = Default::default(); - let mut mapped_errors: Errors = Default::default(); - let mut mapped_ivs: HashMap = Default::default(); - - for (&key, model_value) in &type_model.values { - let stored_element = entity.remove(key).unwrap_or(ElementValue::Null); - let MappedValue { value, iv, error } = - Self::decrypt_and_parse_value(stored_element, session_key, key, model_value)?; - - mapped_decrypted.insert(key.to_string(), value); - if let Some(error) = error { - mapped_errors.insert(key.to_string(), ElementValue::String(error)); - } - if let Some(iv) = iv { - mapped_ivs.insert(key.to_string(), ElementValue::Bytes(iv.clone())); - } - } - - for (&association_name, association_model) in &type_model.associations { - let association_entry = entity - .remove(association_name) - .unwrap_or(ElementValue::Null); - let (mapped_association, errors) = self.map_associations( - type_model, - association_entry, - session_key, - association_name, - association_model, - )?; - - mapped_decrypted.insert(association_name.to_string(), mapped_association); - if !errors.is_empty() { - mapped_errors.insert(association_name.to_string(), ElementValue::Dict(errors)); - } - } - - if type_model.is_encrypted() { - // Only top-level types are expected to have `_errors` in the end but it is removed - // from the aggregates by `extract_errors()`. - mapped_decrypted.insert("_errors".to_string(), ElementValue::Dict(mapped_errors)); - mapped_decrypted.insert("_finalIvs".to_string(), ElementValue::Dict(mapped_ivs)); - } - - Ok(mapped_decrypted) - } - - fn map_associations( - &self, - type_model: &TypeModel, - association_data: ElementValue, - session_key: &GenericAesKey, - association_name: &str, - association_model: &ModelAssociation, - ) -> Result<(ElementValue, Errors), ApiCallError> { - let mut errors: Errors = Default::default(); - let dependency = match association_model.dependency { - Some(dep) => dep, - None => type_model.app, - }; - - if let AssociationType::Aggregation = association_model.association_type { - let aggregate_type_model = self - .type_model_provider - .get_type_model(dependency, association_model.ref_type) - .unwrap_or_else(|| panic!("Undefined type_model {}", association_model.ref_type)); - - match (association_data, association_model.cardinality.borrow()) { - (ElementValue::Null, Cardinality::ZeroOrOne) => Ok((ElementValue::Null, errors)), - (ElementValue::Null, Cardinality::One) => Err(ApiCallError::InternalSdkError { - error_message: format!( - "Value {association_name} with cardinality ONE can't be null" - ), - }), - (ElementValue::Array(arr), Cardinality::Any) => { - let mut aggregate_vec: Vec = Vec::with_capacity(arr.len()); - for (index, aggregate) in arr.into_iter().enumerate() { - match aggregate { - ElementValue::Dict(entity) => { - let mut decrypted_aggregate = self.decrypt_and_map_inner( - aggregate_type_model, - entity, - session_key, - )?; - - // Errors should be grouped inside the top-most object, so they should be - // extracted and removed from aggregates - if decrypted_aggregate.contains_key("_errors") { - let error_key = &format!("{}_{}", association_name, index); - self.extract_errors( - error_key, - &mut errors, - &mut decrypted_aggregate, - ); - } - - aggregate_vec.push(ElementValue::Dict(decrypted_aggregate)); - }, - _ => { - return Err(ApiCallError::InternalSdkError { - error_message: format!( - "Invalid aggregate format. {} isn't a dict", - association_name - ), - }) - }, - } - } - - Ok((ElementValue::Array(aggregate_vec), errors)) - }, - (ElementValue::Dict(dict), Cardinality::One | Cardinality::ZeroOrOne) => { - let decrypted_aggregate = - self.decrypt_and_map_inner(aggregate_type_model, dict, session_key); - match decrypted_aggregate { - Ok(mut dec_aggregate) => { - self.extract_errors(association_name, &mut errors, &mut dec_aggregate); - Ok((ElementValue::Dict(dec_aggregate), errors)) - }, - Err(_) => Err(ApiCallError::InternalSdkError { - error_message: format!( - "Failed to decrypt association {association_name}" - ), - }), - } - }, - _ => Err(ApiCallError::InternalSdkError { - error_message: format!("Invalid association {association_name}"), - }), - } - } else { - Ok((association_data, errors)) - } - } - fn extract_errors( - &self, - association_name: &str, - errors: &mut Errors, - dec_aggregate: &mut ParsedEntity, - ) { - if let Some(ElementValue::Dict(err_dict)) = dec_aggregate.remove("_errors") { - if !err_dict.is_empty() { - errors.insert(association_name.to_string(), ElementValue::Dict(err_dict)); - } - } - } - fn decrypt_and_parse_value( - value: ElementValue, - session_key: &GenericAesKey, - key: &str, - model_value: &ModelValue, - ) -> Result { - match (&model_value.cardinality, &model_value.encrypted, value) { - (Cardinality::One | Cardinality::ZeroOrOne, true, ElementValue::String(s)) - if s.is_empty() => - { - // If the value is default-encrypted (empty string) then return default value and - // empty IV. When re-encrypting we should put the empty value back to not increase - // used storage. - let value = model_value.value_type.get_default(); - Ok(MappedValue { - value, - iv: Some(Vec::new()), - error: None, - }) - }, - (Cardinality::ZeroOrOne, _, ElementValue::Null) => { - // If it's null, and it's permissible then we keep it as such - Ok(MappedValue { - value: ElementValue::Null, - iv: None, - error: None, - }) - }, - (Cardinality::One | Cardinality::ZeroOrOne, true, ElementValue::Bytes(bytes)) => { - // If it's a proper encrypted value then we need to decrypt it, parse it and - // possibly record the IV. - let PlaintextAndIv { - data: plaintext, - iv, - } = session_key - .decrypt_data_and_iv(bytes.as_slice()) - .map_err(|e| ApiCallError::InternalSdkError { - error_message: e.to_string(), - })?; - - match Self::parse_decrypted_value(model_value.value_type.clone(), plaintext) { - Ok(value) => { - // We want to ensure we use the same IV for final encrypted values, as this - // will guarantee we get the same value back when we encrypt it. - let iv = if model_value.is_final { - Some(iv.to_vec()) - } else { - None - }; - Ok(MappedValue { - value, - iv, - error: None, - }) - }, - Err(err) => Ok(MappedValue { - value: model_value.value_type.get_default(), - iv: None, - error: Some(format!("Failed to decrypt {key}. {err}")), - }), - } - }, - (Cardinality::One | Cardinality::ZeroOrOne, false, value) => Ok(MappedValue { - value, - iv: None, - error: None, - }), - _ => Err(ApiCallError::internal(format!( - "Invalid value/cardinality combination for key `{key}`" - ))), - } - } - fn parse_decrypted_value( - value_type: ValueType, - bytes: Vec, - ) -> Result { - match value_type { - ValueType::String => { - let string = String::from_utf8(bytes) - .map_err(|e| ApiCallError::internal_with_err(e, "Invalid string"))?; - Ok(ElementValue::String(string)) - }, - ValueType::Number => { - if bytes.is_empty() { - Ok(ElementValue::Null) - } else { - // Encrypted numbers are encrypted strings. - let string = String::from_utf8(bytes) - .map_err(|e| ApiCallError::internal_with_err(e, "Invalid number string"))?; - let number = string - .parse() - .map_err(|e| ApiCallError::internal_with_err(e, "Invalid number"))?; - - Ok(ElementValue::Number(number)) - } - }, - ValueType::Bytes => Ok(ElementValue::Bytes(bytes.clone())), - ValueType::Date => { - let bytes = array_cast_slice(bytes.as_slice(), "u64") - .map_err(|e| ApiCallError::internal_with_err(e, "Invalid date bytes"))?; - Ok(ElementValue::Date(DateTime::from_millis( - u64::from_be_bytes(bytes), - ))) - }, - ValueType::Boolean => match bytes.as_slice() { - b"1" => Ok(ElementValue::Bool(true)), - b"0" => Ok(ElementValue::Bool(false)), - _ => Err(ApiCallError::InternalSdkError { - error_message: "Failed to parse boolean bytes".to_owned(), - }), - }, - ValueType::CompressedString => { - let uncompressed_bytes = Self::lz4_decompress_decrypted_bytes(bytes.as_slice()) - .map_err(|e| { - ApiCallError::internal_with_err(e, "Cannot decompress compressed string") - })?; - - let uncompressed_string = String::from_utf8(uncompressed_bytes).map_err(|e| { - ApiCallError::internal_with_err( - e, - "Invalid utf-8 character in uncompressed string", - ) - })?; - - Ok(ElementValue::String(uncompressed_string)) - }, - - ValueType::GeneratedId | ValueType::CustomId => { - unreachable!("Cannot convert {value_type:?} to ElementValue"); - }, - } - } - - #[must_use] - pub fn lz4_compress_plain_bytes(bytes: &[u8]) -> Option> { - if bytes.is_empty() { - Some(Vec::new()) - } else if bytes.len() <= MAX_UNCOMPRESSED_INPUT_LZ4 { - Some(lz4_flex::compress(bytes)) - } else { - None - } - } - - pub fn lz4_decompress_decrypted_bytes( - compressed_bytes: &[u8], - ) -> Result, DecompressError> { - if compressed_bytes.is_empty() { - return Ok(Vec::new()); - } - - // since we don't store the uncompressed size we have to guess how much memory we might - // need. - // 12 times the compressed size should work for almost all cases. - let mut uncompressed_bytes = lz4_flex::decompress(compressed_bytes, compressed_bytes.len()); - while let Err(DecompressError::OutputTooSmall { - actual, - expected: _, - }) = uncompressed_bytes - { - uncompressed_bytes = lz4_flex::decompress(compressed_bytes, actual * 2); - } - uncompressed_bytes - } + #[must_use] + pub fn new( + type_model_provider: Arc, + randomizer_facade: RandomizerFacade, + ) -> Self { + EntityFacadeImpl { + type_model_provider, + randomizer_facade, + } + } + + fn should_restore_default_value( + model_value: &ModelValue, + value: &ElementValue, + instance: &ParsedEntity, + key: &str, + ) -> bool { + if model_value.encrypted { + if let Some(final_ivs) = Self::get_final_iv_for_key(instance, key) { + return final_ivs.assert_bytes().is_empty() + && value == &ValueType::get_default(&model_value.value_type); + } + } + false + } + + fn encrypt_value( + model_value: &ModelValue, + instance_value: &ElementValue, + session_key: &GenericAesKey, + iv: Iv, + ) -> Result { + let value_type = &model_value.value_type; + + let element_is_nil = >::is_nil(instance_value); + if !model_value.encrypted + || (element_is_nil && model_value.cardinality == Cardinality::ZeroOrOne) + { + Ok(instance_value.clone()) + } else if element_is_nil { + Err(ApiCallError::internal(format!( + "Nil encrypted value is not accepted. ModelValue Id: {}", + model_value.id + ))) + } else { + let bytes = Self::map_value_to_binary(value_type, instance_value); + let encrypted_data = session_key + .encrypt_data(bytes.as_slice(), iv) + .expect("Cannot encrypt data"); + Ok(ElementValue::Bytes(encrypted_data)) + } + } + + fn get_final_iv_for_key(instance: &ParsedEntity, key: &str) -> Option { + if let Some(final_ivs) = instance.get("_finalIvs") { + if let Some(array) = final_ivs.assert_dict_ref().get(key) { + return Some(ElementValue::Bytes(array.assert_bytes())); + }; + }; + None + } + + fn map_value_to_binary(value_type: &ValueType, value: &ElementValue) -> Vec { + match value_type { + ValueType::Bytes => value.assert_bytes(), + ValueType::String => value.assert_string().as_bytes().to_vec(), + ValueType::Number => value.assert_number().to_string().as_bytes().to_vec(), + ValueType::Date => value + .assert_date() + .as_millis() + .to_string() + .as_bytes() + .to_vec(), + + ValueType::Boolean => if value.assert_bool() { b"1" } else { b"0" }.to_vec(), + ValueType::GeneratedId => value.assert_generated_id().0.as_bytes().to_vec(), + ValueType::CustomId => value.assert_custom_id().0.as_bytes().to_vec(), + ValueType::CompressedString => { + Self::lz4_compress_plain_bytes(value.assert_string().as_bytes()) + // this limit should be checked before creating the instance/entity itself + // eg: client should not accept more input in mail editor once limit is reached + .expect("Unchecked input data? received too large byte for lz4_compression") + } + } + } + + fn encrypt_and_map_inner( + &self, + type_model: &TypeModel, + instance: &ParsedEntity, + sk: &GenericAesKey, + ) -> Result { + let mut encrypted = ParsedEntity::new(); + + for (key, model_value) in &type_model.values { + let instance_value = instance.get(&key.to_string()).ok_or_else(|| { + ApiCallError::internal(format!("Can not find key: {key} in instance: {instance:?}")) + })?; + + let encrypted_value: ElementValue; + + if !model_value.encrypted { + encrypted_value = instance_value.clone() + } else if Self::should_restore_default_value(model_value, instance_value, instance, key) { + // restore the default encrypted value because it has not changed + // note: this branch must be checked *before* the one which reuses IVs as this one checks + // the length. + encrypted_value = ElementValue::String("".to_string()); + } else if model_value.is_final + && Self::get_final_iv_for_key(instance, key).is_some() + { + let final_iv = Iv::from_bytes( + Self::get_final_iv_for_key(instance, key) + .unwrap() + .assert_bytes() + .as_slice(), + ) + .map_err(|err| ApiCallError::internal(format!("iv of illegal size {:?}", err)))?; + + encrypted_value = Self::encrypt_value(model_value, instance_value, sk, final_iv)? + } else { + encrypted_value = Self::encrypt_value( + model_value, + instance_value, + sk, + Iv::generate(&self.randomizer_facade), + )? + } + encrypted.insert(key.to_string(), encrypted_value); + } + + if type_model.element_type == ElementType::Aggregated { + let id_key = String::from("_id"); + match encrypted.get(&id_key) { + Some(ElementValue::Null) => { + let new_id = self.randomizer_facade.generate_random_array::<4>(); + + encrypted.insert( + id_key, + ElementValue::String(BASE64_URL_SAFE_NO_PAD.encode(new_id)), + ); + } + Some(_) => { + // the _id is most likely already set to some valid value + } + None => { unreachable!("aggregate parsedEntity without an _id field encountered"); } + } + } + + for (association_name, association) in &type_model.associations { + let encrypted_association = match association.association_type { + AssociationType::Aggregation => { + self.encrypt_aggregate(type_model, association_name, association, instance, sk)? + } + AssociationType::ElementAssociation + | AssociationType::ListAssociation + | AssociationType::ListElementAssociationCustom + | AssociationType::ListElementAssociationGenerated + | AssociationType::BlobElementAssociation => instance + .get(&association_name.to_string()) + .cloned() + .ok_or(ApiCallError::internal(format!( + "could not find association {association_name} on type {}", + type_model.name + )))?, + }; + encrypted.insert(association_name.to_string(), encrypted_association); + } + + Ok(encrypted) + } + + fn encrypt_aggregate( + &self, + type_model: &TypeModel, + association_name: &str, + association: &ModelAssociation, + instance: &ParsedEntity, + sk: &GenericAesKey, + ) -> Result { + let dependency = association.dependency.unwrap_or(type_model.app); + let aggregated_type_model = self + .type_model_provider + .get_type_model(dependency, association.ref_type) + .ok_or_else(|| { + ApiCallError::internal(format!( + "unknown type model: {:?}", + (dependency, association.ref_type) + )) + })?; + let instance_association = instance.get(&association_name.to_string()).unwrap(); + + match (&association.cardinality, instance_association) { + (Cardinality::ZeroOrOne, ElementValue::Null) => Ok(ElementValue::Null), + + (_, ElementValue::Null) => Err(ApiCallError::internal(format!( + "Undefined attribute {}:{association_name}", + type_model.name + ))), + + (Cardinality::Any, _) => { + let aggregates = instance_association.assert_array(); + let mut encrypted_aggregates = Vec::with_capacity(aggregates.len()); + for aggregate in &aggregates { + let parsed_entity = self.encrypt_and_map_inner( + aggregated_type_model, + &aggregate.assert_dict(), + sk, + )?; + encrypted_aggregates.push(ElementValue::Dict(parsed_entity)); + } + + Ok(ElementValue::Array(encrypted_aggregates)) + } + + (Cardinality::One | Cardinality::ZeroOrOne, _) => { + let parsed_entity = self.encrypt_and_map_inner( + aggregated_type_model, + &instance_association.assert_dict(), + sk, + )?; + Ok(ElementValue::Dict(parsed_entity)) + } + } + } + + fn decrypt_and_map_inner( + &self, + type_model: &TypeModel, + mut entity: ParsedEntity, + session_key: &GenericAesKey, + ) -> Result { + let mut mapped_decrypted: HashMap = Default::default(); + let mut mapped_errors: Errors = Default::default(); + let mut mapped_ivs: HashMap = Default::default(); + + for (&key, model_value) in &type_model.values { + let stored_element = entity.remove(key).unwrap_or(ElementValue::Null); + let MappedValue { value, iv, error } = + Self::decrypt_and_parse_value(stored_element, session_key, key, model_value)?; + + mapped_decrypted.insert(key.to_string(), value); + if let Some(error) = error { + mapped_errors.insert(key.to_string(), ElementValue::String(error)); + } + if let Some(iv) = iv { + mapped_ivs.insert(key.to_string(), ElementValue::Bytes(iv.clone())); + } + } + + for (&association_name, association_model) in &type_model.associations { + let association_entry = entity + .remove(association_name) + .unwrap_or(ElementValue::Null); + let (mapped_association, errors) = self.map_associations( + type_model, + association_entry, + session_key, + association_name, + association_model, + )?; + + mapped_decrypted.insert(association_name.to_string(), mapped_association); + if !errors.is_empty() { + mapped_errors.insert(association_name.to_string(), ElementValue::Dict(errors)); + } + } + + if type_model.is_encrypted() { + // Only top-level types are expected to have `_errors` in the end but it is removed + // from the aggregates by `extract_errors()`. + mapped_decrypted.insert("_errors".to_string(), ElementValue::Dict(mapped_errors)); + mapped_decrypted.insert("_finalIvs".to_string(), ElementValue::Dict(mapped_ivs)); + } + + Ok(mapped_decrypted) + } + + fn map_associations( + &self, + type_model: &TypeModel, + association_data: ElementValue, + session_key: &GenericAesKey, + association_name: &str, + association_model: &ModelAssociation, + ) -> Result<(ElementValue, Errors), ApiCallError> { + let mut errors: Errors = Default::default(); + let dependency = match association_model.dependency { + Some(dep) => dep, + None => type_model.app, + }; + + if let AssociationType::Aggregation = association_model.association_type { + let aggregate_type_model = self + .type_model_provider + .get_type_model(dependency, association_model.ref_type) + .unwrap_or_else(|| panic!("Undefined type_model {}", association_model.ref_type)); + + match (association_data, association_model.cardinality.borrow()) { + (ElementValue::Null, Cardinality::ZeroOrOne) => Ok((ElementValue::Null, errors)), + (ElementValue::Null, Cardinality::One) => Err(ApiCallError::InternalSdkError { + error_message: format!( + "Value {association_name} with cardinality ONE can't be null" + ), + }), + (ElementValue::Array(arr), Cardinality::Any) => { + let mut aggregate_vec: Vec = Vec::with_capacity(arr.len()); + for (index, aggregate) in arr.into_iter().enumerate() { + match aggregate { + ElementValue::Dict(entity) => { + let mut decrypted_aggregate = self.decrypt_and_map_inner( + aggregate_type_model, + entity, + session_key, + )?; + + // Errors should be grouped inside the top-most object, so they should be + // extracted and removed from aggregates + if decrypted_aggregate.contains_key("_errors") { + let error_key = &format!("{}_{}", association_name, index); + self.extract_errors( + error_key, + &mut errors, + &mut decrypted_aggregate, + ); + } + + aggregate_vec.push(ElementValue::Dict(decrypted_aggregate)); + } + _ => { + return Err(ApiCallError::InternalSdkError { + error_message: format!( + "Invalid aggregate format. {} isn't a dict", + association_name + ), + }) + } + } + } + + Ok((ElementValue::Array(aggregate_vec), errors)) + } + (ElementValue::Dict(dict), Cardinality::One | Cardinality::ZeroOrOne) => { + let decrypted_aggregate = + self.decrypt_and_map_inner(aggregate_type_model, dict, session_key); + match decrypted_aggregate { + Ok(mut dec_aggregate) => { + self.extract_errors(association_name, &mut errors, &mut dec_aggregate); + Ok((ElementValue::Dict(dec_aggregate), errors)) + } + Err(_) => Err(ApiCallError::InternalSdkError { + error_message: format!( + "Failed to decrypt association {association_name}" + ), + }), + } + } + _ => Err(ApiCallError::InternalSdkError { + error_message: format!("Invalid association {association_name}"), + }), + } + } else { + Ok((association_data, errors)) + } + } + fn extract_errors( + &self, + association_name: &str, + errors: &mut Errors, + dec_aggregate: &mut ParsedEntity, + ) { + if let Some(ElementValue::Dict(err_dict)) = dec_aggregate.remove("_errors") { + if !err_dict.is_empty() { + errors.insert(association_name.to_string(), ElementValue::Dict(err_dict)); + } + } + } + fn decrypt_and_parse_value( + value: ElementValue, + session_key: &GenericAesKey, + key: &str, + model_value: &ModelValue, + ) -> Result { + match (&model_value.cardinality, &model_value.encrypted, value) { + (Cardinality::One | Cardinality::ZeroOrOne, true, ElementValue::String(s)) + if s.is_empty() => + { + // If the value is default-encrypted (empty string) then return default value and + // empty IV. When re-encrypting we should put the empty value back to not increase + // used storage. + let value = model_value.value_type.get_default(); + Ok(MappedValue { + value, + iv: Some(Vec::new()), + error: None, + }) + } + (Cardinality::ZeroOrOne, _, ElementValue::Null) => { + // If it's null, and it's permissible then we keep it as such + Ok(MappedValue { + value: ElementValue::Null, + iv: None, + error: None, + }) + } + (Cardinality::One | Cardinality::ZeroOrOne, true, ElementValue::Bytes(bytes)) => { + // If it's a proper encrypted value then we need to decrypt it, parse it and + // possibly record the IV. + let PlaintextAndIv { + data: plaintext, + iv, + } = session_key + .decrypt_data_and_iv(bytes.as_slice()) + .map_err(|e| ApiCallError::InternalSdkError { + error_message: e.to_string(), + })?; + + match Self::parse_decrypted_value(model_value.value_type.clone(), plaintext) { + Ok(value) => { + // We want to ensure we use the same IV for final encrypted values, as this + // will guarantee we get the same value back when we encrypt it. + let iv = if model_value.is_final { + Some(iv.to_vec()) + } else { + None + }; + Ok(MappedValue { + value, + iv, + error: None, + }) + } + Err(err) => Ok(MappedValue { + value: model_value.value_type.get_default(), + iv: None, + error: Some(format!("Failed to decrypt {key}. {err}")), + }), + } + } + (Cardinality::One | Cardinality::ZeroOrOne, false, value) => Ok(MappedValue { + value, + iv: None, + error: None, + }), + _ => Err(ApiCallError::internal(format!( + "Invalid value/cardinality combination for key `{key}`" + ))), + } + } + fn parse_decrypted_value( + value_type: ValueType, + bytes: Vec, + ) -> Result { + match value_type { + ValueType::String => { + let string = String::from_utf8(bytes) + .map_err(|e| ApiCallError::internal_with_err(e, "Invalid string"))?; + Ok(ElementValue::String(string)) + } + ValueType::Number => { + if bytes.is_empty() { + Ok(ElementValue::Null) + } else { + // Encrypted numbers are encrypted strings. + let string = String::from_utf8(bytes) + .map_err(|e| ApiCallError::internal_with_err(e, "Invalid number string"))?; + let number = string + .parse() + .map_err(|e| ApiCallError::internal_with_err(e, "Invalid number"))?; + + Ok(ElementValue::Number(number)) + } + } + ValueType::Bytes => Ok(ElementValue::Bytes(bytes.clone())), + ValueType::Date => { + let bytes = array_cast_slice(bytes.as_slice(), "u64") + .map_err(|e| ApiCallError::internal_with_err(e, "Invalid date bytes"))?; + Ok(ElementValue::Date(DateTime::from_millis( + u64::from_be_bytes(bytes), + ))) + } + ValueType::Boolean => match bytes.as_slice() { + b"1" => Ok(ElementValue::Bool(true)), + b"0" => Ok(ElementValue::Bool(false)), + _ => Err(ApiCallError::InternalSdkError { + error_message: "Failed to parse boolean bytes".to_owned(), + }), + }, + ValueType::CompressedString => { + let uncompressed_bytes = Self::lz4_decompress_decrypted_bytes(bytes.as_slice()) + .map_err(|e| { + ApiCallError::internal_with_err(e, "Cannot decompress compressed string") + })?; + + let uncompressed_string = String::from_utf8(uncompressed_bytes).map_err(|e| { + ApiCallError::internal_with_err( + e, + "Invalid utf-8 character in uncompressed string", + ) + })?; + + Ok(ElementValue::String(uncompressed_string)) + } + + ValueType::GeneratedId | ValueType::CustomId => { + unreachable!("Cannot convert {value_type:?} to ElementValue"); + } + } + } + + #[must_use] + pub fn lz4_compress_plain_bytes(bytes: &[u8]) -> Option> { + if bytes.is_empty() { + Some(Vec::new()) + } else if bytes.len() <= MAX_UNCOMPRESSED_INPUT_LZ4 { + Some(lz4_flex::compress(bytes)) + } else { + None + } + } + + pub fn lz4_decompress_decrypted_bytes( + compressed_bytes: &[u8], + ) -> Result, DecompressError> { + if compressed_bytes.is_empty() { + return Ok(Vec::new()); + } + + // since we don't store the uncompressed size we have to guess how much memory we might + // need. + // 12 times the compressed size should work for almost all cases. + let mut uncompressed_bytes = lz4_flex::decompress(compressed_bytes, compressed_bytes.len()); + while let Err(DecompressError::OutputTooSmall { + actual, + expected: _, + }) = uncompressed_bytes + { + uncompressed_bytes = lz4_flex::decompress(compressed_bytes, actual * 2); + } + uncompressed_bytes + } } impl EntityFacade for EntityFacadeImpl { - fn decrypt_and_map( - &self, - type_model: &TypeModel, - entity: ParsedEntity, - resolved_session_key: ResolvedSessionKey, - ) -> Result { - let mut mapped_decrypted = - self.decrypt_and_map_inner(type_model, entity, &resolved_session_key.session_key)?; - mapped_decrypted.insert( - "_ownerEncSessionKey".to_owned(), - ElementValue::Bytes(resolved_session_key.owner_enc_session_key.clone()), - ); - Ok(mapped_decrypted) - } - - fn encrypt_and_map( - &self, - type_model: &TypeModel, - instance: &ParsedEntity, - sk: &GenericAesKey, - ) -> Result { - self.encrypt_and_map_inner(type_model, instance, sk) - } + fn decrypt_and_map( + &self, + type_model: &TypeModel, + entity: ParsedEntity, + resolved_session_key: ResolvedSessionKey, + ) -> Result { + let mut mapped_decrypted = + self.decrypt_and_map_inner(type_model, entity, &resolved_session_key.session_key)?; + mapped_decrypted.insert( + "_ownerEncSessionKey".to_owned(), + ElementValue::Bytes(resolved_session_key.owner_enc_session_key.clone()), + ); + Ok(mapped_decrypted) + } + + fn encrypt_and_map( + &self, + type_model: &TypeModel, + instance: &ParsedEntity, + sk: &GenericAesKey, + ) -> Result { + self.encrypt_and_map_inner(type_model, instance, sk) + } } #[cfg(test)] mod lz4_compressed_string_compatibility_tests { - use crate::crypto::compatibility_test_utils::{ - get_compatibility_test_data, CompressionTestData, - }; - use crate::entities::entity_facade::EntityFacadeImpl; - use base64::prelude::BASE64_STANDARD; - use base64::Engine; - - #[test] - #[ignore] - fn create_compatibility_test_data() { - let test_data = get_compatibility_test_data().compression_tests; - let out = test_data - .iter() - .map(|td| { - BASE64_STANDARD.encode( - EntityFacadeImpl::lz4_compress_plain_bytes(td.uncompressed_text.as_bytes()) - .unwrap(), - ) - }) - .collect::>(); - - println!("List rustCompressed = List.of("); - for (index, o) in out.iter().enumerate() { - print!("\t\t\"{o}\""); - if index != out.len() - 1 { - println!(","); - } - } - println!(");"); - } - - #[test] - fn compatibility_with_js_and_java() { - let test_data = get_compatibility_test_data().compression_tests; - for test_data in test_data.into_iter() { - let CompressionTestData { - uncompressed_text, - compressed_base64_text_java, - compressed_base64_text_java_script, - compressed_base64_text_rust, - } = test_data; - - let new_compressed_bas64_text_rust = - EntityFacadeImpl::lz4_compress_plain_bytes(uncompressed_text.as_bytes()).unwrap(); - - assert_eq!(new_compressed_bas64_text_rust, compressed_base64_text_rust); - - let decompressed_bytes_java = EntityFacadeImpl::lz4_decompress_decrypted_bytes( - compressed_base64_text_java.as_slice(), - ) - .unwrap(); - - let decompressed_bytes_javascript = EntityFacadeImpl::lz4_decompress_decrypted_bytes( - compressed_base64_text_java_script.as_slice(), - ) - .unwrap(); - - let decompressed_bytes_rust = EntityFacadeImpl::lz4_decompress_decrypted_bytes( - compressed_base64_text_rust.as_slice(), - ) - .unwrap(); - - assert_eq!(decompressed_bytes_java, uncompressed_text.as_bytes()); - assert_eq!(decompressed_bytes_javascript, uncompressed_text.as_bytes()); - assert_eq!(decompressed_bytes_rust, uncompressed_text.as_bytes()); - assert_eq!( - uncompressed_text.is_empty(), - decompressed_bytes_java.is_empty() - ); - } - } + use crate::crypto::compatibility_test_utils::{ + get_compatibility_test_data, CompressionTestData, + }; + use crate::entities::entity_facade::EntityFacadeImpl; + use base64::prelude::BASE64_STANDARD; + use base64::Engine; + + #[test] + #[ignore] + fn create_compatibility_test_data() { + let test_data = get_compatibility_test_data().compression_tests; + let out = test_data + .iter() + .map(|td| { + BASE64_STANDARD.encode( + EntityFacadeImpl::lz4_compress_plain_bytes(td.uncompressed_text.as_bytes()) + .unwrap(), + ) + }) + .collect::>(); + + println!("List rustCompressed = List.of("); + for (index, o) in out.iter().enumerate() { + print!("\t\t\"{o}\""); + if index != out.len() - 1 { + println!(","); + } + } + println!(");"); + } + + #[test] + fn compatibility_with_js_and_java() { + let test_data = get_compatibility_test_data().compression_tests; + for test_data in test_data.into_iter() { + let CompressionTestData { + uncompressed_text, + compressed_base64_text_java, + compressed_base64_text_java_script, + compressed_base64_text_rust, + } = test_data; + + let new_compressed_bas64_text_rust = + EntityFacadeImpl::lz4_compress_plain_bytes(uncompressed_text.as_bytes()).unwrap(); + + assert_eq!(new_compressed_bas64_text_rust, compressed_base64_text_rust); + + let decompressed_bytes_java = EntityFacadeImpl::lz4_decompress_decrypted_bytes( + compressed_base64_text_java.as_slice(), + ) + .unwrap(); + + let decompressed_bytes_javascript = EntityFacadeImpl::lz4_decompress_decrypted_bytes( + compressed_base64_text_java_script.as_slice(), + ) + .unwrap(); + + let decompressed_bytes_rust = EntityFacadeImpl::lz4_decompress_decrypted_bytes( + compressed_base64_text_rust.as_slice(), + ) + .unwrap(); + + assert_eq!(decompressed_bytes_java, uncompressed_text.as_bytes()); + assert_eq!(decompressed_bytes_javascript, uncompressed_text.as_bytes()); + assert_eq!(decompressed_bytes_rust, uncompressed_text.as_bytes()); + assert_eq!( + uncompressed_text.is_empty(), + decompressed_bytes_java.is_empty() + ); + } + } } #[cfg(test)] mod tests { - use crate::crypto::crypto_facade::ResolvedSessionKey; - use crate::crypto::key::GenericAesKey; - use crate::crypto::randomizer_facade::test_util::DeterministicRng; - use crate::crypto::randomizer_facade::RandomizerFacade; - use crate::crypto::{aes::Iv, Aes256Key}; - use crate::date::DateTime; - use crate::element_value::{ElementValue, ParsedEntity}; - use crate::entities::entity_facade::{ - EntityFacade, EntityFacadeImpl, MappedValue, MAX_UNCOMPRESSED_INPUT_LZ4, - }; - use crate::entities::generated::sys::CustomerAccountTerminationRequest; - use crate::entities::generated::tutanota::Mail; - use crate::entities::Entity; - use crate::instance_mapper::InstanceMapper; - use crate::json_element::{JsonElement, RawEntity}; - use crate::json_serializer::JsonSerializer; - use crate::metamodel::{Cardinality, ModelValue, ValueType}; - use crate::type_model_provider::init_type_model_provider; - use crate::util::entity_test_utils::generate_email_entity; - use crate::{collection, ApiCallError}; - use std::collections::{BTreeMap, HashMap}; - use std::sync::Arc; - use std::time::SystemTime; - - const KNOWN_SK: [u8; 32] = [ - 83, 168, 168, 203, 48, 91, 246, 102, 175, 252, 39, 110, 36, 141, 4, 216, 135, 201, 226, - 134, 182, 175, 15, 152, 117, 216, 81, 1, 120, 134, 116, 143, - ]; - - const ALL_VALUE_TYPES: &[ValueType] = &[ - ValueType::String, - ValueType::Number, - ValueType::Bytes, - ValueType::Date, - ValueType::Boolean, - ValueType::GeneratedId, - ValueType::CustomId, - ValueType::CompressedString, - ]; - - #[test] - fn test_decrypt_mail() { - let sk = GenericAesKey::Aes256(Aes256Key::from_bytes(KNOWN_SK.as_slice()).unwrap()); - let owner_enc_session_key = vec![0, 1, 2]; - let type_model_provider = Arc::new(init_type_model_provider()); - let raw_entity: RawEntity = make_json_entity(); - let json_serializer = JsonSerializer::new(type_model_provider.clone()); - let encrypted_mail: ParsedEntity = json_serializer - .parse(&Mail::type_ref(), raw_entity) - .unwrap(); - - let entity_facade = EntityFacadeImpl::new( - Arc::clone(&type_model_provider), - RandomizerFacade::from_core(rand_core::OsRng), - ); - let type_ref = Mail::type_ref(); - let type_model = type_model_provider - .get_type_model(type_ref.app, type_ref.type_) - .unwrap(); - - let decrypted_mail = entity_facade - .decrypt_and_map( - type_model, - encrypted_mail, - ResolvedSessionKey { - session_key: sk, - owner_enc_session_key, - }, - ) - .unwrap(); - let instance_mapper = InstanceMapper::new(); - let _mail: Mail = instance_mapper - .parse_entity(decrypted_mail.clone()) - .unwrap(); - - assert_eq!( - &DateTime::from_millis(1720612041643), - decrypted_mail.get("receivedDate").unwrap().assert_date() - ); - assert!(decrypted_mail.get("confidential").unwrap().assert_bool()); - assert_eq!( - "Html email features", - decrypted_mail.get("subject").unwrap().assert_str() - ); - assert_eq!( - "Matthias", - decrypted_mail - .get("sender") - .unwrap() - .assert_dict() - .get("name") - .unwrap() - .assert_str() - ); - assert_eq!( - "map-free@tutanota.de", - decrypted_mail - .get("sender") - .unwrap() - .assert_dict() - .get("address") - .unwrap() - .assert_str() - ); - assert!(decrypted_mail - .get("attachments") - .unwrap() - .assert_array() - .is_empty()); - assert_eq!( + use crate::crypto::crypto_facade::ResolvedSessionKey; + use crate::crypto::key::GenericAesKey; + use crate::crypto::randomizer_facade::test_util::DeterministicRng; + use crate::crypto::randomizer_facade::RandomizerFacade; + use crate::crypto::{aes::Iv, Aes256Key}; + use crate::date::DateTime; + use crate::element_value::{ElementValue, ParsedEntity}; + use crate::entities::entity_facade::{ + EntityFacade, EntityFacadeImpl, MappedValue, MAX_UNCOMPRESSED_INPUT_LZ4, + }; + use crate::entities::generated::sys::CustomerAccountTerminationRequest; + use crate::entities::generated::tutanota::Mail; + use crate::entities::Entity; + use crate::instance_mapper::InstanceMapper; + use crate::json_element::{JsonElement, RawEntity}; + use crate::json_serializer::JsonSerializer; + use crate::metamodel::{Cardinality, ModelValue, ValueType}; + use crate::type_model_provider::init_type_model_provider; + use crate::util::entity_test_utils::generate_email_entity; + use crate::{collection, ApiCallError}; + use std::collections::{BTreeMap, HashMap}; + use std::sync::Arc; + use std::time::SystemTime; + + const KNOWN_SK: [u8; 32] = [ + 83, 168, 168, 203, 48, 91, 246, 102, 175, 252, 39, 110, 36, 141, 4, 216, 135, 201, 226, + 134, 182, 175, 15, 152, 117, 216, 81, 1, 120, 134, 116, 143, + ]; + + const ALL_VALUE_TYPES: &[ValueType] = &[ + ValueType::String, + ValueType::Number, + ValueType::Bytes, + ValueType::Date, + ValueType::Boolean, + ValueType::GeneratedId, + ValueType::CustomId, + ValueType::CompressedString, + ]; + + #[test] + fn test_decrypt_mail() { + let sk = GenericAesKey::Aes256(Aes256Key::from_bytes(KNOWN_SK.as_slice()).unwrap()); + let owner_enc_session_key = vec![0, 1, 2]; + let type_model_provider = Arc::new(init_type_model_provider()); + let raw_entity: RawEntity = make_json_entity(); + let json_serializer = JsonSerializer::new(type_model_provider.clone()); + let encrypted_mail: ParsedEntity = json_serializer + .parse(&Mail::type_ref(), raw_entity) + .unwrap(); + + let entity_facade = EntityFacadeImpl::new( + Arc::clone(&type_model_provider), + RandomizerFacade::from_core(rand_core::OsRng), + ); + let type_ref = Mail::type_ref(); + let type_model = type_model_provider + .get_type_model(type_ref.app, type_ref.type_) + .unwrap(); + + let decrypted_mail = entity_facade + .decrypt_and_map( + type_model, + encrypted_mail, + ResolvedSessionKey { + session_key: sk, + owner_enc_session_key, + }, + ) + .unwrap(); + let instance_mapper = InstanceMapper::new(); + let _mail: Mail = instance_mapper + .parse_entity(decrypted_mail.clone()) + .unwrap(); + + assert_eq!( + &DateTime::from_millis(1720612041643), + decrypted_mail.get("receivedDate").unwrap().assert_date() + ); + assert!(decrypted_mail.get("confidential").unwrap().assert_bool()); + assert_eq!( + "Html email features", + decrypted_mail.get("subject").unwrap().assert_str() + ); + assert_eq!( + "Matthias", + decrypted_mail + .get("sender") + .unwrap() + .assert_dict() + .get("name") + .unwrap() + .assert_str() + ); + assert_eq!( + "map-free@tutanota.de", + decrypted_mail + .get("sender") + .unwrap() + .assert_dict() + .get("address") + .unwrap() + .assert_str() + ); + assert!(decrypted_mail + .get("attachments") + .unwrap() + .assert_array() + .is_empty()); + assert_eq!( decrypted_mail .get("_finalIvs") .expect("has_final_ivs") @@ -815,7 +825,7 @@ mod tests { 0x72, 0x06 ], ); - assert_eq!( + assert_eq!( decrypted_mail .get("sender") .expect("has sender") @@ -826,344 +836,344 @@ mod tests { .len(), 1, ); - } - - #[test] - fn decrypt_compressed_string() { - let model_value = create_model_value(ValueType::CompressedString, true, Cardinality::One); - let sk = GenericAesKey::from_bytes(&[rand::random(); 32]).unwrap(); - let iv = Iv::generate(&RandomizerFacade::from_core(rand_core::OsRng)); - let value = ElementValue::String("this is a string value".to_string()); - - let encrypted_value = - EntityFacadeImpl::encrypt_value(&model_value, &value, &sk, iv.clone()).unwrap(); - - let decrypted_value = - EntityFacadeImpl::decrypt_and_parse_value(encrypted_value, &sk, "test", &model_value); - - assert_eq!( - Ok(MappedValue { - value: ElementValue::String("this is a string value".to_string()), - iv: Some(iv.get_inner().to_vec()), - error: None, - }), - decrypted_value - ); - } - - #[test] - fn decrypt_empty_compressed_string() { - let decrypted_value = EntityFacadeImpl::decrypt_and_parse_value( - ElementValue::String(String::default()), - &GenericAesKey::from_bytes(&KNOWN_SK).unwrap(), - "test", - &create_model_value(ValueType::CompressedString, true, Cardinality::One), - ) - .map(|a| a.value); - - assert_eq!(Ok(ElementValue::String(String::default())), decrypted_value); - } - - #[test] - fn compress_empty_compressed_string() { - let session_key = GenericAesKey::from_bytes(&KNOWN_SK).unwrap(); - let iv = Iv::from_bytes(&rand::random::<[u8; 16]>()).unwrap(); - let encrypted_value = EntityFacadeImpl::encrypt_value( - &create_model_value(ValueType::CompressedString, true, Cardinality::One), - &ElementValue::String(String::default()), - &session_key, - iv.clone(), - ); - - let encrypted_string = session_key - .encrypt_data(String::default().as_bytes(), iv) - .unwrap(); - assert_eq!(Ok(ElementValue::Bytes(encrypted_string)), encrypted_value); - } - - #[test] - fn should_not_compress_too_large_bytes() { - assert_eq!(MAX_UNCOMPRESSED_INPUT_LZ4, 0x7e000000); - let compressed_max_limit = EntityFacadeImpl::lz4_compress_plain_bytes( - b"a".repeat(MAX_UNCOMPRESSED_INPUT_LZ4).as_slice(), - ); - let compressed_over_limit = EntityFacadeImpl::lz4_compress_plain_bytes( - b"a".repeat(MAX_UNCOMPRESSED_INPUT_LZ4 + 1).as_slice(), - ); - - assert!(compressed_max_limit.is_some()); - assert_eq!(None, compressed_over_limit); - } - - #[test] - fn should_decompress_with_no_limit() { - // Construct some compressed text by ourselves instead of calling lz4_flex::compress - // to save ~10seconds ( in dev machine ) of test runtime - // compressed_over_limit is the output of: lz4_flex::compress(b"a".repeat(MAX_UNCOMPRESSED_INPUT).as_slice()); - let mut compressed_over_limit = vec![31, 97, 1, 0]; - let mut compressed_rest = vec![255; 8289918]; - let mut compressed_tail = vec![110, 96, 97, 97, 97, 97, 97, 97]; - compressed_over_limit.append(&mut compressed_rest); - compressed_over_limit.append(&mut compressed_tail); - - let decompressed = - EntityFacadeImpl::lz4_decompress_decrypted_bytes(compressed_over_limit.as_slice()) - .map(|a| a.len()) - .map_err(|_| ()); - - assert_eq!(Ok(MAX_UNCOMPRESSED_INPUT_LZ4 + 10), decompressed); - } - - #[test] - fn decrypt_compressed_string_with_resize() { - let input_bytes = b"test ".repeat(124); - let compressed = - EntityFacadeImpl::lz4_compress_plain_bytes(input_bytes.as_slice()).unwrap(); - let decompressed = - EntityFacadeImpl::lz4_decompress_decrypted_bytes(compressed.as_slice()).unwrap(); - assert_eq!(input_bytes.as_slice(), decompressed.as_slice()); - } - - #[test] - fn encrypt_value_string() { - let model_value = create_model_value(ValueType::String, true, Cardinality::One); - let sk = GenericAesKey::from_bytes(&[rand::random(); 32]).unwrap(); - let iv = Iv::generate(&RandomizerFacade::from_core(rand_core::OsRng)); - let value = ElementValue::String("this is a string value".to_string()); - - let encrypted_value = - EntityFacadeImpl::encrypt_value(&model_value, &value, &sk, iv.clone()); - - let expected = sk - .encrypt_data(value.assert_string().as_bytes(), iv) - .unwrap(); - - assert_eq!(expected, encrypted_value.unwrap().assert_bytes()) - } - - #[test] - fn encrypt_value_compressed_string() { - let model_value = create_model_value(ValueType::CompressedString, true, Cardinality::One); - let sk = GenericAesKey::from_bytes(&[rand::random(); 32]).unwrap(); - let iv = Iv::generate(&RandomizerFacade::from_core(rand_core::OsRng)); - - let value = ElementValue::String("Hello, world".to_string()); - - let encrypted_value = - EntityFacadeImpl::encrypt_value(&model_value, &value, &sk, iv.clone()) - .map(|a| a.assert_bytes()); - - let expected = sk - .clone() - .encrypt_data( - lz4_flex::compress(value.assert_string().as_bytes()).as_slice(), - iv.clone(), - ) - .unwrap(); - assert_eq!(Ok(expected), encrypted_value) - } - - #[test] - fn encrypt_value_bool() { - let model_value = create_model_value(ValueType::Boolean, true, Cardinality::One); - let sk = GenericAesKey::from_bytes(&[rand::random(); 32]).unwrap(); - let iv = Iv::generate(&RandomizerFacade::from_core(rand_core::OsRng)); - - { - let value = ElementValue::Bool(true); - - let encrypted_value = - EntityFacadeImpl::encrypt_value(&model_value, &value, &sk, iv.clone()); - - let expected = sk.clone().encrypt_data("1".as_bytes(), iv.clone()).unwrap(); - assert_eq!(expected, encrypted_value.unwrap().assert_bytes()) - } - - { - let value = ElementValue::Bool(false); - let encrypted_value = - EntityFacadeImpl::encrypt_value(&model_value, &value, &sk, iv.clone()); - - let expected = sk.clone().encrypt_data("0".as_bytes(), iv.clone()).unwrap(); - assert_eq!(expected, encrypted_value.unwrap().assert_bytes()) - } - } - - #[test] - fn encrypt_value_date() { - let model_value = create_model_value(ValueType::Date, true, Cardinality::One); - let sk = GenericAesKey::from_bytes(&[rand::random(); 32]).unwrap(); - let iv = Iv::generate(&RandomizerFacade::from_core(rand_core::OsRng)); - let value = ElementValue::Date(DateTime::from_system_time(SystemTime::now())); - - let encrypted_value = - EntityFacadeImpl::encrypt_value(&model_value, &value, &sk, iv.clone()); - - let expected = sk - .encrypt_data(value.assert_date().as_millis().to_string().as_bytes(), iv) - .unwrap(); - - assert_eq!(expected, encrypted_value.unwrap().assert_bytes()); - } - - #[test] - fn encrypt_value_bytes() { - let model_value = create_model_value(ValueType::Bytes, true, Cardinality::One); - let sk = GenericAesKey::from_bytes(&[rand::random(); 32]).unwrap(); - let randomizer_facade = &RandomizerFacade::from_core(rand_core::OsRng); - let iv = Iv::generate(randomizer_facade); - let value = ElementValue::Bytes(randomizer_facade.generate_random_array::<5>().to_vec()); - - let encrypted_value = - EntityFacadeImpl::encrypt_value(&model_value, &value, &sk, iv.clone()); - - let expected = sk - .encrypt_data(value.assert_bytes().as_slice(), iv) - .unwrap(); - - assert_eq!(expected, encrypted_value.unwrap().assert_bytes()); - } - - #[test] - fn encrypt_value_null() { - let sk = GenericAesKey::from_bytes(&[rand::random(); 32]).unwrap(); - for value_type in ALL_VALUE_TYPES { - assert_eq!( - ElementValue::Null, - EntityFacadeImpl::encrypt_value( - &create_model_value(value_type.clone(), true, Cardinality::ZeroOrOne), - &ElementValue::Null, - &sk, - Iv::generate(&RandomizerFacade::from_core(rand_core::OsRng)), - ) - .unwrap() - ); - } - } - - #[test] - fn encrypt_value_do_not_accept_null() { - let sk = GenericAesKey::from_bytes(&[rand::random(); 32]).unwrap(); - for value_type in ALL_VALUE_TYPES { - assert_eq!( - Err(ApiCallError::internal( - "Nil encrypted value is not accepted. ModelValue Id: 426".to_string() - )), - EntityFacadeImpl::encrypt_value( - &create_model_value(value_type.clone(), true, Cardinality::One), - &ElementValue::Null, - &sk, - Iv::generate(&RandomizerFacade::from_core(rand_core::OsRng)), - ) - ); - } - } - - #[test] - fn encrypt_instance() { - let sk = GenericAesKey::Aes256(Aes256Key::from_bytes(KNOWN_SK.as_slice()).unwrap()); - let owner_enc_session_key = [0, 1, 2]; - - let deterministic_rng = DeterministicRng(20); - let iv = Iv::generate(&RandomizerFacade::from_core(deterministic_rng.clone())); - let type_model_provider = Arc::new(init_type_model_provider()); - - let type_ref = Mail::type_ref(); - let type_model = type_model_provider - .get_type_model(type_ref.app, type_ref.type_) - .unwrap(); - - let entity_facade = EntityFacadeImpl::new( - Arc::clone(&type_model_provider), - RandomizerFacade::from_core(deterministic_rng), - ); - - let (mut expected_encrypted_mail, raw_mail) = generate_email_entity( - &sk, - &iv, - true, - String::from("Hello, world!"), - String::from("Hanover"), - String::from("Munich"), - ); - - // remove finalIvs for easy comparision - { - expected_encrypted_mail.remove("_finalIvs").unwrap(); - expected_encrypted_mail - .get_mut("sender") - .unwrap() - .assert_dict_mut_ref() - .remove("_finalIvs") - .unwrap(); - expected_encrypted_mail - .get_mut("firstRecipient") - .unwrap() - .assert_dict_mut_ref() - .remove("_finalIvs") - .unwrap(); - } - - let encrypted_mail = entity_facade.encrypt_and_map_inner(type_model, &raw_mail, &sk); - - assert_eq!(Ok(expected_encrypted_mail), encrypted_mail); - - // verify every data is preserved as is after decryption - { - let original_mail = raw_mail; - let encrypted_mail = encrypted_mail.unwrap(); - - let mut decrypted_mail = entity_facade - .decrypt_and_map( - type_model, - encrypted_mail.clone(), - ResolvedSessionKey { - session_key: sk.clone(), - owner_enc_session_key: owner_enc_session_key.to_vec(), - }, - ) - .unwrap(); - - // compare all the _finalIvs are initialised with expectedIV - // for simplicity in comparison remove them as well( original_mail don't have _finalIvs ) - verify_final_ivs_and_clear(&iv, &mut decrypted_mail); - - assert_eq!( + } + + #[test] + fn decrypt_compressed_string() { + let model_value = create_model_value(ValueType::CompressedString, true, Cardinality::One); + let sk = GenericAesKey::from_bytes(&[rand::random(); 32]).unwrap(); + let iv = Iv::generate(&RandomizerFacade::from_core(rand_core::OsRng)); + let value = ElementValue::String("this is a string value".to_string()); + + let encrypted_value = + EntityFacadeImpl::encrypt_value(&model_value, &value, &sk, iv.clone()).unwrap(); + + let decrypted_value = + EntityFacadeImpl::decrypt_and_parse_value(encrypted_value, &sk, "test", &model_value); + + assert_eq!( + Ok(MappedValue { + value: ElementValue::String("this is a string value".to_string()), + iv: Some(iv.get_inner().to_vec()), + error: None, + }), + decrypted_value + ); + } + + #[test] + fn decrypt_empty_compressed_string() { + let decrypted_value = EntityFacadeImpl::decrypt_and_parse_value( + ElementValue::String(String::default()), + &GenericAesKey::from_bytes(&KNOWN_SK).unwrap(), + "test", + &create_model_value(ValueType::CompressedString, true, Cardinality::One), + ) + .map(|a| a.value); + + assert_eq!(Ok(ElementValue::String(String::default())), decrypted_value); + } + + #[test] + fn compress_empty_compressed_string() { + let session_key = GenericAesKey::from_bytes(&KNOWN_SK).unwrap(); + let iv = Iv::from_bytes(&rand::random::<[u8; 16]>()).unwrap(); + let encrypted_value = EntityFacadeImpl::encrypt_value( + &create_model_value(ValueType::CompressedString, true, Cardinality::One), + &ElementValue::String(String::default()), + &session_key, + iv.clone(), + ); + + let encrypted_string = session_key + .encrypt_data(String::default().as_bytes(), iv) + .unwrap(); + assert_eq!(Ok(ElementValue::Bytes(encrypted_string)), encrypted_value); + } + + #[test] + fn should_not_compress_too_large_bytes() { + assert_eq!(MAX_UNCOMPRESSED_INPUT_LZ4, 0x7e000000); + let compressed_max_limit = EntityFacadeImpl::lz4_compress_plain_bytes( + b"a".repeat(MAX_UNCOMPRESSED_INPUT_LZ4).as_slice(), + ); + let compressed_over_limit = EntityFacadeImpl::lz4_compress_plain_bytes( + b"a".repeat(MAX_UNCOMPRESSED_INPUT_LZ4 + 1).as_slice(), + ); + + assert!(compressed_max_limit.is_some()); + assert_eq!(None, compressed_over_limit); + } + + #[test] + fn should_decompress_with_no_limit() { + // Construct some compressed text by ourselves instead of calling lz4_flex::compress + // to save ~10seconds ( in dev machine ) of test runtime + // compressed_over_limit is the output of: lz4_flex::compress(b"a".repeat(MAX_UNCOMPRESSED_INPUT).as_slice()); + let mut compressed_over_limit = vec![31, 97, 1, 0]; + let mut compressed_rest = vec![255; 8289918]; + let mut compressed_tail = vec![110, 96, 97, 97, 97, 97, 97, 97]; + compressed_over_limit.append(&mut compressed_rest); + compressed_over_limit.append(&mut compressed_tail); + + let decompressed = + EntityFacadeImpl::lz4_decompress_decrypted_bytes(compressed_over_limit.as_slice()) + .map(|a| a.len()) + .map_err(|_| ()); + + assert_eq!(Ok(MAX_UNCOMPRESSED_INPUT_LZ4 + 10), decompressed); + } + + #[test] + fn decrypt_compressed_string_with_resize() { + let input_bytes = b"test ".repeat(124); + let compressed = + EntityFacadeImpl::lz4_compress_plain_bytes(input_bytes.as_slice()).unwrap(); + let decompressed = + EntityFacadeImpl::lz4_decompress_decrypted_bytes(compressed.as_slice()).unwrap(); + assert_eq!(input_bytes.as_slice(), decompressed.as_slice()); + } + + #[test] + fn encrypt_value_string() { + let model_value = create_model_value(ValueType::String, true, Cardinality::One); + let sk = GenericAesKey::from_bytes(&[rand::random(); 32]).unwrap(); + let iv = Iv::generate(&RandomizerFacade::from_core(rand_core::OsRng)); + let value = ElementValue::String("this is a string value".to_string()); + + let encrypted_value = + EntityFacadeImpl::encrypt_value(&model_value, &value, &sk, iv.clone()); + + let expected = sk + .encrypt_data(value.assert_string().as_bytes(), iv) + .unwrap(); + + assert_eq!(expected, encrypted_value.unwrap().assert_bytes()) + } + + #[test] + fn encrypt_value_compressed_string() { + let model_value = create_model_value(ValueType::CompressedString, true, Cardinality::One); + let sk = GenericAesKey::from_bytes(&[rand::random(); 32]).unwrap(); + let iv = Iv::generate(&RandomizerFacade::from_core(rand_core::OsRng)); + + let value = ElementValue::String("Hello, world".to_string()); + + let encrypted_value = + EntityFacadeImpl::encrypt_value(&model_value, &value, &sk, iv.clone()) + .map(|a| a.assert_bytes()); + + let expected = sk + .clone() + .encrypt_data( + lz4_flex::compress(value.assert_string().as_bytes()).as_slice(), + iv.clone(), + ) + .unwrap(); + assert_eq!(Ok(expected), encrypted_value) + } + + #[test] + fn encrypt_value_bool() { + let model_value = create_model_value(ValueType::Boolean, true, Cardinality::One); + let sk = GenericAesKey::from_bytes(&[rand::random(); 32]).unwrap(); + let iv = Iv::generate(&RandomizerFacade::from_core(rand_core::OsRng)); + + { + let value = ElementValue::Bool(true); + + let encrypted_value = + EntityFacadeImpl::encrypt_value(&model_value, &value, &sk, iv.clone()); + + let expected = sk.clone().encrypt_data("1".as_bytes(), iv.clone()).unwrap(); + assert_eq!(expected, encrypted_value.unwrap().assert_bytes()) + } + + { + let value = ElementValue::Bool(false); + let encrypted_value = + EntityFacadeImpl::encrypt_value(&model_value, &value, &sk, iv.clone()); + + let expected = sk.clone().encrypt_data("0".as_bytes(), iv.clone()).unwrap(); + assert_eq!(expected, encrypted_value.unwrap().assert_bytes()) + } + } + + #[test] + fn encrypt_value_date() { + let model_value = create_model_value(ValueType::Date, true, Cardinality::One); + let sk = GenericAesKey::from_bytes(&[rand::random(); 32]).unwrap(); + let iv = Iv::generate(&RandomizerFacade::from_core(rand_core::OsRng)); + let value = ElementValue::Date(DateTime::from_system_time(SystemTime::now())); + + let encrypted_value = + EntityFacadeImpl::encrypt_value(&model_value, &value, &sk, iv.clone()); + + let expected = sk + .encrypt_data(value.assert_date().as_millis().to_string().as_bytes(), iv) + .unwrap(); + + assert_eq!(expected, encrypted_value.unwrap().assert_bytes()); + } + + #[test] + fn encrypt_value_bytes() { + let model_value = create_model_value(ValueType::Bytes, true, Cardinality::One); + let sk = GenericAesKey::from_bytes(&[rand::random(); 32]).unwrap(); + let randomizer_facade = &RandomizerFacade::from_core(rand_core::OsRng); + let iv = Iv::generate(randomizer_facade); + let value = ElementValue::Bytes(randomizer_facade.generate_random_array::<5>().to_vec()); + + let encrypted_value = + EntityFacadeImpl::encrypt_value(&model_value, &value, &sk, iv.clone()); + + let expected = sk + .encrypt_data(value.assert_bytes().as_slice(), iv) + .unwrap(); + + assert_eq!(expected, encrypted_value.unwrap().assert_bytes()); + } + + #[test] + fn encrypt_value_null() { + let sk = GenericAesKey::from_bytes(&[rand::random(); 32]).unwrap(); + for value_type in ALL_VALUE_TYPES { + assert_eq!( + ElementValue::Null, + EntityFacadeImpl::encrypt_value( + &create_model_value(value_type.clone(), true, Cardinality::ZeroOrOne), + &ElementValue::Null, + &sk, + Iv::generate(&RandomizerFacade::from_core(rand_core::OsRng)), + ) + .unwrap() + ); + } + } + + #[test] + fn encrypt_value_do_not_accept_null() { + let sk = GenericAesKey::from_bytes(&[rand::random(); 32]).unwrap(); + for value_type in ALL_VALUE_TYPES { + assert_eq!( + Err(ApiCallError::internal( + "Nil encrypted value is not accepted. ModelValue Id: 426".to_string() + )), + EntityFacadeImpl::encrypt_value( + &create_model_value(value_type.clone(), true, Cardinality::One), + &ElementValue::Null, + &sk, + Iv::generate(&RandomizerFacade::from_core(rand_core::OsRng)), + ) + ); + } + } + + #[test] + fn encrypt_instance() { + let sk = GenericAesKey::Aes256(Aes256Key::from_bytes(KNOWN_SK.as_slice()).unwrap()); + let owner_enc_session_key = [0, 1, 2]; + + let deterministic_rng = DeterministicRng(20); + let iv = Iv::generate(&RandomizerFacade::from_core(deterministic_rng.clone())); + let type_model_provider = Arc::new(init_type_model_provider()); + + let type_ref = Mail::type_ref(); + let type_model = type_model_provider + .get_type_model(type_ref.app, type_ref.type_) + .unwrap(); + + let entity_facade = EntityFacadeImpl::new( + Arc::clone(&type_model_provider), + RandomizerFacade::from_core(deterministic_rng), + ); + + let (mut expected_encrypted_mail, raw_mail) = generate_email_entity( + &sk, + &iv, + true, + String::from("Hello, world!"), + String::from("Hanover"), + String::from("Munich"), + ); + + // remove finalIvs for easy comparision + { + expected_encrypted_mail.remove("_finalIvs").unwrap(); + expected_encrypted_mail + .get_mut("sender") + .unwrap() + .assert_dict_mut_ref() + .remove("_finalIvs") + .unwrap(); + expected_encrypted_mail + .get_mut("firstRecipient") + .unwrap() + .assert_dict_mut_ref() + .remove("_finalIvs") + .unwrap(); + } + + let encrypted_mail = entity_facade.encrypt_and_map_inner(type_model, &raw_mail, &sk); + + assert_eq!(Ok(expected_encrypted_mail), encrypted_mail); + + // verify every data is preserved as is after decryption + { + let original_mail = raw_mail; + let encrypted_mail = encrypted_mail.unwrap(); + + let mut decrypted_mail = entity_facade + .decrypt_and_map( + type_model, + encrypted_mail.clone(), + ResolvedSessionKey { + session_key: sk.clone(), + owner_enc_session_key: owner_enc_session_key.to_vec(), + }, + ) + .unwrap(); + + // compare all the _finalIvs are initialised with expectedIV + // for simplicity in comparison remove them as well( original_mail don't have _finalIvs ) + verify_final_ivs_and_clear(&iv, &mut decrypted_mail); + + assert_eq!( Some(&ElementValue::Bytes(owner_enc_session_key.to_vec())), decrypted_mail.get("_ownerEncSessionKey"), ); - decrypted_mail.insert("_ownerEncSessionKey".to_string(), ElementValue::Null); - - assert_eq!( - Some(ElementValue::Dict(HashMap::new())), - decrypted_mail.remove("_errors") - ); - - // comparison with sorted fields. only for easy for debugging - assert_eq!( - map_to_string(&original_mail), - map_to_string(&decrypted_mail) - ); - assert_eq!(original_mail, decrypted_mail); - } - } - - #[test] - fn encrypt_unencrypted_to_db_literal() { - let type_model_provider = Arc::new(init_type_model_provider()); - let json_serializer = JsonSerializer::new(type_model_provider.clone()); - let entity_facade = EntityFacadeImpl::new( - Arc::clone(&type_model_provider), - RandomizerFacade::from_core(rand_core::OsRng), - ); - let type_ref = CustomerAccountTerminationRequest::type_ref(); - let type_model = type_model_provider - .get_type_model(type_ref.app, type_ref.type_) - .unwrap(); - let sk = GenericAesKey::from_bytes(rand::random::<[u8; 32]>().as_slice()).unwrap(); - - let dummy_date = DateTime::from_system_time(SystemTime::now()); - let instance: RawEntity = collection! { + decrypted_mail.insert("_ownerEncSessionKey".to_string(), ElementValue::Null); + + assert_eq!( + Some(ElementValue::Dict(HashMap::new())), + decrypted_mail.remove("_errors") + ); + + // comparison with sorted fields. only for easy for debugging + assert_eq!( + map_to_string(&original_mail), + map_to_string(&decrypted_mail) + ); + assert_eq!(original_mail, decrypted_mail); + } + } + + #[test] + fn encrypt_unencrypted_to_db_literal() { + let type_model_provider = Arc::new(init_type_model_provider()); + let json_serializer = JsonSerializer::new(type_model_provider.clone()); + let entity_facade = EntityFacadeImpl::new( + Arc::clone(&type_model_provider), + RandomizerFacade::from_core(rand_core::OsRng), + ); + let type_ref = CustomerAccountTerminationRequest::type_ref(); + let type_model = type_model_provider + .get_type_model(type_ref.app, type_ref.type_) + .unwrap(); + let sk = GenericAesKey::from_bytes(rand::random::<[u8; 32]>().as_slice()).unwrap(); + + let dummy_date = DateTime::from_system_time(SystemTime::now()); + let instance: RawEntity = collection! { "_format" => JsonElement::String("0".to_string()), "_id" => JsonElement::Array(vec![JsonElement::String("O1RT2Dj--3-0".to_string()); 2]), "_ownerGroup" => JsonElement::Null, @@ -1172,149 +1182,149 @@ mod tests { "terminationRequestDate" => JsonElement::String(dummy_date.as_millis().to_string()), "customer" => JsonElement::String("customId".to_string()), }; - let instance = json_serializer.parse(&type_ref, instance).unwrap(); - - let encrypted_instance = entity_facade.encrypt_and_map(type_model, &instance, &sk); - - // unencrypted value should be kept as-is - assert_eq!(Ok(instance), encrypted_instance); - } - - #[test] - fn encryption_final_ivs_will_be_reused() { - let type_model_provider = Arc::new(init_type_model_provider()); - - let rng = DeterministicRng(13); - let entity_facade = EntityFacadeImpl::new( - Arc::clone(&type_model_provider), - RandomizerFacade::from_core(rng.clone()), - ); - let type_ref = Mail::type_ref(); - let type_model = type_model_provider - .get_type_model(type_ref.app, type_ref.type_) - .unwrap(); - let sk = GenericAesKey::from_bytes(rand::random::<[u8; 32]>().as_slice()).unwrap(); - let new_iv = Iv::from_bytes(&rand::random::<[u8; 16]>()).unwrap(); - let original_iv = Iv::generate(&RandomizerFacade::from_core(rng.clone())); - - // use two separate iv - assert_ne!(original_iv.get_inner(), new_iv.get_inner()); - - let (_, mut unencrypted_mail) = generate_email_entity( - &sk, - &original_iv, - true, - String::from("Hello, world!"), - String::from("Hanover"), - String::from("Munich"), - ); - - // set separate finalIv for some field - let final_iv_for_subject = [( - "subject".to_string(), - ElementValue::Bytes(new_iv.get_inner().to_vec()), - )] - .into_iter() - .collect::>(); - - unencrypted_mail.insert( - "_finalIvs".to_string(), - ElementValue::Dict(final_iv_for_subject), - ); - - let encrypted_mail = entity_facade - .encrypt_and_map_inner(type_model, &unencrypted_mail, &sk) - .unwrap(); - - let encrypted_subject = encrypted_mail.get("subject").unwrap(); - let subject_and_iv = sk - .decrypt_data_and_iv(&encrypted_subject.assert_bytes()) - .unwrap(); - - assert_eq!( - Ok("Hello, world!".to_string()), - String::from_utf8(subject_and_iv.data) - ); - assert_eq!(new_iv.get_inner(), &subject_and_iv.iv); - - // other fields should be encrypted with origin_iv - let encrypted_recipient_name = encrypted_mail - .get("firstRecipient") - .unwrap() - .assert_dict() - .get("name") - .unwrap() - .assert_bytes(); - let recipient_and_iv = sk.decrypt_data_and_iv(&encrypted_recipient_name).unwrap(); - assert_eq!(original_iv.get_inner().to_vec(), recipient_and_iv.iv) - } - - #[test] - #[ignore = "todo: Right now we will anyway try to encrypt the default value even for final fields.\ + let instance = json_serializer.parse(&type_ref, instance).unwrap(); + + let encrypted_instance = entity_facade.encrypt_and_map(type_model, &instance, &sk); + + // unencrypted value should be kept as-is + assert_eq!(Ok(instance), encrypted_instance); + } + + #[test] + fn encryption_final_ivs_will_be_reused() { + let type_model_provider = Arc::new(init_type_model_provider()); + + let rng = DeterministicRng(13); + let entity_facade = EntityFacadeImpl::new( + Arc::clone(&type_model_provider), + RandomizerFacade::from_core(rng.clone()), + ); + let type_ref = Mail::type_ref(); + let type_model = type_model_provider + .get_type_model(type_ref.app, type_ref.type_) + .unwrap(); + let sk = GenericAesKey::from_bytes(rand::random::<[u8; 32]>().as_slice()).unwrap(); + let new_iv = Iv::from_bytes(&rand::random::<[u8; 16]>()).unwrap(); + let original_iv = Iv::generate(&RandomizerFacade::from_core(rng.clone())); + + // use two separate iv + assert_ne!(original_iv.get_inner(), new_iv.get_inner()); + + let (_, mut unencrypted_mail) = generate_email_entity( + &sk, + &original_iv, + true, + String::from("Hello, world!"), + String::from("Hanover"), + String::from("Munich"), + ); + + // set separate finalIv for some field + let final_iv_for_subject = [( + "subject".to_string(), + ElementValue::Bytes(new_iv.get_inner().to_vec()), + )] + .into_iter() + .collect::>(); + + unencrypted_mail.insert( + "_finalIvs".to_string(), + ElementValue::Dict(final_iv_for_subject), + ); + + let encrypted_mail = entity_facade + .encrypt_and_map_inner(type_model, &unencrypted_mail, &sk) + .unwrap(); + + let encrypted_subject = encrypted_mail.get("subject").unwrap(); + let subject_and_iv = sk + .decrypt_data_and_iv(&encrypted_subject.assert_bytes()) + .unwrap(); + + assert_eq!( + Ok("Hello, world!".to_string()), + String::from_utf8(subject_and_iv.data) + ); + assert_eq!(new_iv.get_inner(), &subject_and_iv.iv); + + // other fields should be encrypted with origin_iv + let encrypted_recipient_name = encrypted_mail + .get("firstRecipient") + .unwrap() + .assert_dict() + .get("name") + .unwrap() + .assert_bytes(); + let recipient_and_iv = sk.decrypt_data_and_iv(&encrypted_recipient_name).unwrap(); + assert_eq!(original_iv.get_inner().to_vec(), recipient_and_iv.iv) + } + + #[test] + #[ignore = "todo: Right now we will anyway try to encrypt the default value even for final fields.\ This is however not intended. We skip the implementation because we did not need it for service call?"] - fn empty_final_iv_and_default_value_should_be_preserved() { - let type_model_provider = Arc::new(init_type_model_provider()); - let entity_facade = EntityFacadeImpl::new( - Arc::clone(&type_model_provider), - RandomizerFacade::from_core(rand_core::OsRng), - ); - let type_ref = Mail::type_ref(); - let type_model = type_model_provider - .get_type_model(type_ref.app, type_ref.type_) - .unwrap(); - let sk = GenericAesKey::from_bytes(rand::random::<[u8; 32]>().as_slice()).unwrap(); - let iv = Iv::from_bytes(&rand::random::<[u8; 16]>()).unwrap(); - - let default_subject = String::from(""); - let (_, unencrypted_mail) = generate_email_entity( - &sk, - &iv, - true, - default_subject.clone(), - String::from("Hanover"), - String::from("Munich"), - ); - - let encrypted_mail = entity_facade - .encrypt_and_map_inner(type_model, &unencrypted_mail, &sk) - .unwrap(); - - let encrypted_subject = encrypted_mail.get("subject").unwrap().assert_bytes(); - assert_eq!(default_subject.as_bytes(), encrypted_subject.as_slice()); - } - - fn map_to_string(map: &HashMap) -> String { - let mut out = String::new(); - let sorted_map: BTreeMap = map.clone().into_iter().collect(); - for (key, value) in &sorted_map { - match value { - ElementValue::Dict(aggregate) => { - out.push_str(&format!("{}: {}\n", key, map_to_string(aggregate))) - }, - _ => out.push_str(&format!("{}: {:?}\n", key, value)), - } - } - out - } - - fn verify_final_ivs_and_clear(iv: &Iv, instance: &mut ParsedEntity) { - for (name, value) in instance.iter_mut() { - match value { - ElementValue::Dict(value_map) if name == "_finalIvs" => { - for (_n, actual_iv) in value_map.iter() { - assert_eq!(iv.get_inner(), actual_iv.assert_bytes().as_slice()); - } - value_map.clear(); - }, - - ElementValue::Dict(value_map) => verify_final_ivs_and_clear(iv, value_map), - _ => {}, - } - } - } - - fn make_json_entity() -> RawEntity { - collection! { + fn empty_final_iv_and_default_value_should_be_preserved() { + let type_model_provider = Arc::new(init_type_model_provider()); + let entity_facade = EntityFacadeImpl::new( + Arc::clone(&type_model_provider), + RandomizerFacade::from_core(rand_core::OsRng), + ); + let type_ref = Mail::type_ref(); + let type_model = type_model_provider + .get_type_model(type_ref.app, type_ref.type_) + .unwrap(); + let sk = GenericAesKey::from_bytes(rand::random::<[u8; 32]>().as_slice()).unwrap(); + let iv = Iv::from_bytes(&rand::random::<[u8; 16]>()).unwrap(); + + let default_subject = String::from(""); + let (_, unencrypted_mail) = generate_email_entity( + &sk, + &iv, + true, + default_subject.clone(), + String::from("Hanover"), + String::from("Munich"), + ); + + let encrypted_mail = entity_facade + .encrypt_and_map_inner(type_model, &unencrypted_mail, &sk) + .unwrap(); + + let encrypted_subject = encrypted_mail.get("subject").unwrap().assert_bytes(); + assert_eq!(default_subject.as_bytes(), encrypted_subject.as_slice()); + } + + fn map_to_string(map: &HashMap) -> String { + let mut out = String::new(); + let sorted_map: BTreeMap = map.clone().into_iter().collect(); + for (key, value) in &sorted_map { + match value { + ElementValue::Dict(aggregate) => { + out.push_str(&format!("{}: {}\n", key, map_to_string(aggregate))) + } + _ => out.push_str(&format!("{}: {:?}\n", key, value)), + } + } + out + } + + fn verify_final_ivs_and_clear(iv: &Iv, instance: &mut ParsedEntity) { + for (name, value) in instance.iter_mut() { + match value { + ElementValue::Dict(value_map) if name == "_finalIvs" => { + for (_n, actual_iv) in value_map.iter() { + assert_eq!(iv.get_inner(), actual_iv.assert_bytes().as_slice()); + } + value_map.clear(); + } + + ElementValue::Dict(value_map) => verify_final_ivs_and_clear(iv, value_map), + _ => {} + } + } + } + + fn make_json_entity() -> RawEntity { + collection! { "sentDate"=> JsonElement::Null, "_ownerEncSessionKey"=> JsonElement::String( "AbK4PO4dConOew4jXt7UcmL9I73z1NA14EgbpBEw8J9ipgjD3i92SakgAv7SFXOE59VlWQ5dw3whqqSzkwoQavWWkDeJep1JzdP4ZyzNFMO7".to_string(), @@ -1438,19 +1448,19 @@ mod tests { ], ), "sets"=> JsonElement::Array(vec![]),} - } - - fn create_model_value( - value_type: ValueType, - encrypted: bool, - cardinality: Cardinality, - ) -> ModelValue { - ModelValue { - id: 426, - value_type, - cardinality, - is_final: true, - encrypted, - } - } + } + + fn create_model_value( + value_type: ValueType, + encrypted: bool, + cardinality: Cardinality, + ) -> ModelValue { + ModelValue { + id: 426, + value_type, + cardinality, + is_final: true, + encrypted, + } + } } diff --git a/tuta-sdk/rust/sdk/src/entities/json_size_estimator.rs b/tuta-sdk/rust/sdk/src/entities/json_size_estimator.rs index 15e94d1cbaa..e23124196a6 100644 --- a/tuta-sdk/rust/sdk/src/entities/json_size_estimator.rs +++ b/tuta-sdk/rust/sdk/src/entities/json_size_estimator.rs @@ -67,8 +67,8 @@ impl<'a> Serializer for &'a mut SizeEstimatingSerializer { unimplemented!("serialize_i32"); } - fn serialize_i64(self, _v: i64) -> Result { - unimplemented!("serialize_i64"); + fn serialize_i64(self, v: i64) -> Result { + Ok((v + 1).ilog10() as usize + 1) } fn serialize_u8(self, _v: u8) -> Result { From 4301cbb329e780947a647d6fa4abc16fcbbff88f Mon Sep 17 00:00:00 2001 From: nig Date: Wed, 13 Nov 2024 12:06:42 +0100 Subject: [PATCH 17/32] fix imap example --- packages/node-mimimi/examples/import_imap_mail.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/node-mimimi/examples/import_imap_mail.rs b/packages/node-mimimi/examples/import_imap_mail.rs index 06767c18071..31a5d141095 100644 --- a/packages/node-mimimi/examples/import_imap_mail.rs +++ b/packages/node-mimimi/examples/import_imap_mail.rs @@ -1,3 +1,5 @@ +use std::sync::Mutex; + /// How to: Import IMAP mail from Greenmail to TutaMail /// /// @@ -71,15 +73,20 @@ async fn main() { .system_folder_by_type(MailSetKind::Inbox) .expect("inbox should exist"); + let id = logged_in_sdk + .mail_facade() + .get_group_id_for_mail_address("map-free@tutanota.de") + .await + .unwrap(); let mut importer = ImporterApi::new( logged_in_sdk, - import_source, - "map-free@tutanota.de".to_string(), - inbox_folder._id.clone(), + id, + inbox_folder._id.clone().unwrap(), + Arc::new(Mutex::new(import_source)), ); let import_status = importer - .continue_import() + .continue_import_inner() .await .expect("Cannot complete import"); assert!(ImportState::Finished == import_status.state,); From c86a44e10451887a0e28834849e3dea81f4c9259 Mon Sep 17 00:00:00 2001 From: nig Date: Wed, 13 Nov 2024 11:48:22 +0100 Subject: [PATCH 18/32] add example to cargo.toml --- packages/node-mimimi/Cargo.toml | 6 +- .../node-mimimi/examples/import_eml_mail.rs | 70 +++++++++++++++++++ .../testmail/2024-10-25-10h15m39s-test.eml | 15 ++++ .../testmail/2024-10-25-10h17m19s-test.eml | 15 ++++ .../testmail/2024-10-25-10h17m48s-test.eml | 15 ++++ .../testmail/2024-10-25-10h20m09s-test.eml | 15 ++++ .../testmail/2024-10-25-10h25m00s-test.eml | 15 ++++ .../testmail/2024-10-25-10h28m24s-test.eml | 15 ++++ .../testmail/2024-10-25-10h39m05s-test.eml | 15 ++++ .../testmail/2024-10-25-10h39m40s-.eml | 15 ++++ .../testmail/2024-10-25-10h48m47s-test.eml | 15 ++++ .../testmail/2024-10-25-11h00m19s-test.eml | 15 ++++ .../testmail/2024-10-25-12h16m50s-.eml | 15 ++++ .../testmail/2024-10-25-14h44m35s-test.eml | 15 ++++ 14 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 packages/node-mimimi/examples/import_eml_mail.rs create mode 100644 packages/node-mimimi/examples/testmail/2024-10-25-10h15m39s-test.eml create mode 100644 packages/node-mimimi/examples/testmail/2024-10-25-10h17m19s-test.eml create mode 100644 packages/node-mimimi/examples/testmail/2024-10-25-10h17m48s-test.eml create mode 100644 packages/node-mimimi/examples/testmail/2024-10-25-10h20m09s-test.eml create mode 100644 packages/node-mimimi/examples/testmail/2024-10-25-10h25m00s-test.eml create mode 100644 packages/node-mimimi/examples/testmail/2024-10-25-10h28m24s-test.eml create mode 100644 packages/node-mimimi/examples/testmail/2024-10-25-10h39m05s-test.eml create mode 100644 packages/node-mimimi/examples/testmail/2024-10-25-10h39m40s-.eml create mode 100644 packages/node-mimimi/examples/testmail/2024-10-25-10h48m47s-test.eml create mode 100644 packages/node-mimimi/examples/testmail/2024-10-25-11h00m19s-test.eml create mode 100644 packages/node-mimimi/examples/testmail/2024-10-25-12h16m50s-.eml create mode 100644 packages/node-mimimi/examples/testmail/2024-10-25-14h44m35s-test.eml diff --git a/packages/node-mimimi/Cargo.toml b/packages/node-mimimi/Cargo.toml index 5b6522f833d..02774e7857a 100644 --- a/packages/node-mimimi/Cargo.toml +++ b/packages/node-mimimi/Cargo.toml @@ -54,4 +54,8 @@ javascript = ["dep:napi-derive"] [[example]] name = "import_imap_mail" -path = "examples/import_imap_mail.rs" \ No newline at end of file +path = "examples/import_imap_mail.rs" + +[[example]] +name = "import_eml_mail" +path = "examples/import_eml_mail.rs" \ No newline at end of file diff --git a/packages/node-mimimi/examples/import_eml_mail.rs b/packages/node-mimimi/examples/import_eml_mail.rs new file mode 100644 index 00000000000..bf0bb6efecc --- /dev/null +++ b/packages/node-mimimi/examples/import_eml_mail.rs @@ -0,0 +1,70 @@ +#[cfg(not(feature = "javascript"))] +#[tokio::main] +async fn main() { + use tutao_node_mimimi::importer::file_reader::import_client::FileImport; + use std::sync::Mutex; + + use std::sync::Arc; + use tutao_node_mimimi::importer::imap_reader::{ + import_client::ImapImport, ImapCredentials, ImapImportConfig, LoginMechanism, + }; + use tutao_node_mimimi::importer::{ImportSource, ImportState, ImporterApi}; + use tutasdk::folder_system::MailSetKind; + use tutasdk::net::native_rest_client::NativeRestClient; + use tutasdk::Sdk; + + let sdk = Sdk::new( + "http://localhost:9000".to_string(), + Arc::new(NativeRestClient::try_new().unwrap()), + ); + let logged_in_sdk = sdk + .create_session("map-free@tutanota.de", "map") + .await + .unwrap(); + let import_source = ImportSource::LocalFile { + fs_email_client: FileImport::new(get_file_paths()).unwrap(), + }; + + let mail_facade = logged_in_sdk.mail_facade(); + let mailbox = mail_facade.load_user_mailbox().await.unwrap(); + let folders = mail_facade + .load_folders_for_mailbox(&mailbox) + .await + .unwrap(); + let inbox_folder = folders + .system_folder_by_type(MailSetKind::Inbox) + .expect("inbox should exist"); + + let id = logged_in_sdk + .mail_facade() + .get_group_id_for_mail_address("map-free@tutanota.de") + .await + .unwrap(); + let mut importer = ImporterApi::new( + logged_in_sdk, + id, + inbox_folder._id.clone().unwrap(), + Arc::new(Mutex::new(import_source)), + ); + + let import_status = importer + .continue_import_inner() + .await + .expect("Cannot complete import"); + assert!(ImportState::Finished == import_status.state,); + assert!(import_status.imported_mails > 0); +} + +#[cfg(feature = "javascript")] +fn main() { + panic!("Only runnable without javascript feature "); +} + +fn get_file_paths() -> Vec { + let Ok(paths) = std::fs::read_dir("./examples/testmail") else { + panic!("could not load test mail files names, are you in the node-mimimi project root?") + }; + paths + .map(|path| path.unwrap().path().to_str().unwrap().to_owned()) + .collect() +} diff --git a/packages/node-mimimi/examples/testmail/2024-10-25-10h15m39s-test.eml b/packages/node-mimimi/examples/testmail/2024-10-25-10h15m39s-test.eml new file mode 100644 index 00000000000..ece65eff271 --- /dev/null +++ b/packages/node-mimimi/examples/testmail/2024-10-25-10h15m39s-test.eml @@ -0,0 +1,15 @@ +From: freepancakes@tutanota.com +MIME-Version: 1.0 +To: Freepancakes +Subject: =?UTF-8?B?dGVzdA==?= +Date: Fri, 25 Oct 2024 08:15:39 +0000 +Content-Type: multipart/related; boundary="------------79Bu5A16qPEYcVIZL@tutanota" + +--------------79Bu5A16qPEYcVIZL@tutanota +Content-Type: text/html; charset=UTF-8 +Content-transfer-encoding: base64 + +PGJyPjxicj4tLQo8YnI+ClNlY3VyZWQgd2l0aCBUdXRhIE1haWw6Cjxicj4KaHR0cHM6Ly90dXRhLm +NvbQ== + +--------------79Bu5A16qPEYcVIZL@tutanota-- \ No newline at end of file diff --git a/packages/node-mimimi/examples/testmail/2024-10-25-10h17m19s-test.eml b/packages/node-mimimi/examples/testmail/2024-10-25-10h17m19s-test.eml new file mode 100644 index 00000000000..cf7de23d942 --- /dev/null +++ b/packages/node-mimimi/examples/testmail/2024-10-25-10h17m19s-test.eml @@ -0,0 +1,15 @@ +From: freepancakes@tutanota.com +MIME-Version: 1.0 +To: Freepancakes +Subject: =?UTF-8?B?dGVzdA==?= +Date: Fri, 25 Oct 2024 08:17:19 +0000 +Content-Type: multipart/related; boundary="------------79Bu5A16qPEYcVIZL@tutanota" + +--------------79Bu5A16qPEYcVIZL@tutanota +Content-Type: text/html; charset=UTF-8 +Content-transfer-encoding: base64 + +PGJyPjxicj4tLQo8YnI+ClNlY3VyZWQgd2l0aCBUdXRhIE1haWw6Cjxicj4KaHR0cHM6Ly90dXRhLm +NvbQ== + +--------------79Bu5A16qPEYcVIZL@tutanota-- \ No newline at end of file diff --git a/packages/node-mimimi/examples/testmail/2024-10-25-10h17m48s-test.eml b/packages/node-mimimi/examples/testmail/2024-10-25-10h17m48s-test.eml new file mode 100644 index 00000000000..aae2007ca3e --- /dev/null +++ b/packages/node-mimimi/examples/testmail/2024-10-25-10h17m48s-test.eml @@ -0,0 +1,15 @@ +From: freepancakes@tutanota.com +MIME-Version: 1.0 +To: Freepancakes +Subject: =?UTF-8?B?dGVzdA==?= +Date: Fri, 25 Oct 2024 08:17:48 +0000 +Content-Type: multipart/related; boundary="------------79Bu5A16qPEYcVIZL@tutanota" + +--------------79Bu5A16qPEYcVIZL@tutanota +Content-Type: text/html; charset=UTF-8 +Content-transfer-encoding: base64 + +PGJyPjxicj4tLQo8YnI+ClNlY3VyZWQgd2l0aCBUdXRhIE1haWw6Cjxicj4KaHR0cHM6Ly90dXRhLm +NvbQ== + +--------------79Bu5A16qPEYcVIZL@tutanota-- \ No newline at end of file diff --git a/packages/node-mimimi/examples/testmail/2024-10-25-10h20m09s-test.eml b/packages/node-mimimi/examples/testmail/2024-10-25-10h20m09s-test.eml new file mode 100644 index 00000000000..b2c9a9e84b1 --- /dev/null +++ b/packages/node-mimimi/examples/testmail/2024-10-25-10h20m09s-test.eml @@ -0,0 +1,15 @@ +From: freepancakes@tutanota.com +MIME-Version: 1.0 +To: Freepancakes +Subject: =?UTF-8?B?dGVzdA==?= +Date: Fri, 25 Oct 2024 08:20:09 +0000 +Content-Type: multipart/related; boundary="------------79Bu5A16qPEYcVIZL@tutanota" + +--------------79Bu5A16qPEYcVIZL@tutanota +Content-Type: text/html; charset=UTF-8 +Content-transfer-encoding: base64 + +PGJyPjxicj4tLQo8YnI+ClNlY3VyZWQgd2l0aCBUdXRhIE1haWw6Cjxicj4KaHR0cHM6Ly90dXRhLm +NvbQ== + +--------------79Bu5A16qPEYcVIZL@tutanota-- \ No newline at end of file diff --git a/packages/node-mimimi/examples/testmail/2024-10-25-10h25m00s-test.eml b/packages/node-mimimi/examples/testmail/2024-10-25-10h25m00s-test.eml new file mode 100644 index 00000000000..5f35258938a --- /dev/null +++ b/packages/node-mimimi/examples/testmail/2024-10-25-10h25m00s-test.eml @@ -0,0 +1,15 @@ +From: freepancakes@tutanota.com +MIME-Version: 1.0 +To: Freepancakes +Subject: =?UTF-8?B?dGVzdA==?= +Date: Fri, 25 Oct 2024 08:25:00 +0000 +Content-Type: multipart/related; boundary="------------79Bu5A16qPEYcVIZL@tutanota" + +--------------79Bu5A16qPEYcVIZL@tutanota +Content-Type: text/html; charset=UTF-8 +Content-transfer-encoding: base64 + +PGJyPjxicj4tLQo8YnI+ClNlY3VyZWQgd2l0aCBUdXRhIE1haWw6Cjxicj4KaHR0cHM6Ly90dXRhLm +NvbQ== + +--------------79Bu5A16qPEYcVIZL@tutanota-- \ No newline at end of file diff --git a/packages/node-mimimi/examples/testmail/2024-10-25-10h28m24s-test.eml b/packages/node-mimimi/examples/testmail/2024-10-25-10h28m24s-test.eml new file mode 100644 index 00000000000..8e7d224249b --- /dev/null +++ b/packages/node-mimimi/examples/testmail/2024-10-25-10h28m24s-test.eml @@ -0,0 +1,15 @@ +From: freepancakes@tutanota.com +MIME-Version: 1.0 +To: Freepancakes +Subject: =?UTF-8?B?dGVzdA==?= +Date: Fri, 25 Oct 2024 08:28:24 +0000 +Content-Type: multipart/related; boundary="------------79Bu5A16qPEYcVIZL@tutanota" + +--------------79Bu5A16qPEYcVIZL@tutanota +Content-Type: text/html; charset=UTF-8 +Content-transfer-encoding: base64 + +PGJyPjxicj4tLQo8YnI+ClNlY3VyZWQgd2l0aCBUdXRhIE1haWw6Cjxicj4KaHR0cHM6Ly90dXRhLm +NvbQ== + +--------------79Bu5A16qPEYcVIZL@tutanota-- \ No newline at end of file diff --git a/packages/node-mimimi/examples/testmail/2024-10-25-10h39m05s-test.eml b/packages/node-mimimi/examples/testmail/2024-10-25-10h39m05s-test.eml new file mode 100644 index 00000000000..d6a4a5e96b0 --- /dev/null +++ b/packages/node-mimimi/examples/testmail/2024-10-25-10h39m05s-test.eml @@ -0,0 +1,15 @@ +From: freepancakes@tutanota.com +MIME-Version: 1.0 +To: Freepancakes +Subject: =?UTF-8?B?dGVzdA==?= +Date: Fri, 25 Oct 2024 08:39:05 +0000 +Content-Type: multipart/related; boundary="------------79Bu5A16qPEYcVIZL@tutanota" + +--------------79Bu5A16qPEYcVIZL@tutanota +Content-Type: text/html; charset=UTF-8 +Content-transfer-encoding: base64 + +PGJyPjxicj4tLQo8YnI+ClNlY3VyZWQgd2l0aCBUdXRhIE1haWw6Cjxicj4KaHR0cHM6Ly90dXRhLm +NvbQ== + +--------------79Bu5A16qPEYcVIZL@tutanota-- \ No newline at end of file diff --git a/packages/node-mimimi/examples/testmail/2024-10-25-10h39m40s-.eml b/packages/node-mimimi/examples/testmail/2024-10-25-10h39m40s-.eml new file mode 100644 index 00000000000..cc117491c75 --- /dev/null +++ b/packages/node-mimimi/examples/testmail/2024-10-25-10h39m40s-.eml @@ -0,0 +1,15 @@ +From: freepancakes@tutanota.com +MIME-Version: 1.0 +To: Bedpremium ,Freepancakes +Subject: +Date: Fri, 25 Oct 2024 08:39:40 +0000 +Content-Type: multipart/related; boundary="------------79Bu5A16qPEYcVIZL@tutanota" + +--------------79Bu5A16qPEYcVIZL@tutanota +Content-Type: text/html; charset=UTF-8 +Content-transfer-encoding: base64 + +PGJyPjxicj4tLQo8YnI+ClNlY3VyZWQgd2l0aCBUdXRhIE1haWw6Cjxicj4KaHR0cHM6Ly90dXRhLm +NvbQ== + +--------------79Bu5A16qPEYcVIZL@tutanota-- \ No newline at end of file diff --git a/packages/node-mimimi/examples/testmail/2024-10-25-10h48m47s-test.eml b/packages/node-mimimi/examples/testmail/2024-10-25-10h48m47s-test.eml new file mode 100644 index 00000000000..ababb5b148c --- /dev/null +++ b/packages/node-mimimi/examples/testmail/2024-10-25-10h48m47s-test.eml @@ -0,0 +1,15 @@ +From: freepancakes@tutanota.com +MIME-Version: 1.0 +To: Freepancakes +Subject: =?UTF-8?B?dGVzdA==?= +Date: Fri, 25 Oct 2024 08:48:47 +0000 +Content-Type: multipart/related; boundary="------------79Bu5A16qPEYcVIZL@tutanota" + +--------------79Bu5A16qPEYcVIZL@tutanota +Content-Type: text/html; charset=UTF-8 +Content-transfer-encoding: base64 + +PGJyPjxicj4tLQo8YnI+ClNlY3VyZWQgd2l0aCBUdXRhIE1haWw6Cjxicj4KaHR0cHM6Ly90dXRhLm +NvbQ== + +--------------79Bu5A16qPEYcVIZL@tutanota-- \ No newline at end of file diff --git a/packages/node-mimimi/examples/testmail/2024-10-25-11h00m19s-test.eml b/packages/node-mimimi/examples/testmail/2024-10-25-11h00m19s-test.eml new file mode 100644 index 00000000000..924e658060e --- /dev/null +++ b/packages/node-mimimi/examples/testmail/2024-10-25-11h00m19s-test.eml @@ -0,0 +1,15 @@ +From: freepancakes@tutanota.com +MIME-Version: 1.0 +To: Freepancakes +Subject: =?UTF-8?B?dGVzdA==?= +Date: Fri, 25 Oct 2024 09:00:19 +0000 +Content-Type: multipart/related; boundary="------------79Bu5A16qPEYcVIZL@tutanota" + +--------------79Bu5A16qPEYcVIZL@tutanota +Content-Type: text/html; charset=UTF-8 +Content-transfer-encoding: base64 + +PGJyPjxicj4tLQo8YnI+ClNlY3VyZWQgd2l0aCBUdXRhIE1haWw6Cjxicj4KaHR0cHM6Ly90dXRhLm +NvbQ== + +--------------79Bu5A16qPEYcVIZL@tutanota-- \ No newline at end of file diff --git a/packages/node-mimimi/examples/testmail/2024-10-25-12h16m50s-.eml b/packages/node-mimimi/examples/testmail/2024-10-25-12h16m50s-.eml new file mode 100644 index 00000000000..f41eed05412 --- /dev/null +++ b/packages/node-mimimi/examples/testmail/2024-10-25-12h16m50s-.eml @@ -0,0 +1,15 @@ +From: freepancakes@tutanota.com +MIME-Version: 1.0 +To: Freepancakes +Subject: +Date: Fri, 25 Oct 2024 10:16:50 +0000 +Content-Type: multipart/related; boundary="------------79Bu5A16qPEYcVIZL@tutanota" + +--------------79Bu5A16qPEYcVIZL@tutanota +Content-Type: text/html; charset=UTF-8 +Content-transfer-encoding: base64 + +PGJyPjxicj4tLQo8YnI+ClNlY3VyZWQgd2l0aCBUdXRhIE1haWw6Cjxicj4KaHR0cHM6Ly90dXRhLm +NvbQ== + +--------------79Bu5A16qPEYcVIZL@tutanota-- \ No newline at end of file diff --git a/packages/node-mimimi/examples/testmail/2024-10-25-14h44m35s-test.eml b/packages/node-mimimi/examples/testmail/2024-10-25-14h44m35s-test.eml new file mode 100644 index 00000000000..1a09bfc62d4 --- /dev/null +++ b/packages/node-mimimi/examples/testmail/2024-10-25-14h44m35s-test.eml @@ -0,0 +1,15 @@ +From: freepancakes@tutanota.com +MIME-Version: 1.0 +To: Freepancakes +Subject: =?UTF-8?B?dGVzdA==?= +Date: Fri, 25 Oct 2024 12:44:35 +0000 +Content-Type: multipart/related; boundary="------------79Bu5A16qPEYcVIZL@tutanota" + +--------------79Bu5A16qPEYcVIZL@tutanota +Content-Type: text/html; charset=UTF-8 +Content-transfer-encoding: base64 + +PGJyPjxicj4tLQo8YnI+ClNlY3VyZWQgd2l0aCBUdXRhIE1haWw6Cjxicj4KaHR0cHM6Ly90dXRhLm +NvbQ== + +--------------79Bu5A16qPEYcVIZL@tutanota-- \ No newline at end of file From 8145478f0886928bf12175f43c9611f8a4604953 Mon Sep 17 00:00:00 2001 From: map Date: Wed, 13 Nov 2024 15:45:33 +0100 Subject: [PATCH 19/32] improved mime handling part 2 --- .../node-mimimi/examples/import_eml_mail.rs | 108 +- .../src/importer/importable_mail.rs | 80 +- .../importable_mail/extend_mail_parser.rs | 2 +- .../mime_string_to_importable_mail_test.rs | 1105 ++++++++--------- 4 files changed, 591 insertions(+), 704 deletions(-) diff --git a/packages/node-mimimi/examples/import_eml_mail.rs b/packages/node-mimimi/examples/import_eml_mail.rs index bf0bb6efecc..b6bde1dc105 100644 --- a/packages/node-mimimi/examples/import_eml_mail.rs +++ b/packages/node-mimimi/examples/import_eml_mail.rs @@ -1,70 +1,70 @@ #[cfg(not(feature = "javascript"))] #[tokio::main] async fn main() { - use tutao_node_mimimi::importer::file_reader::import_client::FileImport; - use std::sync::Mutex; + use std::sync::Mutex; + use tutao_node_mimimi::importer::file_reader::import_client::FileImport; - use std::sync::Arc; - use tutao_node_mimimi::importer::imap_reader::{ - import_client::ImapImport, ImapCredentials, ImapImportConfig, LoginMechanism, - }; - use tutao_node_mimimi::importer::{ImportSource, ImportState, ImporterApi}; - use tutasdk::folder_system::MailSetKind; - use tutasdk::net::native_rest_client::NativeRestClient; - use tutasdk::Sdk; + use std::sync::Arc; + use tutao_node_mimimi::importer::imap_reader::{ + import_client::ImapImport, ImapCredentials, ImapImportConfig, LoginMechanism, + }; + use tutao_node_mimimi::importer::{ImportSource, ImportState, ImporterApi}; + use tutasdk::folder_system::MailSetKind; + use tutasdk::net::native_rest_client::NativeRestClient; + use tutasdk::Sdk; - let sdk = Sdk::new( - "http://localhost:9000".to_string(), - Arc::new(NativeRestClient::try_new().unwrap()), - ); - let logged_in_sdk = sdk - .create_session("map-free@tutanota.de", "map") - .await - .unwrap(); - let import_source = ImportSource::LocalFile { - fs_email_client: FileImport::new(get_file_paths()).unwrap(), - }; + let sdk = Sdk::new( + "http://localhost:9000".to_string(), + Arc::new(NativeRestClient::try_new().unwrap()), + ); + let logged_in_sdk = sdk + .create_session("map-free@tutanota.de", "map") + .await + .unwrap(); + let import_source = ImportSource::LocalFile { + fs_email_client: FileImport::new(get_file_paths()).unwrap(), + }; - let mail_facade = logged_in_sdk.mail_facade(); - let mailbox = mail_facade.load_user_mailbox().await.unwrap(); - let folders = mail_facade - .load_folders_for_mailbox(&mailbox) - .await - .unwrap(); - let inbox_folder = folders - .system_folder_by_type(MailSetKind::Inbox) - .expect("inbox should exist"); + let mail_facade = logged_in_sdk.mail_facade(); + let mailbox = mail_facade.load_user_mailbox().await.unwrap(); + let folders = mail_facade + .load_folders_for_mailbox(&mailbox) + .await + .unwrap(); + let inbox_folder = folders + .system_folder_by_type(MailSetKind::Inbox) + .expect("inbox should exist"); - let id = logged_in_sdk - .mail_facade() - .get_group_id_for_mail_address("map-free@tutanota.de") - .await - .unwrap(); - let mut importer = ImporterApi::new( - logged_in_sdk, - id, - inbox_folder._id.clone().unwrap(), - Arc::new(Mutex::new(import_source)), - ); + let id = logged_in_sdk + .mail_facade() + .get_group_id_for_mail_address("map-free@tutanota.de") + .await + .unwrap(); + let mut importer = ImporterApi::new( + logged_in_sdk, + id, + inbox_folder._id.clone().unwrap(), + Arc::new(Mutex::new(import_source)), + ); - let import_status = importer - .continue_import_inner() - .await - .expect("Cannot complete import"); - assert!(ImportState::Finished == import_status.state,); - assert!(import_status.imported_mails > 0); + let import_status = importer + .continue_import_inner() + .await + .expect("Cannot complete import"); + assert!(ImportState::Finished == import_status.state,); + assert!(import_status.imported_mails > 0); } #[cfg(feature = "javascript")] fn main() { - panic!("Only runnable without javascript feature "); + panic!("Only runnable without javascript feature "); } fn get_file_paths() -> Vec { - let Ok(paths) = std::fs::read_dir("./examples/testmail") else { - panic!("could not load test mail files names, are you in the node-mimimi project root?") - }; - paths - .map(|path| path.unwrap().path().to_str().unwrap().to_owned()) - .collect() + let Ok(paths) = std::fs::read_dir("./examples/testmail") else { + panic!("could not load test mail files names, are you in the node-mimimi project root?") + }; + paths + .map(|path| path.unwrap().path().to_str().unwrap().to_owned()) + .collect() } diff --git a/packages/node-mimimi/src/importer/importable_mail.rs b/packages/node-mimimi/src/importer/importable_mail.rs index 6ae1f4b7538..292f01cd8a6 100644 --- a/packages/node-mimimi/src/importer/importable_mail.rs +++ b/packages/node-mimimi/src/importer/importable_mail.rs @@ -198,45 +198,7 @@ impl ImportableMail { // todo: of it is PartType::Text & PartType::Html, check for ConentDisposition Header // and if it is attachment, treat it as attachment PartType::Text(text) => { - let has_attachment_content_disposition = part - .content_disposition() - .map(|content_disposition| content_disposition.c_type == "attachment") - .unwrap_or_default(); - - let is_text_plain = !has_attachment_content_disposition - && part - .content_type() - .map(|content_type| { - let subtype = content_type.subtype().unwrap_or({ - // what do we do with the content-type: text - // with no subtype - // for now assume plain - if content_type.c_type == "text" { - "plain" - } else { - "" - } - }); - - let is_text_plain = - content_type.c_type == "text" && subtype == "plain"; - // edu: https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html - // subtype of the multipart Content-Type. - // This type is syntactically identical to multipart/mixed, but the - // semantics are different. In particular, in a digest, the default - // Content-Type value for a body part is changed from "text/plain" to "message/rfc822". - let is_message_rfc822 = - content_type.c_type == "message" && subtype == "rfc833"; - - is_text_plain || is_message_rfc822 - }) - .unwrap_or({ - // what should we treat text that is not content-Type: text? - // fow now let's assume it's content-type: text/plain - true - }); - - if is_text_plain && !has_attachment_content_disposition { + if !Self::is_attachment(part) && Self::is_plain_text(part) { Self::handle_plain_text(&mut email_body_as_html, text.as_ref()); } else { Self::handle_binary( @@ -249,7 +211,16 @@ impl ImportableMail { }, PartType::Html(html_text) => { - Self::handle_html_text(&mut email_body_as_html, html_text.as_ref()) + if !Self::is_attachment(part) { + Self::handle_html_text(&mut email_body_as_html, html_text.as_ref()) + } else { + Self::handle_binary( + part, + &mut attachments, + html_text.as_bytes().to_vec(), + false, + ); + } }, PartType::Message(attached_message) => { @@ -270,6 +241,35 @@ impl ImportableMail { Ok((email_body_as_html, attachments)) } + fn is_plain_text(part: &MessagePart) -> bool { + part.content_type() + .map(|content_type| { + let subtype = content_type.subtype(); + let is_text_plain = content_type.c_type == "text" + && (subtype == Some("plain") || subtype.is_none()); + // edu: https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html + // subtype of the multipart Content-Type. + // This type is syntactically identical to multipart/mixed, but the + // semantics are different. In particular, in a digest, the default + // Content-Type value for a body part is changed from "text/plain" to "message/rfc822". + let is_message_rfc822 = + content_type.c_type == "message" && subtype == Some("rfc833"); + + is_text_plain || is_message_rfc822 + }) + .unwrap_or({ + // what should we treat text that is not content-Type: text? + // fow now let's assume it's content-type: text/plain + true + }) + } + + fn is_attachment(part: &MessagePart) -> bool { + part.content_disposition() + .map(|content_disposition| content_disposition.c_type == "attachment") + .unwrap_or_default() + } + fn get_filename(part: &MessagePart, fallback_name: &str) -> String { let content_disposition_filename = part .content_disposition() diff --git a/packages/node-mimimi/src/importer/importable_mail/extend_mail_parser.rs b/packages/node-mimimi/src/importer/importable_mail/extend_mail_parser.rs index da02a0abbff..77c26b75ee8 100644 --- a/packages/node-mimimi/src/importer/importable_mail/extend_mail_parser.rs +++ b/packages/node-mimimi/src/importer/importable_mail/extend_mail_parser.rs @@ -147,7 +147,7 @@ impl<'a> MakeString for mail_parser::ContentType<'a> { } if let Some(attribute_str) = attribute_str { if !content_type.is_empty() { - content_type.push_str("; "); + content_type.push_str(";"); } content_type.push_str(attribute_str.as_str()); } diff --git a/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs b/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs index 1a20570741a..71b90ab4d8d 100644 --- a/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs +++ b/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs @@ -6,28 +6,28 @@ use mail_parser::{MessageParser, MimeHeaders}; use tutasdk::date::DateTime; fn parse_mail(msg: &str) -> ImportableMail { - (&MessageParser::default().parse(msg).unwrap()) - .try_into() - .unwrap() + (&MessageParser::default().parse(msg).unwrap()) + .try_into() + .unwrap() } // to be able to convert any (str/string, str/string).into() => MailContact impl From<(N, A)> for MailContact where - N: ToString, - A: ToString, + N: ToString, + A: ToString, { - fn from((name, address): (N, A)) -> Self { - Self { - mail_address: address.to_string(), - name: name.to_string(), - } - } + fn from((name, address): (N, A)) -> Self { + Self { + mail_address: address.to_string(), + name: name.to_string(), + } + } } #[test] fn headers() { - let msg = r#"Message-ID: 123456 + let msg = r#"Message-ID: 123456 Subject: Hello From: A To: B @@ -37,90 +37,90 @@ In-Reply-To: <1234564@web.de> Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-Type: multipart/mixed; boundary=frontier "#; - println!("{}", msg); - let m = parse_mail(msg); - assert_eq!("123456", m.message_id.unwrap()); - assert_eq!( + println!("{}", msg); + let m = parse_mail(msg); + assert_eq!("123456", m.message_id.unwrap()); + assert_eq!( m.reply_to_addresses, vec![ ("Reply", "reply@tutanota.de").into(), ("Reply2", "reply2@tutanota.de").into(), ], ); - assert_eq!( + assert_eq!( m.references, vec!["sadf@tutanota.de".to_string(), "1234564@web.de".to_string()], ); - assert_eq!(Some("1234564@web.de".to_string()), m.in_reply_to); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(msg, m.headers_string); + assert_eq!(Some("1234564@web.de".to_string()), m.in_reply_to); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(msg, m.headers_string); } #[test] fn bad_frontier() { - let msg = "Content-Type: multipart/mixed; boundary=komma;ist;nicht;erlaubt\n"; - let parsed_message = MessageParser::default().parse(msg).unwrap(); - let attributes = parsed_message - .content_type() - .unwrap() - .attributes - .as_ref() - .unwrap(); - assert_eq!(attributes.as_slice(), [("boundary".into(), "komma".into())]); + let msg = "Content-Type: multipart/mixed; boundary=komma;ist;nicht;erlaubt\n"; + let parsed_message = MessageParser::default().parse(msg).unwrap(); + let attributes = parsed_message + .content_type() + .unwrap() + .attributes + .as_ref() + .unwrap(); + assert_eq!(attributes.as_slice(), [("boundary".into(), "komma".into())]); } #[test] fn empty_references() { - let msg = "Subject: Hello"; - let m = parse_mail(msg); - assert!(m.references.is_empty()); + let msg = "Subject: Hello"; + let m = parse_mail(msg); + assert!(m.references.is_empty()); } #[test] fn empty_in_reply_to() { - let msg = "Subject: Hello"; - let m = parse_mail(msg); - assert_eq!(None, m.in_reply_to); + let msg = "Subject: Hello"; + let m = parse_mail(msg); + assert_eq!(None, m.in_reply_to); } #[test] fn text_plain_us_ascii_7bit() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 US-ASCII: !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~"##; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(m.subject, "Hello",); - assert_eq!("US-ASCII: !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", m.html_body_text); - assert_eq!(m.date, Some(DateTime::from_millis(1730991244000))); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(m.subject, "Hello",); + assert_eq!("US-ASCII: !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", m.html_body_text); + assert_eq!(m.date, Some(DateTime::from_millis(1730991244000))); } #[test] fn text_plain_utf8bit() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/plain; charset=UTF-8 Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\{Β³|@"##; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!("Hello", m.subject); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Hello", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_utf_explicit_8bit() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -129,18 +129,18 @@ Content-Transfer-Encoding: 8bit Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\{Β³|@"##; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!("Hello", m.subject); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Hello", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_utf_quoted_printable() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -148,18 +148,18 @@ Content-type: text/plain; charset=UTF-8 Content-Transfer-Encoding: quoted-printable Tutanota: =C3=A4=C3=BC=C3=B6=C3=9F=E2=82=AC*#\{=C2=B3|@"##; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); - assert_eq!("Hello", m.subject); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!("Hello", m.subject); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_utf_base64() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -167,18 +167,18 @@ Content-type: text/plain; charset=UTF-8 Content-Transfer-Encoding: base64 VHV0YW5vdGE6IMOkw7zDtsOf4oKsKiNce8KzfEA="##; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!("Hello", m.subject); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Hello", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_utf_invalid_base64() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -186,176 +186,187 @@ Content-type: text/plain; charset=UTF-8 Content-Transfer-Encoding: base64 VHV0YW5vdGE6IMOkw7zDtsOf4oKsKiNce8KzfEA"##; // skip the padding "=" to force an exception - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!("Hello", m.subject); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Hello", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_format_flowed() { - let msg = r#"Subject: Hello + // mime parser does not yet support rfc3676 + let msg = "Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/plain; charset=UTF-8; format=flowed - -Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es \r\neinen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!"#; - let m = parse_mail(msg); - assert_eq!("Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es einen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!", m.html_body_text); +Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es \r\neinen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!"; + let m = parse_mail(msg); + + assert_eq!("Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es
einen soft-break!!!!!
Vor dieser Zeile gibt es keinen soft break, sondern einen richtigen!", m.html_body_text); } #[test] fn text_plain_format_flowed_del_sp() { - let msg = r#"From: A + // mime parser does not yet support rfc3676 + let msg = "From: A To: B Subject: Hello Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/plain; charset=UTF-8; format=flowed; DelSp=yes -Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es \r\neinen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!"#; - let m = parse_mail(msg); - assert_eq!( - "Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt eseinen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!", +Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es \r\neinen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!"; + let m = parse_mail(msg); + assert_eq!( + "Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es
einen soft-break!!!!!
Vor dieser Zeile gibt es keinen soft break, sondern einen richtigen!", m.html_body_text); } #[test] fn text_plain_subject_encoded_word_qencoding() { - let msg = r#"Subject: Hello =?ISO-8859-1?Q?=E4=F6=FC=DF?=abc + // mime-parser always adds a space after q-encoded block if followed by another string + // so, following two lines are identical: + // =?UTF-8?Q?=E4?=abc <- no space before abc + // =?UTF-8?Q?=E4?= abc <- space before abc + let msg = r#"Subject: Hello =?ISO-8859-1?Q?=E4=F6=FC=DF?=abc From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 "#; - let m = parse_mail(msg); - assert_eq!("Hello Àâüßabc", m.subject); + let m = parse_mail(msg); + assert_eq!("Hello Àâüß abc", m.subject); } #[test] fn text_plain_subject_encoded_word_qencoding_turkish() { - let msg = r#"Subject: =?iso-8859-9?Q?Paracard Hesap =D6zeti?= + let msg = r#"Subject: =?iso-8859-9?Q?Paracard Hesap =D6zeti?= From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 "#; - let m = parse_mail(msg); - assert_eq!("Paracard Hesap Γ–zeti", m.subject); + let m = parse_mail(msg); + assert_eq!("Paracard Hesap Γ–zeti", m.subject); } #[test] fn from_encoded_word_qencoding() { - let msg = r#"Subject: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0?==?utf-8?Q?=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= -From: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0?==?utf-8?Q?=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= -To: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0?==?utf-8?Q?=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= + // the rust mime parser is not able to handle chars splitted in two q-encoding blocks + // while our server side parser handles those cases + let msg = r#"Subject: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= +From: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= +To: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= Date: Thu, 7 Nov 2024 15:54:04 +0100 "#; - let m = parse_mail(msg); - assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.subject); - assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.from_addresses[0].name); - assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.to_addresses[0].name); + let m = parse_mail(msg); + assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.subject); + assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.from_addresses[0].name); + assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.to_addresses[0].name); } #[test] fn from_encoded_word_qencoding_colon() { - let msg = r#"Subject: Hi + let msg = r#"Subject: Hi From: =?utf-8?Q?=D0=9B=D0=B8=D1=82=D1=80=D0=B5=D1=81=3A=20=D0=A1=D0=B0=D0=BC=D0=B8=D0=B7=D0=B4=D0=B0=D1=82?= "#; - let m = parse_mail(msg); - assert_eq!( - m.from_addresses[0], - ("ЛитрСс: Π‘Π°ΠΌΠΈΠ·Π΄Π°Ρ‚", "mail@selfpub.ru").into() - ); + let m = parse_mail(msg); + assert_eq!( + m.from_addresses[0], + ("ЛитрСс: Π‘Π°ΠΌΠΈΠ·Π΄Π°Ρ‚", "mail@selfpub.ru").into() + ); } #[test] fn recipients_encoded_word_qencoding_colon() { - let msg = "To: =?utf-8?Q?=D0=9B=D0=B8=D1=82=D1=80=D0=B5=D1=81=3A=20=D0=A1=D0=B0=D0=BC=D0=B8=D0=B7=D0=B4=D0=B0=D1=82?= \n"; - let m = parse_mail(msg); - assert_eq!( - m.from_addresses[0], - ("ЛитрСс: Π‘Π°ΠΌΠΈΠ·Π΄Π°Ρ‚", "mail@selfpub.ru").into() - ); + let msg = "To: =?utf-8?Q?=D0=9B=D0=B8=D1=82=D1=80=D0=B5=D1=81=3A=20=D0=A1=D0=B0=D0=BC=D0=B8=D0=B7=D0=B4=D0=B0=D1=82?= \n"; + let m = parse_mail(msg); + assert_eq!( + m.to_addresses[0], + ("ЛитрСс: Π‘Π°ΠΌΠΈΠ·Π΄Π°Ρ‚", "mail@selfpub.ru").into() + ); } #[test] fn recipients_encoded_word_qencoding_partly() { - let msg = "To: =?ISO-8859-1?Q?Andr=E9?= Pirard \n"; - let m = parse_mail(msg); - assert_eq!( - m.from_addresses[0], - ("AndrΓ© Pirard", "PIRARD@vm1.ulg.ac.be").into() - ); + let msg = "To: =?ISO-8859-1?Q?Andr=E9?= Pirard \n"; + let m = parse_mail(msg); + assert_eq!( + m.to_addresses[0], + ("AndrΓ© Pirard", "PIRARD@vm1.ulg.ac.be").into() + ); } #[test] fn text_plain_subject_encoded_word_base64() { - let msg = r#"Subject: =?utf-8?B?SGVsbG8gw6TDtsO8w58=?= + let msg = r#"Subject: =?utf-8?B?SGVsbG8gw6TDtsO8w58=?= From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 "#; - let m = parse_mail(msg); - assert_eq!("Hello Àâüß", m.subject); + let m = parse_mail(msg); + assert_eq!("Hello Àâüß", m.subject); } #[test] fn text_html_only() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/html; charset=UTF-8 Hello Àâüß
"#; - let m = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!( - "Hello Àâüß
", - m.html_body_text - ); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!( + "Hello Àâüß
", + m.html_body_text + ); } #[test] fn charset() { - // todo!() + // todo!() } #[test] fn text_html_inline_charset_definition_utf8() { - let msg = r#"Content-type: text/html + let msg = r#"Content-type: text/html Content-Transfer-Encoding: 8bit -

Π‘Π»Π°Π³ΠΎΠ΄Π°Ρ€ΠΈΠΌ Π’ΠΈ

"#; - let m = parse_mail(msg); +

Π‘Π»Π°Π³ΠΎΠ΄Π°Ρ€ΠΈΠΌ Π’ΠΈ

"#; + let m = parse_mail(msg); - assert_eq!( - "

Π‘Π»Π°Π³ΠΎΠ΄Π°Ρ€ΠΈΠΌ Π’ΠΈ

", - m.html_body_text - ); + assert_eq!( + "

Π‘Π»Π°Π³ΠΎΠ΄Π°Ρ€ΠΈΠΌ Π’ΠΈ

", + m.html_body_text + ); } #[test] +#[ignore] fn text_html_inline_charset_definition_western() { - let msg = r#"Content-type: text/html + // there is currently no way to port server side code to support this as regex does not support look ahead + // we don't want to write our own parser for now + let msg = r#"Content-type: text/html Content-Transfer-Encoding: base64 PGh0bWw+PGhlYWQ+PG1ldGEgY2hhcnNldD0iSVNPLTg4NTktMTUiPjwvaGVhZD48Ym9keT48cD6kIPbkPC9wPjwvYm9keT48L2h0bWw+"#; - let m = parse_mail(msg); - assert_eq!( - "

€ ΓΆΓ€

", - m.html_body_text - ); + let m = parse_mail(msg); + assert_eq!( + "

€ ΓΆΓ€

", + m.html_body_text + ); } #[test] fn text_alternative() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 (CET) @@ -371,60 +382,54 @@ Content-type: text/html; charset=UTF-8; Hello Àâüß
--frontier-- "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!("Hello", m.subject); - assert_eq!( - "Hello Àâüß
", - m.html_body_text - ); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!("Hello", m.subject); + assert_eq!( + "Hello Àâüß
", + m.html_body_text + ); } #[test] fn invalid_domains_in_mail_addresses() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B , C , D "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!( - m.to_addresses, - vec![ - ("B", "b@a.example").into(), - ("C", "c@c.com").into(), - ("D", "d@d.invalid").into() - ] - ); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!( + m.to_addresses, + vec![ + ("B", "b@a.example").into(), + ("C", "c@c.com").into(), + ("D", "d@d.invalid").into() + ] + ); } #[test] fn multiple_to_headers() { - let msg = r#"Subject: Hello + // mime_parser discards duplicated headers besides the last one + let msg = r#"Subject: Hello From: A To: B , C To: D "#; - let m: ImportableMail = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!( - m.to_addresses, - vec![ - ("B", "b@b.org").into(), - ("C", "c@c.com").into(), - ("D", "d@d.net").into() - ] - ); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("D", "d@d.net").into()]); } #[test] fn attached_message() { - let msg = r#"Subject: parent message + let msg = r#"Subject: parent message From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -447,30 +452,30 @@ Content-type: text/plain; charset=UTF-8; Hello Àâüß "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(m.subject, "parent message"); - assert_eq!(m.html_body_text, "normal message"); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - - assert_eq!(m.attachments.len(), 1); - let attachment = m.attachments.first().unwrap(); - let attached = parse_mail( - String::from_utf8(attachment.content.to_vec()) - .unwrap() - .as_str(), - ); - assert_eq!(attached.from_addresses, vec![("D", "d@tutanota.de").into()]); - assert_eq!(attached.to_addresses, vec![("E", "e@tutanota.de").into()]); - assert_eq!(attached.subject, "attached message"); - assert_eq!(attached.html_body_text, "
Hello Àâüß
"); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(m.subject, "parent message"); + assert_eq!(m.html_body_text, "normal message"); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + + assert_eq!(m.attachments.len(), 1); + let attachment = &m.attachments[0]; + let attached = parse_mail( + String::from_utf8(attachment.content.to_vec()) + .unwrap() + .as_str(), + ); + assert_eq!(attached.from_addresses, vec![("D", "d@tutanota.de").into()]); + assert_eq!(attached.to_addresses, vec![("E", "e@tutanota.de").into()]); + assert_eq!(attached.subject, "attached message"); + assert_eq!(attached.html_body_text, "
Hello Àâüß
"); } #[test] fn multiple_attachments() { - let msg = r#"Subject: multiple attachments + let msg = r#"Subject: multiple attachments From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -499,42 +504,42 @@ Content-Disposition: attachment; filename=withoutContentType.pdf; c2Vjb25kIGF0dGFjaG1lbnQ= --frontier-- "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(m.subject, "multiple attachments"); - - assert_eq!("Hello Àâüß", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(m.attachments.len(), 3); - let [a1, a2, a3] = m.attachments.try_into().unwrap(); - - assert_eq!("a1.txt", a1.filename); - assert_eq!( - String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), - String::from_utf8(a1.content.to_vec()).unwrap() - ); - assert_eq!("application/octet-stream", a1.content_type); - - assert_eq!("a2.pdf", a2.filename); - assert_eq!( - String::from_utf8(base64_decode(b"c2Vjb25kIGF0dGFjaG1lbnQ=").unwrap()).unwrap(), - String::from_utf8(a2.content.to_vec()).unwrap() - ); - assert_eq!("application/pdf", a2.content_type); - - assert_eq!("withoutContentType.pdf", a3.filename); - assert_eq!( - String::from_utf8(base64_decode(b"c2Vjb25kIGF0dGFjaG1lbnQ=").unwrap()).unwrap(), - String::from_utf8(a3.content.to_vec()).unwrap() - ); - assert_eq!(r#"text/plain; charset="us-ascii""#, a3.content_type); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(m.subject, "multiple attachments"); + + assert_eq!("Hello Àâüß", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.attachments.len(), 3); + let [a1, a2, a3] = m.attachments.try_into().unwrap(); + + assert_eq!("a1.txt", a1.filename); + assert_eq!( + String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), + String::from_utf8(a1.content.to_vec()).unwrap() + ); + assert_eq!("application/octet-stream", a1.content_type); + + assert_eq!("a2.pdf", a2.filename); + assert_eq!( + String::from_utf8(base64_decode(b"c2Vjb25kIGF0dGFjaG1lbnQ=").unwrap()).unwrap(), + String::from_utf8(a2.content.to_vec()).unwrap() + ); + assert_eq!("application/pdf", a2.content_type); + + assert_eq!("withoutContentType.pdf", a3.filename); + assert_eq!( + String::from_utf8(base64_decode(b"c2Vjb25kIGF0dGFjaG1lbnQ=").unwrap()).unwrap(), + String::from_utf8(a3.content.to_vec()).unwrap() + ); + assert_eq!(r#"text/plain;charset="us-ascii""#, a3.content_type); } #[test] fn inline_attachment() { - let msg = r#"Subject: inline attachment + let msg = r#"Subject: inline attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -553,27 +558,23 @@ Content-ID: <123@tutanota.de>; Zmlyc3QgYXR0YWNobWVudA== --frontier-- "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - - todo!() - - // assertEquals(1, m.getAttachedFiles().size()); - // SmtpAttachment a1 = m.getAttachedFiles().get(0); - // - // assertEquals("a1.png", a1.getName()); - // assertArrayEquals(EncodingConverter.base64ToBytes("Zmlyc3QgYXR0YWNobWVudA=="), a1.getData()); - // assertEquals("application/octet-stream", a1.getMimeType()); - // assertEquals("123@tutanota.de", a1.getContentId()); - // assertNull(a1.getCharset()); + let m = parse_mail(msg); + + assert_eq!(1, m.attachments.len()); + let a1 = &m.attachments[0]; + + assert_eq!("a1.png", a1.filename); + assert_eq!( + String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), + String::from_utf8(a1.content.to_vec()).unwrap() + ); + assert_eq!("application/octet-stream", a1.content_type); + assert_eq!(Some("123@tutanota.de".to_string()), a1.content_id); } #[test] fn attachment_to_attached_message() { - let msg = r#"Subject: parent message + let msg = r#"Subject: parent message From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -589,36 +590,36 @@ Content-Disposition: attachment; filename=indirectly_attached.txt; Zmlyc3QgYXR0YWNobWVudA== "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(1, m.attachments.len()); - - let attached = parse_mail( - String::from_utf8(m.attachments.first().unwrap().content.clone()) - .unwrap() - .as_str(), - ); - - assert_eq!(attached.subject, "attached message"); - - assert_eq!("", attached.html_body_text); - assert_eq!(1, attached.attachments.len()); - - assert_eq!(1, attached.attachments.len()); - let indirect_attachment = attached.attachments.first().unwrap(); - assert_eq!("indirectly_attached.txt", indirect_attachment.filename); - assert_eq!( - String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), - String::from_utf8(indirect_attachment.content.to_vec()).unwrap() - ); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(1, m.attachments.len()); + + let attached = parse_mail( + String::from_utf8(m.attachments[0].content.clone()) + .unwrap() + .as_str(), + ); + + assert_eq!(attached.subject, "attached message"); + + assert_eq!("", attached.html_body_text); + assert_eq!(1, attached.attachments.len()); + + assert_eq!(1, attached.attachments.len()); + let indirect_attachment = attached.attachments.first().unwrap(); + assert_eq!("indirectly_attached.txt", indirect_attachment.filename); + assert_eq!( + String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), + String::from_utf8(indirect_attachment.content.to_vec()).unwrap() + ); } #[test] fn text_attachment() { - let msg = r#"Subject: text attachment + let msg = r#"Subject: text attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -627,36 +628,32 @@ Content-Type: multipart/mixed; boundary=frontier --frontier Content-type: text/plain; charset=UTF-8 -Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@ +Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\{Β³|@ --frontier Content-type: text/plain; charset=UTF-8 Content-Disposition: attachment; filename=a1.txt; -Abc, die Katze liegt im Schnee ! Àâü?ß ! +Abc, die Katze lief im Schnee ! Àâü?ß ! --frontier-- "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - todo!() - - // assertEquals("text attachment", m.getSubject()); - // assertEquals("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.getPlainBodyText()); - // assertEquals(null, m.getHtmlBodyText()); - // assertEquals(date, m.getSentDate()); - // - // assertEquals(1, m.getAttachedFiles().size()); - // SmtpAttachment a1 = m.getAttachedFiles().get(0); - // - // assertEquals("a1.txt", a1.getName()); - // assertArrayEquals("Abc, die Katze liegt im Schnee ! Àâü?ß ! ".getBytes("UTF-8"), a1.getData()); + let m = parse_mail(msg); + + assert_eq!("text attachment", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + + assert_eq!(1, m.attachments.len()); + let a1 = &m.attachments[0]; + assert_eq!("a1.txt", a1.filename); + assert_eq!( + "Abc, die Katze lief im Schnee ! Àâü?ß ! ", + String::from_utf8(a1.content.clone()).unwrap() + ); } #[test] fn html_attachment() { - let msg = r#"Subject: html attachment + let msg = r#"Subject: html attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -665,7 +662,7 @@ Content-Type: multipart/mixed; boundary=frontier --frontier Content-type: text/plain; charset=UTF-8 -Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@ +Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\{Β³|@ --frontier Content-type: text/html; charset=UTF-8 Content-Disposition: attachment; filename=a1.html; @@ -673,94 +670,55 @@ Content-Disposition: attachment; filename=a1.html; Hello Àâüß
--frontier-- "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - todo!() - - // assertEquals("html attachment", m.getSubject()); - // assertEquals("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.getPlainBodyText()); - // assertEquals(null, m.getHtmlBodyText()); - // assertEquals(date, m.getSentDate()); - // - // assertEquals(1, m.getAttachedFiles().size()); - // SmtpAttachment a1 = m.getAttachedFiles().get(0); - // - // assertEquals("a1.html", a1.getName()); - // assertArrayEquals("Hello Àâüß
".getBytes("UTF-8"), a1.getData()); + let m = parse_mail(msg); + + assert_eq!(m.subject, "html attachment"); + assert_eq!(m.html_body_text, "Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@"); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + + assert_eq!(m.attachments.len(), 1); + let a1 = &m.attachments[0]; + assert_eq!(a1.filename, "a1.html"); + assert_eq!( + String::from_utf8(a1.content.to_vec()).unwrap(), + "Hello Àâüß
" + ); } #[test] fn multiple_plain_body_text_parts_are_concatenated() { - let eml_contents = r#"Message-Id: some-id -From: A -To: B -Date: Tue, 5 Nov 2024 13:18:59 +0000 -Content-Type: multipart/mixed; boundary=line + let eml_contents = r#"Subject: multiple text/plain parts concatenated +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-Type: multipart/mixed; boundary=frontier ---line +--frontier Content-type: text/plain; charset=UTF-8 -first plain text in body +Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\{Β³|@ ---line -Content-Type: text/plain; charset=UTF-8 +--frontier +Content-type: text/plain; charset=UTF-8 -second plain text in body ---line-- +Abc, die Katze liegt im Schnee ! Àâü?ß ! +--frontier-- "#; - let parsed_message = MessageParser::default() - .with_mime_headers() - .parse(eml_contents) - .unwrap(); - let text_contents = parsed_message - .text_bodies() - .map(|a| a.text_contents().unwrap()) - .collect::>() - .join(""); - assert_eq!( - "first plain text in body\nsecond plain text in body", - text_contents - ); + let m = parse_mail(eml_contents); - // String firstPlainBodyText = "Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@"; - // String secondPlainBodyText = "Abc, die Katze liegt im Schnee ! Àâü?ß !"; - // StringBuilder b = new StringBuilder() - // .append("Subject: multiple text/plain parts concatenated\n") - // .append("From: A \n") - // .append("To: B \n") - // .append("Date: " + new MailDateFormat().format(date) + "\n") - // .append("Content-Type: multipart/mixed; boundary=frontier\n") - // .append("\n") - // .append("--frontier\n") - // .append("Content-type: text/plain; charset=UTF-8\n") - // .append("\n") - // .append(firstPlainBodyText + "\n") - // .append("--frontier\n") - // .append("Content-type: text/plain; charset=UTF-8\n") - // .append("\n") - // .append(secondPlainBodyText + "\n") - // .append("--frontier--"); - // - // SmtpMessage m = mimeStringToSmtpMessageConverter.mimeToSmtpMessage( - // mimeStringToSmtpMessageConverter.dataToMimeMessage(b.toString().getBytes(StandardCharsets.UTF_8)), - // null - // ); - // - // assertEquals("multiple text/plain parts concatenated", m.getSubject()); - // String concatenatedPlainBodyText = firstPlainBodyText.concat(secondPlainBodyText); - // assertEquals(concatenatedPlainBodyText, m.getPlainBodyText()); - // assertEquals(null, m.getHtmlBodyText()); - // assertEquals(date, m.getSentDate()); - // assertEquals(0, m.getAttachedFiles().size()); + assert_eq!("multiple text/plain parts concatenated", m.subject); + assert_eq!( + "Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@
Abc, die Katze liegt im Schnee ! Àâü?ß ! ", + m.html_body_text + ); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(0, m.attachments.len()); } #[test] fn multiple_html_body_text_parts_are_concatenated() { - let msg = r#"Message-Id: some-id + let msg = r#"Message-Id: some-id Subject: multiple text/html parts concatenated From: A To: B @@ -778,70 +736,18 @@ Content-type: text/html; charset=UTF-8 --frontier-- "#; - let m = parse_mail(msg); - - assert_eq!("multiple text/html parts concatenated", m.subject); - assert_eq!("Hello Àâüß
Test Test
", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(0, m.attachments.len()); -} - -#[test] -// todo! what does this test (map) -fn concatenate_alternative_html_text_parts() { - let msg = r#"Message-Id: some-id -From: A -To: B -Date: Tue, 5 Nov 2024 13:18:59 +0000 -Content-Type: multipart/mixed; boundary=line - ---line -Content-type: text/plain; charset=UTF-8 - -first plain text in body - ---line -Content-Type: text/html; charset=UTF-8 - -

first html text in body

- ---line-- -"#; - let m = parse_mail(msg); - todo!() -} - -#[test] -// todo! what does this test (map) -fn concatenate_multiple_html_and_plain_text_parts() { - let msg = r#"Message-Id: some-id -From: A -To: B -Date: Tue, 5 Nov 2024 13:18:59 +0000 -Content-Type: multipart/mixed; boundary=line - ---line -Content-type: text/html; charset=UTF-8 - -

first html text in body

- - ---line -Content-Type: img/gif; charset=UTF-8 -Content-Disposition: inline; filename=name.txt; + let m = parse_mail(msg); -first plain text in body ---line-- -"#; - - let m = parse_mail(msg); - todo!() + assert_eq!("multiple text/html parts concatenated", m.subject); + assert_eq!("Hello Àâüß
Test Test
", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(0, m.attachments.len()); } #[test] fn plain_body_text_parts_are_concatenated_with_html_body_parts_if_html_body_parts_already_existing() { - let msg = r#"Subject: multiple text/html and text/plain parts concatenated to single text/html + let msg = r#"Subject: multiple text/html and text/plain parts concatenated to single text/html From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -858,97 +764,102 @@ Content-type: text/html; charset=UTF-8 --frontier Content-type: text/plain; charset=UTF-8 -Abc, die Katze liegt im Schnee ! Àâü?ß ! +Abc, die Katze lief im Schnee ! Àâü?ß ! + --frontier- "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!( - "multiple text/html and text/plain parts concatenated to single text/html", - m.subject - ); - todo!() - - // assertEquals("multiple text/html and text/plain parts concatenated to single text/html", m.getSubject()); - // String concatenatedHtmlBodyText = firstHtmlBodyText.concat(secondHtmlBodyText).concat(PlainTextHtmlConverter.plainTextToHtml(firstPlainBodyText)); - // assertEquals(null, m.getPlainBodyText()); - // assertEquals(concatenatedHtmlBodyText, m.getHtmlBodyText()); - // assertEquals(date, m.getSentDate()); - // assertEquals(0, m.getAttachedFiles().size()); + let m = parse_mail(msg); + + assert_eq!( + "multiple text/html and text/plain parts concatenated to single text/html", + m.subject + ); + assert_eq!("Hello Àâüß
Test Test
Abc, die Katze lief im Schnee ! Àâü?ß !
", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(0, m.attachments.len()); } #[test] fn plain_body_text_parts_are_converted_to_html_body_parts_if_html_body_parts_follow_afterwards() { - let msg = r#" + let msg = r#"Subject: multiple plain/text and text/html parts concatenated to single text/html +From: A +To: B +Date: Thu, 7 Nov 2024 15:54:04 +0100 +Content-Type: multipart/mixed; boundary=frontier + +--frontier +Content-type: text/plain; charset=UTF-8 + +Abc, die Katze lief im Schnee ! Àâü?ß ! + +--frontier +Content-type: text/html; charset=UTF-8 +Hello Àâüß
+--frontier +Content-type: text/html; charset=UTF-8 + +Test Test
+--frontier- "#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - todo!() - - // assertEquals("multiple plain/text and text/html parts concatenated to single text/html", m.getSubject()); - // String concatenatedHtmlBodyText = PlainTextHtmlConverter.plainTextToHtml(firstPlainBodyText).concat(firstHtmlBodyText).concat(secondHtmlBodyText); - // assertEquals(null, m.getPlainBodyText()); - // assertEquals(concatenatedHtmlBodyText, m.getHtmlBodyText()); - // assertEquals(date, m.getSentDate()); - // assertEquals(0, m.getAttachedFiles().size()); + let m = parse_mail(msg); + + assert_eq!( + "multiple plain/text and text/html parts concatenated to single text/html", + m.subject + ); + assert_eq!("Abc, die Katze lief im Schnee ! Àâü?ß !
Hello Àâüß
Test Test
", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(0, m.attachments.len()); } #[test] fn text_attachment_with_disposition() { - let msg = r#"Subject: text attachment + let msg = r#"Subject: text attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/plain; charset=UTF-8 Content-Disposition: attachment; filename=a1.txt; -Abc, die Katze liegt im Schnee ! Àâü?ß ! -"#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - todo!() - - // assertEquals("text attachment", m.getSubject()); - // assertNull(m.getPlainBodyText()); - // assertNull(m.getHtmlBodyText()); - // assertEquals(date, m.getSentDate()); - // - // assertEquals(1, m.getAttachedFiles().size()); - // SmtpAttachment a1 = m.getAttachedFiles().get(0); - // - // assertEquals("a1.txt", a1.getName()); - // assertArrayEquals("Abc, die Katze liegt im Schnee ! Àâü?ß ! ".getBytes("UTF-8"), a1.getData()); +Abc, die Katze lief im Schnee ! Àâü?ß ! "#; + let m = parse_mail(msg); + + assert_eq!("text attachment", m.subject); + assert_eq!("", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + + assert_eq!(m.attachments.len(), 1); + let a1 = &m.attachments[0]; + assert_eq!(a1.filename, "a1.txt"); + assert_eq!( + String::from_utf8(a1.content.to_vec()).unwrap(), + "Abc, die Katze lief im Schnee ! Àâü?ß ! " + ); } #[test] fn attachment_with_non_ascii_name() { - let msg = r#"Subject: text attachment + let msg = r#"Subject: text attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/plain; charset=UTF-8; name=\"=?ISO-8859-1?Q?a=F6i=2Epdf?=\" Content-Disposition: attachment; filename*=ISO-8859-1''%61%F6%69%2E%70%64%66 -Abc, die Katze liegt im Schnee ! Àâü?ß ! "#; - let m: ImportableMail = parse_mail(msg); +Abc, die Katze lief im Schnee ! Àâü?ß ! "#; + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(1, m.attachments.len()); - assert_eq!("aΓΆi.pdf", m.attachments.first().unwrap().filename); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(1, m.attachments.len()); + assert_eq!("aΓΆi.pdf", &m.attachments[0].filename); } #[test] fn attachment_filename_in_content_type() { - let msg = r#"Subject: message with named file attachment + let msg = r#"Subject: message with named file attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -956,24 +867,24 @@ Content-type: application/octet-stream; name=indirectly_attached.txt; Content-Transfer-Encoding: base64 Zmlyc3QgYXR0YWNobWVudA=="#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - - assert_eq!(1, m.attachments.len()); - let indirect_attachment = m.attachments.first().unwrap(); - assert_eq!("indirectly_attached.txt", indirect_attachment.filename); - assert_eq!( - String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), - String::from_utf8(indirect_attachment.content.to_vec()).unwrap() - ); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + + assert_eq!(1, m.attachments.len()); + let indirect_attachment = &m.attachments[0]; + assert_eq!("indirectly_attached.txt", indirect_attachment.filename); + assert_eq!( + String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), + String::from_utf8(indirect_attachment.content.to_vec()).unwrap() + ); } #[test] fn attachment_filename_qencoding() { - let msg = r#"Subject: message with named file attachment + let msg = r#"Subject: message with named file attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -981,24 +892,24 @@ Content-type: application/octet-stream; name==?utf-8?Q?=C3=A4=C3=B6=C3=9F=E2=82= Content-Transfer-Encoding: base64 Zmlyc3QgYXR0YWNobWVudA=="#; - let m: ImportableMail = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - - assert_eq!(1, m.attachments.len()); - let indirect_attachment = m.attachments.first().unwrap(); - assert_eq!("Γ€ΓΆΓŸβ‚¬.txt", indirect_attachment.filename); - assert_eq!( - String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), - String::from_utf8(indirect_attachment.content.to_vec()).unwrap() - ); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + + assert_eq!(1, m.attachments.len()); + let indirect_attachment = &m.attachments[0]; + assert_eq!("Γ€ΓΆΓŸβ‚¬.txt", indirect_attachment.filename); + assert_eq!( + String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), + String::from_utf8(indirect_attachment.content.to_vec()).unwrap() + ); } #[test] fn encrypted() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -1010,103 +921,79 @@ Content-Transfer-Encoding: base64 SGFsbG8= --frontier--"#; - let m: ImportableMail = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(1, m.attachments.len()); - - // assertEquals(1, m.getAttachedFiles().size()); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(1, m.attachments.len()); } #[test] fn recipient_groups() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: foo:a@b.example.de,c@d.example.de,e@f.example.de; Reply-To: ??? Date: Thu, 7 Nov 2024 15:54:04 +0100"#; - let m = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!( - m.to_addresses, - vec![ - ("", "a@b.example.de").into(), - ("", "c@d.example.de").into(), - ("", "e@f.example.de").into() - ] - ); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!( + m.to_addresses, + vec![ + ("", "a@b.example.de").into(), + ("", "c@d.example.de").into(), + ("", "e@f.example.de").into() + ] + ); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn undisclosed_recipients() { - let msg = r#"To: undisclosed-recipients:;"#; - let m = parse_mail(msg); + let msg = r#"To: undisclosed-recipients:;"#; + let m = parse_mail(msg); - assert_eq!(0, m.to_addresses.len()); + assert_eq!(0, m.to_addresses.len()); } #[test] fn long_content_type() { - let msg = r#"From: A + let msg = r#"From: A Content-type: multipart/mixed; boundary=frontier --frontier -Content-Type: text/plain; charset=us-ascii; name=withoutContentType.pdf +Content-Type: text/plain; charset=us-ascii; name=discardThisName.pdf Content-Disposition: attachment; filename=withoutContentType.pdf; Message --frontier-- "#; - let m = parse_mail(msg); - - let attachment = m.attachments.first().unwrap(); - assert_eq!("withoutContentType.pdf", attachment.filename); - assert_eq!("text/plain", attachment.content_type); - todo!() - // assertEquals("us-ascii", m.getAttachedFiles().get(0).getCharset()); + let m = parse_mail(msg); + + let attachment = &m.attachments[0]; + assert_eq!("withoutContentType.pdf", attachment.filename); + assert_eq!( + "text/plain;charset=\"us-ascii\";name=\"discardThisName.pdf\"", + attachment.content_type + ); } #[test] +#[ignore] fn normalize_header_value() { - todo!() - // // trim and remove LF and CR - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" ").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" ").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" \r \n ").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" \n \r ").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds("\n\r \r\n").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" | \n\r | ").toArray(new String[0])); - // assertArrayEquals(new String[0], MimeStringToSmtpMessageConverter.stripMessageIds(" <>").toArray(new String[0])); - // - // // remove comments - // assertArrayEquals(new String[0], MimeStringToSmtpMessageConverter.stripMessageIds(" (abc)").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" (abc) ").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" (abc) (def) ").toArray(new String[0])); - // - // // illegal comments - // assertArrayEquals(new String[0], MimeStringToSmtpMessageConverter.stripMessageIds(" (abc").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" abc) ").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" abc (abc").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" )abc def( ").toArray(new String[0])); - // - // // ids in comments are currently recognized - // assertArrayEquals(new String[]{"a@b", "g@h", "i@d"}, - // MimeStringToSmtpMessageConverter.stripMessageIds(" (abc) (def) ()").toArray(new String[0])); - // assertArrayEquals(new String[]{"a@b"}, MimeStringToSmtpMessageConverter.stripMessageIds(" (abc def) ").toArray(new String[0])); + // already done by mail_parser } #[test] fn get_spf_result() { - // net yet used on rust + // net yet used on rust } #[test] fn mail_from_with_delemiter() { - let msg = r#"Message-ID: 123456 + let msg = r#"Message-ID: 123456 Subject: Hello From: A,B To: B @@ -1116,48 +1003,50 @@ Content-Type: multipart/mixed; boundary=frontier --frontier "#; - let m = parse_mail(msg); + let m = parse_mail(msg); - todo!() - // assertEquals("A, B ", m.getSender().getMailAddress()); - // assertEquals("", m.getSender().getName()); - // assertFalse(m.getSender().isValid()); + assert_eq!( + m.from_addresses, + vec![("", "A").into(), ("B", "a@external.de").into()] + ); } #[test] fn incomplete_text_content_type() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text any body text"#; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!("any body text", m.html_body_text); + assert_eq!("any body text", m.html_body_text); } #[test] fn calendar_content_type() { - let msg = r#"Message-ID: 123456 + let msg = r#"Message-ID: 123456 Subject: Hello From: A To: B References: <1234564@web.de> Date: Thu, 7 Nov 2024 15:54:04 +0100 -Content-Type: text/calendar; charset=\"UTF-8\"; method=REQUEST +Content-Type: text/calendar; charset="UTF-8"; method=REQUEST "#; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!("text/calendar", m.attachments.first().unwrap().content_type); - // assertEquals("REQUEST", m.getAttachedFiles().get(0).getCalendarMethod()); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!( + "text/calendar;charset=\"UTF-8\";method=\"REQUEST\"", + &m.attachments[0].content_type + ); } #[test] fn calendar_content_type_method() { - let msg = r#"Message-ID: 123456 + let msg = r#"Message-ID: 123456 Subject: Hello From: A To: B @@ -1165,31 +1054,29 @@ References: <1234564@web.de> Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-Type: text/calendar; charset="UTF-8"; method=request; "#; - let m: ImportableMail = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - let attachment = m.attachments.first().unwrap(); - assert_eq!("text/calendar", attachment.content_type); - // todo! assert_eq!("REQUEST", calendar_method) + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!( + "text/calendar;charset=\"UTF-8\";method=\"request\"", + &m.attachments[0].content_type + ); } #[test] -fn invalid_content_types_default_to_text_plain() { - let invalid_content_types = vec![ - "Content-Type:", - "Content-Type: _", - "Content-Type: text", - "Content-Type; text/html", - "Content-Type; invalid/type", - "Content-Type: application/pdf; no_parameter_name.pdf", - ]; - for invalid_content_type in invalid_content_types { - let parsed = MessageParser::default() - .parse(invalid_content_type) - .unwrap(); - assert_eq!( - "text/plain", - parsed.content_type().unwrap().c_type.to_string() - ); - } +fn invalid_content_types_default_to_None() { + let invalid_content_types = vec![ + "Content-Type:", + "Content-Type: _", + "Content-Type: text", + "Content-Type; text/html", + "Content-Type; invalid/type", + "Content-Type: application/pdf; no_parameter_name.pdf", + ]; + for invalid_content_type in invalid_content_types { + let parsed = MessageParser::default() + .parse(invalid_content_type) + .unwrap(); + assert_eq!(None, parsed.content_type()); + } } From d80538215bcea97054d29723c7b78d645305974c Mon Sep 17 00:00:00 2001 From: map Date: Wed, 13 Nov 2024 18:14:35 +0100 Subject: [PATCH 20/32] improved mime handling part 3 --- .../src/importer/importable_mail.rs | 22 +- .../mime_string_to_importable_mail_test.rs | 900 +++++++++--------- .../msg_file_compatibility_test.rs | 62 +- 3 files changed, 485 insertions(+), 499 deletions(-) diff --git a/packages/node-mimimi/src/importer/importable_mail.rs b/packages/node-mimimi/src/importer/importable_mail.rs index 292f01cd8a6..34ad965bdbc 100644 --- a/packages/node-mimimi/src/importer/importable_mail.rs +++ b/packages/node-mimimi/src/importer/importable_mail.rs @@ -185,7 +185,6 @@ impl ImportableMail { if multipart_ignored_alternative.contains(&part_id) { continue; } - match &part.body { PartType::Binary(binary_content) => { Self::handle_binary(part, &mut attachments, binary_content.to_vec(), false); @@ -195,10 +194,9 @@ impl ImportableMail { Self::handle_binary(part, &mut attachments, binary_content.to_vec(), true); }, - // todo: of it is PartType::Text & PartType::Html, check for ConentDisposition Header - // and if it is attachment, treat it as attachment PartType::Text(text) => { - if !Self::is_attachment(part) && Self::is_plain_text(part) { + if !Self::is_attachment(&email_body_as_html, part) && Self::is_plain_text(part) + { Self::handle_plain_text(&mut email_body_as_html, text.as_ref()); } else { Self::handle_binary( @@ -211,7 +209,7 @@ impl ImportableMail { }, PartType::Html(html_text) => { - if !Self::is_attachment(part) { + if !Self::is_attachment(&email_body_as_html, part) { Self::handle_html_text(&mut email_body_as_html, html_text.as_ref()) } else { Self::handle_binary( @@ -253,9 +251,9 @@ impl ImportableMail { // semantics are different. In particular, in a digest, the default // Content-Type value for a body part is changed from "text/plain" to "message/rfc822". let is_message_rfc822 = - content_type.c_type == "message" && subtype == Some("rfc833"); + content_type.c_type == "message" && subtype == Some("rfc822"); - is_text_plain || is_message_rfc822 + is_text_plain || (is_message_rfc822) }) .unwrap_or({ // what should we treat text that is not content-Type: text? @@ -264,10 +262,11 @@ impl ImportableMail { }) } - fn is_attachment(part: &MessagePart) -> bool { + fn is_attachment(email_body_as_html: &String, part: &MessagePart) -> bool { part.content_disposition() .map(|content_disposition| content_disposition.c_type == "attachment") .unwrap_or_default() + || (!email_body_as_html.is_empty() && part.content_id().is_some()) } fn get_filename(part: &MessagePart, fallback_name: &str) -> String { @@ -326,11 +325,7 @@ impl ImportableMail { let is_multipart_alternative = part .content_type() .map(|content_type| { - assert_eq!( - "multipart", content_type.c_type, - "Multipart is not multipart?" - ); - content_type.subtype() == Some("alternative") + content_type.c_type == "multipart" && content_type.subtype() == Some("alternative") }) .unwrap_or_default(); @@ -446,6 +441,7 @@ impl ImportableMail { Self::get_filename(&message.parts[0], &message.subject().unwrap_or("unknown")); let content_type = message .content_type() + .ok_or_else(|| Self::default_content_type()) .map(MakeString::make_string) .unwrap_or_default() .to_string(); diff --git a/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs b/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs index 71b90ab4d8d..6edc38f611f 100644 --- a/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs +++ b/packages/node-mimimi/src/importer/importable_mail/mime_string_to_importable_mail_test.rs @@ -6,28 +6,28 @@ use mail_parser::{MessageParser, MimeHeaders}; use tutasdk::date::DateTime; fn parse_mail(msg: &str) -> ImportableMail { - (&MessageParser::default().parse(msg).unwrap()) - .try_into() - .unwrap() + (&MessageParser::default().parse(msg).unwrap()) + .try_into() + .unwrap() } // to be able to convert any (str/string, str/string).into() => MailContact impl From<(N, A)> for MailContact where - N: ToString, - A: ToString, + N: ToString, + A: ToString, { - fn from((name, address): (N, A)) -> Self { - Self { - mail_address: address.to_string(), - name: name.to_string(), - } - } + fn from((name, address): (N, A)) -> Self { + Self { + mail_address: address.to_string(), + name: name.to_string(), + } + } } #[test] fn headers() { - let msg = r#"Message-ID: 123456 + let msg = r#"Message-ID: 123456 Subject: Hello From: A To: B @@ -37,90 +37,90 @@ In-Reply-To: <1234564@web.de> Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-Type: multipart/mixed; boundary=frontier "#; - println!("{}", msg); - let m = parse_mail(msg); - assert_eq!("123456", m.message_id.unwrap()); - assert_eq!( + println!("{}", msg); + let m = parse_mail(msg); + assert_eq!("123456", m.message_id.unwrap()); + assert_eq!( m.reply_to_addresses, vec![ ("Reply", "reply@tutanota.de").into(), ("Reply2", "reply2@tutanota.de").into(), ], ); - assert_eq!( + assert_eq!( m.references, vec!["sadf@tutanota.de".to_string(), "1234564@web.de".to_string()], ); - assert_eq!(Some("1234564@web.de".to_string()), m.in_reply_to); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(msg, m.headers_string); + assert_eq!(Some("1234564@web.de".to_string()), m.in_reply_to); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(msg, m.headers_string); } #[test] fn bad_frontier() { - let msg = "Content-Type: multipart/mixed; boundary=komma;ist;nicht;erlaubt\n"; - let parsed_message = MessageParser::default().parse(msg).unwrap(); - let attributes = parsed_message - .content_type() - .unwrap() - .attributes - .as_ref() - .unwrap(); - assert_eq!(attributes.as_slice(), [("boundary".into(), "komma".into())]); + let msg = "Content-Type: multipart/mixed; boundary=komma;ist;nicht;erlaubt\n"; + let parsed_message = MessageParser::default().parse(msg).unwrap(); + let attributes = parsed_message + .content_type() + .unwrap() + .attributes + .as_ref() + .unwrap(); + assert_eq!(attributes.as_slice(), [("boundary".into(), "komma".into())]); } #[test] fn empty_references() { - let msg = "Subject: Hello"; - let m = parse_mail(msg); - assert!(m.references.is_empty()); + let msg = "Subject: Hello"; + let m = parse_mail(msg); + assert!(m.references.is_empty()); } #[test] fn empty_in_reply_to() { - let msg = "Subject: Hello"; - let m = parse_mail(msg); - assert_eq!(None, m.in_reply_to); + let msg = "Subject: Hello"; + let m = parse_mail(msg); + assert_eq!(None, m.in_reply_to); } #[test] fn text_plain_us_ascii_7bit() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 US-ASCII: !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~"##; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(m.subject, "Hello",); - assert_eq!("US-ASCII: !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", m.html_body_text); - assert_eq!(m.date, Some(DateTime::from_millis(1730991244000))); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(m.subject, "Hello",); + assert_eq!("US-ASCII: !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", m.html_body_text); + assert_eq!(m.date, Some(DateTime::from_millis(1730991244000))); } #[test] fn text_plain_utf8bit() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/plain; charset=UTF-8 Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\{Β³|@"##; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!("Hello", m.subject); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Hello", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_utf_explicit_8bit() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -129,18 +129,18 @@ Content-Transfer-Encoding: 8bit Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\{Β³|@"##; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!("Hello", m.subject); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Hello", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_utf_quoted_printable() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -148,18 +148,18 @@ Content-type: text/plain; charset=UTF-8 Content-Transfer-Encoding: quoted-printable Tutanota: =C3=A4=C3=BC=C3=B6=C3=9F=E2=82=AC*#\{=C2=B3|@"##; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); - assert_eq!("Hello", m.subject); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!("Hello", m.subject); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_utf_base64() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -167,18 +167,18 @@ Content-type: text/plain; charset=UTF-8 Content-Transfer-Encoding: base64 VHV0YW5vdGE6IMOkw7zDtsOf4oKsKiNce8KzfEA="##; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!("Hello", m.subject); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Hello", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_utf_invalid_base64() { - let msg = r##"Subject: Hello + let msg = r##"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -186,187 +186,187 @@ Content-type: text/plain; charset=UTF-8 Content-Transfer-Encoding: base64 VHV0YW5vdGE6IMOkw7zDtsOf4oKsKiNce8KzfEA"##; // skip the padding "=" to force an exception - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!("Hello", m.subject); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!("Hello", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn text_plain_format_flowed() { - // mime parser does not yet support rfc3676 - let msg = "Subject: Hello + // mime parser does not yet support rfc3676 + let msg = "Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/plain; charset=UTF-8; format=flowed Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es \r\neinen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!"; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!("Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es
einen soft-break!!!!!
Vor dieser Zeile gibt es keinen soft break, sondern einen richtigen!", m.html_body_text); + assert_eq!("Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es
einen soft-break!!!!!
Vor dieser Zeile gibt es keinen soft break, sondern einen richtigen!", m.html_body_text); } #[test] fn text_plain_format_flowed_del_sp() { - // mime parser does not yet support rfc3676 - let msg = "From: A + // mime parser does not yet support rfc3676 + let msg = "From: A To: B Subject: Hello Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/plain; charset=UTF-8; format=flowed; DelSp=yes Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es \r\neinen soft-break!!!!!\r\nVor dieser Zeile gibt es keinen soft break, sondern einen richtigen!"; - let m = parse_mail(msg); - assert_eq!( + let m = parse_mail(msg); + assert_eq!( "Tutanota: ist so toll und diese Zeile wird lΓ€nger, deshalb gibt es
einen soft-break!!!!!
Vor dieser Zeile gibt es keinen soft break, sondern einen richtigen!", m.html_body_text); } #[test] fn text_plain_subject_encoded_word_qencoding() { - // mime-parser always adds a space after q-encoded block if followed by another string - // so, following two lines are identical: - // =?UTF-8?Q?=E4?=abc <- no space before abc - // =?UTF-8?Q?=E4?= abc <- space before abc - let msg = r#"Subject: Hello =?ISO-8859-1?Q?=E4=F6=FC=DF?=abc + // mime-parser always adds a space after q-encoded block if followed by another string + // so, following two lines are identical: + // =?UTF-8?Q?=E4?=abc <- no space before abc + // =?UTF-8?Q?=E4?= abc <- space before abc + let msg = r#"Subject: Hello =?ISO-8859-1?Q?=E4=F6=FC=DF?=abc From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 "#; - let m = parse_mail(msg); - assert_eq!("Hello Àâüß abc", m.subject); + let m = parse_mail(msg); + assert_eq!("Hello Àâüß abc", m.subject); } #[test] fn text_plain_subject_encoded_word_qencoding_turkish() { - let msg = r#"Subject: =?iso-8859-9?Q?Paracard Hesap =D6zeti?= + let msg = r#"Subject: =?iso-8859-9?Q?Paracard Hesap =D6zeti?= From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 "#; - let m = parse_mail(msg); - assert_eq!("Paracard Hesap Γ–zeti", m.subject); + let m = parse_mail(msg); + assert_eq!("Paracard Hesap Γ–zeti", m.subject); } #[test] fn from_encoded_word_qencoding() { - // the rust mime parser is not able to handle chars splitted in two q-encoding blocks - // while our server side parser handles those cases - let msg = r#"Subject: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= + // the rust mime parser is not able to handle chars splitted in two q-encoding blocks + // while our server side parser handles those cases + let msg = r#"Subject: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= From: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= To: =?utf-8?Q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5?==?utf-8?Q?_=D0=B4=D0=B5=D0=BC=D0=BE=D0=BA=D1=80=D0=B0=D1=82=D1=8B?= Date: Thu, 7 Nov 2024 15:54:04 +0100 "#; - let m = parse_mail(msg); - assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.subject); - assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.from_addresses[0].name); - assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.to_addresses[0].name); + let m = parse_mail(msg); + assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.subject); + assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.from_addresses[0].name); + assert_eq!("ΠžΠ±ΡŠΠ΅Π΄ΠΈΠ½Π΅Π½Π½Ρ‹Π΅ Π΄Π΅ΠΌΠΎΠΊΡ€Π°Ρ‚Ρ‹", m.to_addresses[0].name); } #[test] fn from_encoded_word_qencoding_colon() { - let msg = r#"Subject: Hi + let msg = r#"Subject: Hi From: =?utf-8?Q?=D0=9B=D0=B8=D1=82=D1=80=D0=B5=D1=81=3A=20=D0=A1=D0=B0=D0=BC=D0=B8=D0=B7=D0=B4=D0=B0=D1=82?= "#; - let m = parse_mail(msg); - assert_eq!( - m.from_addresses[0], - ("ЛитрСс: Π‘Π°ΠΌΠΈΠ·Π΄Π°Ρ‚", "mail@selfpub.ru").into() - ); + let m = parse_mail(msg); + assert_eq!( + m.from_addresses[0], + ("ЛитрСс: Π‘Π°ΠΌΠΈΠ·Π΄Π°Ρ‚", "mail@selfpub.ru").into() + ); } #[test] fn recipients_encoded_word_qencoding_colon() { - let msg = "To: =?utf-8?Q?=D0=9B=D0=B8=D1=82=D1=80=D0=B5=D1=81=3A=20=D0=A1=D0=B0=D0=BC=D0=B8=D0=B7=D0=B4=D0=B0=D1=82?= \n"; - let m = parse_mail(msg); - assert_eq!( - m.to_addresses[0], - ("ЛитрСс: Π‘Π°ΠΌΠΈΠ·Π΄Π°Ρ‚", "mail@selfpub.ru").into() - ); + let msg = "To: =?utf-8?Q?=D0=9B=D0=B8=D1=82=D1=80=D0=B5=D1=81=3A=20=D0=A1=D0=B0=D0=BC=D0=B8=D0=B7=D0=B4=D0=B0=D1=82?= \n"; + let m = parse_mail(msg); + assert_eq!( + m.to_addresses[0], + ("ЛитрСс: Π‘Π°ΠΌΠΈΠ·Π΄Π°Ρ‚", "mail@selfpub.ru").into() + ); } #[test] fn recipients_encoded_word_qencoding_partly() { - let msg = "To: =?ISO-8859-1?Q?Andr=E9?= Pirard \n"; - let m = parse_mail(msg); - assert_eq!( - m.to_addresses[0], - ("AndrΓ© Pirard", "PIRARD@vm1.ulg.ac.be").into() - ); + let msg = "To: =?ISO-8859-1?Q?Andr=E9?= Pirard \n"; + let m = parse_mail(msg); + assert_eq!( + m.to_addresses[0], + ("AndrΓ© Pirard", "PIRARD@vm1.ulg.ac.be").into() + ); } #[test] fn text_plain_subject_encoded_word_base64() { - let msg = r#"Subject: =?utf-8?B?SGVsbG8gw6TDtsO8w58=?= + let msg = r#"Subject: =?utf-8?B?SGVsbG8gw6TDtsO8w58=?= From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 "#; - let m = parse_mail(msg); - assert_eq!("Hello Àâüß", m.subject); + let m = parse_mail(msg); + assert_eq!("Hello Àâüß", m.subject); } #[test] fn text_html_only() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text/html; charset=UTF-8 Hello Àâüß
"#; - let m = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!( - "Hello Àâüß
", - m.html_body_text - ); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!( + "Hello Àâüß
", + m.html_body_text + ); } #[test] fn charset() { - // todo!() + // todo!() } #[test] fn text_html_inline_charset_definition_utf8() { - let msg = r#"Content-type: text/html + let msg = r#"Content-type: text/html Content-Transfer-Encoding: 8bit

Π‘Π»Π°Π³ΠΎΠ΄Π°Ρ€ΠΈΠΌ Π’ΠΈ

"#; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!( - "

Π‘Π»Π°Π³ΠΎΠ΄Π°Ρ€ΠΈΠΌ Π’ΠΈ

", - m.html_body_text - ); + assert_eq!( + "

Π‘Π»Π°Π³ΠΎΠ΄Π°Ρ€ΠΈΠΌ Π’ΠΈ

", + m.html_body_text + ); } #[test] #[ignore] fn text_html_inline_charset_definition_western() { - // there is currently no way to port server side code to support this as regex does not support look ahead - // we don't want to write our own parser for now - let msg = r#"Content-type: text/html + // there is currently no way to port server side code to support this as regex does not support look ahead + // we don't want to write our own parser for now + let msg = r#"Content-type: text/html Content-Transfer-Encoding: base64 PGh0bWw+PGhlYWQ+PG1ldGEgY2hhcnNldD0iSVNPLTg4NTktMTUiPjwvaGVhZD48Ym9keT48cD6kIPbkPC9wPjwvYm9keT48L2h0bWw+"#; - let m = parse_mail(msg); - assert_eq!( - "

€ ΓΆΓ€

", - m.html_body_text - ); + let m = parse_mail(msg); + assert_eq!( + "

€ ΓΆΓ€

", + m.html_body_text + ); } #[test] fn text_alternative() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 (CET) @@ -382,54 +382,54 @@ Content-type: text/html; charset=UTF-8; Hello Àâüß
--frontier-- "#; - let m = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!("Hello", m.subject); - assert_eq!( - "Hello Àâüß
", - m.html_body_text - ); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!("Hello", m.subject); + assert_eq!( + "Hello Àâüß
", + m.html_body_text + ); } #[test] fn invalid_domains_in_mail_addresses() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B , C , D "#; - let m = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!( - m.to_addresses, - vec![ - ("B", "b@a.example").into(), - ("C", "c@c.com").into(), - ("D", "d@d.invalid").into() - ] - ); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!( + m.to_addresses, + vec![ + ("B", "b@a.example").into(), + ("C", "c@c.com").into(), + ("D", "d@d.invalid").into() + ] + ); } #[test] fn multiple_to_headers() { - // mime_parser discards duplicated headers besides the last one - let msg = r#"Subject: Hello + // mime_parser discards duplicated headers besides the last one + let msg = r#"Subject: Hello From: A To: B , C To: D "#; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("D", "d@d.net").into()]); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("D", "d@d.net").into()]); } #[test] fn attached_message() { - let msg = r#"Subject: parent message + let msg = r#"Subject: parent message From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -452,30 +452,30 @@ Content-type: text/plain; charset=UTF-8; Hello Àâüß "#; - let m = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(m.subject, "parent message"); - assert_eq!(m.html_body_text, "normal message"); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - - assert_eq!(m.attachments.len(), 1); - let attachment = &m.attachments[0]; - let attached = parse_mail( - String::from_utf8(attachment.content.to_vec()) - .unwrap() - .as_str(), - ); - assert_eq!(attached.from_addresses, vec![("D", "d@tutanota.de").into()]); - assert_eq!(attached.to_addresses, vec![("E", "e@tutanota.de").into()]); - assert_eq!(attached.subject, "attached message"); - assert_eq!(attached.html_body_text, "
Hello Àâüß
"); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(m.subject, "parent message"); + assert_eq!(m.html_body_text, "normal message"); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + + assert_eq!(m.attachments.len(), 1); + let attachment = &m.attachments[0]; + let attached = parse_mail( + String::from_utf8(attachment.content.to_vec()) + .unwrap() + .as_str(), + ); + assert_eq!(attached.from_addresses, vec![("D", "d@tutanota.de").into()]); + assert_eq!(attached.to_addresses, vec![("E", "e@tutanota.de").into()]); + assert_eq!(attached.subject, "attached message"); + assert_eq!(attached.html_body_text, "
Hello Àâüß
"); } #[test] fn multiple_attachments() { - let msg = r#"Subject: multiple attachments + let msg = r#"Subject: multiple attachments From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -504,42 +504,42 @@ Content-Disposition: attachment; filename=withoutContentType.pdf; c2Vjb25kIGF0dGFjaG1lbnQ= --frontier-- "#; - let m = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(m.subject, "multiple attachments"); - - assert_eq!("Hello Àâüß", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(m.attachments.len(), 3); - let [a1, a2, a3] = m.attachments.try_into().unwrap(); - - assert_eq!("a1.txt", a1.filename); - assert_eq!( - String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), - String::from_utf8(a1.content.to_vec()).unwrap() - ); - assert_eq!("application/octet-stream", a1.content_type); - - assert_eq!("a2.pdf", a2.filename); - assert_eq!( - String::from_utf8(base64_decode(b"c2Vjb25kIGF0dGFjaG1lbnQ=").unwrap()).unwrap(), - String::from_utf8(a2.content.to_vec()).unwrap() - ); - assert_eq!("application/pdf", a2.content_type); - - assert_eq!("withoutContentType.pdf", a3.filename); - assert_eq!( - String::from_utf8(base64_decode(b"c2Vjb25kIGF0dGFjaG1lbnQ=").unwrap()).unwrap(), - String::from_utf8(a3.content.to_vec()).unwrap() - ); - assert_eq!(r#"text/plain;charset="us-ascii""#, a3.content_type); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(m.subject, "multiple attachments"); + + assert_eq!("Hello Àâüß", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(m.attachments.len(), 3); + let [a1, a2, a3] = m.attachments.try_into().unwrap(); + + assert_eq!("a1.txt", a1.filename); + assert_eq!( + String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), + String::from_utf8(a1.content.to_vec()).unwrap() + ); + assert_eq!("application/octet-stream", a1.content_type); + + assert_eq!("a2.pdf", a2.filename); + assert_eq!( + String::from_utf8(base64_decode(b"c2Vjb25kIGF0dGFjaG1lbnQ=").unwrap()).unwrap(), + String::from_utf8(a2.content.to_vec()).unwrap() + ); + assert_eq!("application/pdf", a2.content_type); + + assert_eq!("withoutContentType.pdf", a3.filename); + assert_eq!( + String::from_utf8(base64_decode(b"c2Vjb25kIGF0dGFjaG1lbnQ=").unwrap()).unwrap(), + String::from_utf8(a3.content.to_vec()).unwrap() + ); + assert_eq!(r#"text/plain;charset="us-ascii""#, a3.content_type); } #[test] fn inline_attachment() { - let msg = r#"Subject: inline attachment + let msg = r#"Subject: inline attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -558,23 +558,23 @@ Content-ID: <123@tutanota.de>; Zmlyc3QgYXR0YWNobWVudA== --frontier-- "#; - let m = parse_mail(msg); - - assert_eq!(1, m.attachments.len()); - let a1 = &m.attachments[0]; - - assert_eq!("a1.png", a1.filename); - assert_eq!( - String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), - String::from_utf8(a1.content.to_vec()).unwrap() - ); - assert_eq!("application/octet-stream", a1.content_type); - assert_eq!(Some("123@tutanota.de".to_string()), a1.content_id); + let m = parse_mail(msg); + + assert_eq!(1, m.attachments.len()); + let a1 = &m.attachments[0]; + + assert_eq!("a1.png", a1.filename); + assert_eq!( + String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), + String::from_utf8(a1.content.to_vec()).unwrap() + ); + assert_eq!("application/octet-stream", a1.content_type); + assert_eq!(Some("123@tutanota.de".to_string()), a1.content_id); } #[test] fn attachment_to_attached_message() { - let msg = r#"Subject: parent message + let msg = r#"Subject: parent message From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -590,36 +590,36 @@ Content-Disposition: attachment; filename=indirectly_attached.txt; Zmlyc3QgYXR0YWNobWVudA== "#; - let m = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(1, m.attachments.len()); - - let attached = parse_mail( - String::from_utf8(m.attachments[0].content.clone()) - .unwrap() - .as_str(), - ); - - assert_eq!(attached.subject, "attached message"); - - assert_eq!("", attached.html_body_text); - assert_eq!(1, attached.attachments.len()); - - assert_eq!(1, attached.attachments.len()); - let indirect_attachment = attached.attachments.first().unwrap(); - assert_eq!("indirectly_attached.txt", indirect_attachment.filename); - assert_eq!( - String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), - String::from_utf8(indirect_attachment.content.to_vec()).unwrap() - ); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(1, m.attachments.len()); + + let attached = parse_mail( + String::from_utf8(m.attachments[0].content.clone()) + .unwrap() + .as_str(), + ); + + assert_eq!(attached.subject, "attached message"); + + assert_eq!("", attached.html_body_text); + assert_eq!(1, attached.attachments.len()); + + assert_eq!(1, attached.attachments.len()); + let indirect_attachment = attached.attachments.first().unwrap(); + assert_eq!("indirectly_attached.txt", indirect_attachment.filename); + assert_eq!( + String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), + String::from_utf8(indirect_attachment.content.to_vec()).unwrap() + ); } #[test] fn text_attachment() { - let msg = r#"Subject: text attachment + let msg = r#"Subject: text attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -634,26 +634,27 @@ Content-type: text/plain; charset=UTF-8 Content-Disposition: attachment; filename=a1.txt; Abc, die Katze lief im Schnee ! Àâü?ß ! + --frontier-- "#; - let m = parse_mail(msg); - - assert_eq!("text attachment", m.subject); - assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - - assert_eq!(1, m.attachments.len()); - let a1 = &m.attachments[0]; - assert_eq!("a1.txt", a1.filename); - assert_eq!( - "Abc, die Katze lief im Schnee ! Àâü?ß ! ", - String::from_utf8(a1.content.clone()).unwrap() - ); + let m = parse_mail(msg); + + assert_eq!("text attachment", m.subject); + assert_eq!("Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + + assert_eq!(1, m.attachments.len()); + let a1 = &m.attachments[0]; + assert_eq!("a1.txt", a1.filename); + assert_eq!( + "Abc, die Katze lief im Schnee ! Àâü?ß !\n", + String::from_utf8(a1.content.clone()).unwrap() + ); } #[test] fn html_attachment() { - let msg = r#"Subject: html attachment + let msg = r#"Subject: html attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -670,24 +671,24 @@ Content-Disposition: attachment; filename=a1.html; Hello Àâüß
--frontier-- "#; - let m = parse_mail(msg); - - assert_eq!(m.subject, "html attachment"); - assert_eq!(m.html_body_text, "Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@"); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - - assert_eq!(m.attachments.len(), 1); - let a1 = &m.attachments[0]; - assert_eq!(a1.filename, "a1.html"); - assert_eq!( - String::from_utf8(a1.content.to_vec()).unwrap(), - "Hello Àâüß
" - ); + let m = parse_mail(msg); + + assert_eq!(m.subject, "html attachment"); + assert_eq!(m.html_body_text, "Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@"); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + + assert_eq!(m.attachments.len(), 1); + let a1 = &m.attachments[0]; + assert_eq!(a1.filename, "a1.html"); + assert_eq!( + String::from_utf8(a1.content.to_vec()).unwrap(), + "Hello Àâüß
" + ); } #[test] fn multiple_plain_body_text_parts_are_concatenated() { - let eml_contents = r#"Subject: multiple text/plain parts concatenated + let eml_contents = r#"Subject: multiple text/plain parts concatenated From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -702,23 +703,24 @@ Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\{Β³|@ Content-type: text/plain; charset=UTF-8 Abc, die Katze liegt im Schnee ! Àâü?ß ! + --frontier-- "#; - let m = parse_mail(eml_contents); + let m = parse_mail(eml_contents); - assert_eq!("multiple text/plain parts concatenated", m.subject); - assert_eq!( - "Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@
Abc, die Katze liegt im Schnee ! Àâü?ß ! ", - m.html_body_text - ); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(0, m.attachments.len()); + assert_eq!("multiple text/plain parts concatenated", m.subject); + assert_eq!( + "Tutanota: Γ€ΓΌΓΆΓŸβ‚¬*#\\{Β³|@
Abc, die Katze liegt im Schnee ! Àâü?ß !
", + m.html_body_text + ); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(0, m.attachments.len()); } #[test] fn multiple_html_body_text_parts_are_concatenated() { - let msg = r#"Message-Id: some-id + let msg = r#"Message-Id: some-id Subject: multiple text/html parts concatenated From: A To: B @@ -736,18 +738,18 @@ Content-type: text/html; charset=UTF-8 --frontier-- "#; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!("multiple text/html parts concatenated", m.subject); - assert_eq!("Hello Àâüß
Test Test
", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(0, m.attachments.len()); + assert_eq!("multiple text/html parts concatenated", m.subject); + assert_eq!("Hello Àâüß
Test Test
", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(0, m.attachments.len()); } #[test] fn plain_body_text_parts_are_concatenated_with_html_body_parts_if_html_body_parts_already_existing() { - let msg = r#"Subject: multiple text/html and text/plain parts concatenated to single text/html + let msg = r#"Subject: multiple text/html and text/plain parts concatenated to single text/html From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -768,20 +770,20 @@ Abc, die Katze lief im Schnee ! Àâü?ß ! --frontier- "#; - let m = parse_mail(msg); - - assert_eq!( - "multiple text/html and text/plain parts concatenated to single text/html", - m.subject - ); - assert_eq!("Hello Àâüß
Test Test
Abc, die Katze lief im Schnee ! Àâü?ß !
", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(0, m.attachments.len()); + let m = parse_mail(msg); + + assert_eq!( + "multiple text/html and text/plain parts concatenated to single text/html", + m.subject + ); + assert_eq!("Hello Àâüß
Test Test
Abc, die Katze lief im Schnee ! Àâü?ß !
", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(0, m.attachments.len()); } #[test] fn plain_body_text_parts_are_converted_to_html_body_parts_if_html_body_parts_follow_afterwards() { - let msg = r#"Subject: multiple plain/text and text/html parts concatenated to single text/html + let msg = r#"Subject: multiple plain/text and text/html parts concatenated to single text/html From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -802,20 +804,20 @@ Content-type: text/html; charset=UTF-8 Test Test
--frontier- "#; - let m = parse_mail(msg); - - assert_eq!( - "multiple plain/text and text/html parts concatenated to single text/html", - m.subject - ); - assert_eq!("Abc, die Katze lief im Schnee ! Àâü?ß !
Hello Àâüß
Test Test
", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(0, m.attachments.len()); + let m = parse_mail(msg); + + assert_eq!( + "multiple plain/text and text/html parts concatenated to single text/html", + m.subject + ); + assert_eq!("Abc, die Katze lief im Schnee ! Àâü?ß !
Hello Àâüß
Test Test
", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(0, m.attachments.len()); } #[test] fn text_attachment_with_disposition() { - let msg = r#"Subject: text attachment + let msg = r#"Subject: text attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -823,24 +825,24 @@ Content-type: text/plain; charset=UTF-8 Content-Disposition: attachment; filename=a1.txt; Abc, die Katze lief im Schnee ! Àâü?ß ! "#; - let m = parse_mail(msg); - - assert_eq!("text attachment", m.subject); - assert_eq!("", m.html_body_text); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - - assert_eq!(m.attachments.len(), 1); - let a1 = &m.attachments[0]; - assert_eq!(a1.filename, "a1.txt"); - assert_eq!( - String::from_utf8(a1.content.to_vec()).unwrap(), - "Abc, die Katze lief im Schnee ! Àâü?ß ! " - ); + let m = parse_mail(msg); + + assert_eq!("text attachment", m.subject); + assert_eq!("", m.html_body_text); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + + assert_eq!(m.attachments.len(), 1); + let a1 = &m.attachments[0]; + assert_eq!(a1.filename, "a1.txt"); + assert_eq!( + String::from_utf8(a1.content.to_vec()).unwrap(), + "Abc, die Katze lief im Schnee ! Àâü?ß ! " + ); } #[test] fn attachment_with_non_ascii_name() { - let msg = r#"Subject: text attachment + let msg = r#"Subject: text attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -848,18 +850,18 @@ Content-type: text/plain; charset=UTF-8; name=\"=?ISO-8859-1?Q?a=F6i=2Epdf?=\" Content-Disposition: attachment; filename*=ISO-8859-1''%61%F6%69%2E%70%64%66 Abc, die Katze lief im Schnee ! Àâü?ß ! "#; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(1, m.attachments.len()); - assert_eq!("aΓΆi.pdf", &m.attachments[0].filename); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(1, m.attachments.len()); + assert_eq!("aΓΆi.pdf", &m.attachments[0].filename); } #[test] fn attachment_filename_in_content_type() { - let msg = r#"Subject: message with named file attachment + let msg = r#"Subject: message with named file attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -867,24 +869,24 @@ Content-type: application/octet-stream; name=indirectly_attached.txt; Content-Transfer-Encoding: base64 Zmlyc3QgYXR0YWNobWVudA=="#; - let m = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - - assert_eq!(1, m.attachments.len()); - let indirect_attachment = &m.attachments[0]; - assert_eq!("indirectly_attached.txt", indirect_attachment.filename); - assert_eq!( - String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), - String::from_utf8(indirect_attachment.content.to_vec()).unwrap() - ); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + + assert_eq!(1, m.attachments.len()); + let indirect_attachment = &m.attachments[0]; + assert_eq!("indirectly_attached.txt", indirect_attachment.filename); + assert_eq!( + String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), + String::from_utf8(indirect_attachment.content.to_vec()).unwrap() + ); } #[test] fn attachment_filename_qencoding() { - let msg = r#"Subject: message with named file attachment + let msg = r#"Subject: message with named file attachment From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -892,24 +894,24 @@ Content-type: application/octet-stream; name==?utf-8?Q?=C3=A4=C3=B6=C3=9F=E2=82= Content-Transfer-Encoding: base64 Zmlyc3QgYXR0YWNobWVudA=="#; - let m = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - - assert_eq!(1, m.attachments.len()); - let indirect_attachment = &m.attachments[0]; - assert_eq!("Γ€ΓΆΓŸβ‚¬.txt", indirect_attachment.filename); - assert_eq!( - String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), - String::from_utf8(indirect_attachment.content.to_vec()).unwrap() - ); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + + assert_eq!(1, m.attachments.len()); + let indirect_attachment = &m.attachments[0]; + assert_eq!("Γ€ΓΆΓŸβ‚¬.txt", indirect_attachment.filename); + assert_eq!( + String::from_utf8(base64_decode(b"Zmlyc3QgYXR0YWNobWVudA==").unwrap()).unwrap(), + String::from_utf8(indirect_attachment.content.to_vec()).unwrap() + ); } #[test] fn encrypted() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 @@ -921,46 +923,46 @@ Content-Transfer-Encoding: base64 SGFsbG8= --frontier--"#; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); - assert_eq!(1, m.attachments.len()); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!(m.to_addresses, vec![("B", "b@tutanota.de").into()]); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + assert_eq!(1, m.attachments.len()); } #[test] fn recipient_groups() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: foo:a@b.example.de,c@d.example.de,e@f.example.de; Reply-To: ??? Date: Thu, 7 Nov 2024 15:54:04 +0100"#; - let m = parse_mail(msg); - - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!( - m.to_addresses, - vec![ - ("", "a@b.example.de").into(), - ("", "c@d.example.de").into(), - ("", "e@f.example.de").into() - ] - ); - assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); + let m = parse_mail(msg); + + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!( + m.to_addresses, + vec![ + ("", "a@b.example.de").into(), + ("", "c@d.example.de").into(), + ("", "e@f.example.de").into() + ] + ); + assert_eq!(Some(DateTime::from_millis(1730991244000)), m.date); } #[test] fn undisclosed_recipients() { - let msg = r#"To: undisclosed-recipients:;"#; - let m = parse_mail(msg); + let msg = r#"To: undisclosed-recipients:;"#; + let m = parse_mail(msg); - assert_eq!(0, m.to_addresses.len()); + assert_eq!(0, m.to_addresses.len()); } #[test] fn long_content_type() { - let msg = r#"From: A + let msg = r#"From: A Content-type: multipart/mixed; boundary=frontier --frontier @@ -970,30 +972,30 @@ Content-Disposition: attachment; filename=withoutContentType.pdf; Message --frontier-- "#; - let m = parse_mail(msg); - - let attachment = &m.attachments[0]; - assert_eq!("withoutContentType.pdf", attachment.filename); - assert_eq!( - "text/plain;charset=\"us-ascii\";name=\"discardThisName.pdf\"", - attachment.content_type - ); + let m = parse_mail(msg); + + let attachment = &m.attachments[0]; + assert_eq!("withoutContentType.pdf", attachment.filename); + assert_eq!( + "text/plain;charset=\"us-ascii\";name=\"discardThisName.pdf\"", + attachment.content_type + ); } #[test] #[ignore] fn normalize_header_value() { - // already done by mail_parser + // already done by mail_parser } #[test] fn get_spf_result() { - // net yet used on rust + // net yet used on rust } #[test] fn mail_from_with_delemiter() { - let msg = r#"Message-ID: 123456 + let msg = r#"Message-ID: 123456 Subject: Hello From: A,B To: B @@ -1003,31 +1005,31 @@ Content-Type: multipart/mixed; boundary=frontier --frontier "#; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!( - m.from_addresses, - vec![("", "A").into(), ("B", "a@external.de").into()] - ); + assert_eq!( + m.from_addresses, + vec![("", "A").into(), ("B", "a@external.de").into()] + ); } #[test] fn incomplete_text_content_type() { - let msg = r#"Subject: Hello + let msg = r#"Subject: Hello From: A To: B Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-type: text any body text"#; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!("any body text", m.html_body_text); + assert_eq!("any body text", m.html_body_text); } #[test] fn calendar_content_type() { - let msg = r#"Message-ID: 123456 + let msg = r#"Message-ID: 123456 Subject: Hello From: A To: B @@ -1035,18 +1037,18 @@ References: <1234564@web.de> Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-Type: text/calendar; charset="UTF-8"; method=REQUEST "#; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!( - "text/calendar;charset=\"UTF-8\";method=\"REQUEST\"", - &m.attachments[0].content_type - ); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!( + "text/calendar;charset=\"UTF-8\";method=\"REQUEST\"", + &m.attachments[0].content_type + ); } #[test] fn calendar_content_type_method() { - let msg = r#"Message-ID: 123456 + let msg = r#"Message-ID: 123456 Subject: Hello From: A To: B @@ -1054,29 +1056,29 @@ References: <1234564@web.de> Date: Thu, 7 Nov 2024 15:54:04 +0100 Content-Type: text/calendar; charset="UTF-8"; method=request; "#; - let m = parse_mail(msg); + let m = parse_mail(msg); - assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); - assert_eq!( - "text/calendar;charset=\"UTF-8\";method=\"request\"", - &m.attachments[0].content_type - ); + assert_eq!(m.from_addresses, vec![("A", "a@tutanota.de").into()]); + assert_eq!( + "text/calendar;charset=\"UTF-8\";method=\"request\"", + &m.attachments[0].content_type + ); } #[test] fn invalid_content_types_default_to_None() { - let invalid_content_types = vec![ - "Content-Type:", - "Content-Type: _", - "Content-Type: text", - "Content-Type; text/html", - "Content-Type; invalid/type", - "Content-Type: application/pdf; no_parameter_name.pdf", - ]; - for invalid_content_type in invalid_content_types { - let parsed = MessageParser::default() - .parse(invalid_content_type) - .unwrap(); - assert_eq!(None, parsed.content_type()); - } + let invalid_content_types = vec![ + "Content-Type:", + "Content-Type: _", + "Content-Type: text", + "Content-Type; text/html", + "Content-Type; invalid/type", + "Content-Type: application/pdf; no_parameter_name.pdf", + ]; + for invalid_content_type in invalid_content_types { + let parsed = MessageParser::default() + .parse(invalid_content_type) + .unwrap(); + assert_eq!(None, parsed.content_type()); + } } diff --git a/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs b/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs index 8d0417e7863..1b810ccf8a3 100644 --- a/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs +++ b/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs @@ -1,8 +1,9 @@ //! keep in sync with MimeToolsTestMessages.java -use crate::importer::importable_mail::{ImportableMail, ImportableMailAttachment, MailContact}; +use crate::importer::importable_mail::{ImportableMail, MailContact}; use serde::Deserialize; use std::borrow::Cow; +use std::collections::HashSet; use std::io::Read; #[test] @@ -11,46 +12,33 @@ fn mime_tools_test_messages() { let source_message_paths = std::fs::read_dir(DATA_DIR) .unwrap() .map(Result::unwrap) - .map(|p| p.file_name().to_str().unwrap().to_string()) - .filter(|p| p.ends_with(".msg")); + .filter(|p| { + p.path() + .into_os_string() + .into_string() + .unwrap() + .ends_with(".msg") + }); - // everything else is related to multipart i suppose, - const IGNORED_FILES: &[&str] = &[ - "multi-igor.msg", - "multi-bad.msg", - "multi-digest.msg", - "2002_06_12_doublebound.msg", - "attachment-filename-encoding-Latin1.msg", - "attachment-filename-encoding-UTF8.msg", - "multi-digest.msg", - "multi-igor2.msg", - "multi-nested.msg", - "multi-nested3.msg", - "multi-nested2.msg", - "infinite.msg", // have encoding problem - ]; + let ignored_files = [ + "infinite.msg", // encoding not specified so we are falling back to us-ascii but message contains chars encoded in different charset + "multi-digest.msg", // body correctly interpreted as message/rfc822 (due to multipart/digest) whereas the server seems to default to plain/text even for multipart/digest + "multi-bad.msg", // first part is not ignored because of duplicate content-type header, java parser opts for first content-type whereas rust mime-parser uses second content-type header + ] + .into_iter() + .collect::>(); - for message_file_name in source_message_paths - .filter(|p| { - IGNORED_FILES - .iter() - .filter(|f| f.starts_with(p.as_str())) - .next() - .is_none() - }) - .chain(IGNORED_FILES.iter().map(|s| { - eprintln!("Ignored file: "); - s.to_string() - })) - .map(|a| { - eprintln!("{a} .....Testing"); - a - }) { - let message_path = format!("{DATA_DIR}/{message_file_name}"); + for message_file_path in source_message_paths { + eprintln!("File: {:?}", message_file_path); + let message_filename = message_file_path.file_name().into_string().unwrap(); + if ignored_files.contains(message_filename.as_str()) { + eprintln!("ignored.."); + continue; + } // let message_file_content = std::fs::r(&message_path.path()).unwrap() let mut message_file_content = vec![]; - std::fs::File::open(message_path.as_str()) + std::fs::File::open(message_file_path.path()) .unwrap() .read_to_end(&mut message_file_content) .unwrap(); @@ -60,7 +48,7 @@ fn mime_tools_test_messages() { let expected_json_file_name = format!( "{DATA_DIR}/{}", - message_file_name.replace(".msg", "-expected.json") + message_filename.replace(".msg", "-expected.json") ); let FileContent { result: expected_result, From 5186747e5872c5b0af4a8ae10af0e149fd1c696d Mon Sep 17 00:00:00 2001 From: jhm <17314077+jomapp@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:33:37 +0100 Subject: [PATCH 21/32] change TutanotaModelV77 (should be merged with other commit) --- schemas/tutanota.json | 10 + .../api/entities/tutanota/TypeModels.js | 131 ++-- src/common/api/entities/tutanota/TypeRefs.ts | 6 +- .../sdk/src/entities/generated/accounting.rs | 8 +- .../rust/sdk/src/entities/generated/base.rs | 5 +- .../sdk/src/entities/generated/monitor.rs | 26 +- .../sdk/src/entities/generated/storage.rs | 41 +- .../rust/sdk/src/entities/generated/sys.rs | 678 +++++++++++++++++- .../sdk/src/entities/generated/tutanota.rs | 377 +++++++++- .../rust/sdk/src/entities/generated/usage.rs | 26 +- .../sdk/src/services/generated/accounting.rs | 15 +- .../rust/sdk/src/services/generated/base.rs | 8 +- .../rust/sdk/src/services/generated/gossip.rs | 8 +- .../sdk/src/services/generated/monitor.rs | 22 +- .../sdk/src/services/generated/storage.rs | 41 +- .../rust/sdk/src/services/generated/sys.rs | 480 ++++--------- .../sdk/src/services/generated/tutanota.rs | 189 ++--- .../rust/sdk/src/services/generated/usage.rs | 48 +- .../rust/sdk/src/type_models/tutanota.json | 131 ++-- 19 files changed, 1539 insertions(+), 711 deletions(-) diff --git a/schemas/tutanota.json b/schemas/tutanota.json index 23be430f0fe..57b9a350fca 100644 --- a/schemas/tutanota.json +++ b/schemas/tutanota.json @@ -510,6 +510,16 @@ "info": "RemoveAssociation MailboxGroupRoot/whitelistRequests." } ] + }, + { + "version": 77, + "changes": [ + { + "name": "AddAssociation", + "sourceType": "MailBox", + "info": "AddAssociation MailBox/importedAttachments/LIST_ASSOCIATION/1479." + } + ] } ] } diff --git a/src/common/api/entities/tutanota/TypeModels.js b/src/common/api/entities/tutanota/TypeModels.js index 4094f4e1e4c..d982afdbc16 100644 --- a/src/common/api/entities/tutanota/TypeModels.js +++ b/src/common/api/entities/tutanota/TypeModels.js @@ -4072,15 +4072,15 @@ export const typeModels = { "name": "ImportAttachment", "since": 77, "type": "AGGREGATED_TYPE", - "id": 1490, - "rootId": "CHR1dGFub3RhAAXS", + "id": 1491, + "rootId": "CHR1dGFub3RhAAXT", "versioned": false, "encrypted": false, "values": { "_id": { "final": true, "name": "_id", - "id": 1491, + "id": 1492, "since": 77, "type": "CustomId", "cardinality": "One", @@ -4089,28 +4089,37 @@ export const typeModels = { "ownerEncFileSessionKey": { "final": true, "name": "ownerEncFileSessionKey", - "id": 1492, + "id": 1493, "since": 77, "type": "Bytes", "cardinality": "One", "encrypted": false + }, + "ownerFileKeyVersion": { + "final": false, + "name": "ownerFileKeyVersion", + "id": 1494, + "since": 77, + "type": "Number", + "cardinality": "One", + "encrypted": false } }, "associations": { - "existingFile": { + "existingAttachmentFile": { "final": true, - "name": "existingFile", - "id": 1494, + "name": "existingAttachmentFile", + "id": 1496, "since": 77, "type": "LIST_ELEMENT_ASSOCIATION_GENERATED", "cardinality": "ZeroOrOne", "refType": "File", "dependency": null }, - "newFile": { + "newAttachment": { "final": true, - "name": "newFile", - "id": 1493, + "name": "newAttachment", + "id": 1495, "since": 77, "type": "AGGREGATION", "cardinality": "ZeroOrOne", @@ -4125,15 +4134,15 @@ export const typeModels = { "name": "ImportMailData", "since": 77, "type": "AGGREGATED_TYPE", - "id": 1495, - "rootId": "CHR1dGFub3RhAAXX", + "id": 1497, + "rootId": "CHR1dGFub3RhAAXZ", "versioned": false, "encrypted": false, "values": { "_id": { "final": true, "name": "_id", - "id": 1496, + "id": 1498, "since": 77, "type": "CustomId", "cardinality": "One", @@ -4142,7 +4151,7 @@ export const typeModels = { "compressedBodyText": { "final": true, "name": "compressedBodyText", - "id": 1498, + "id": 1500, "since": 77, "type": "CompressedString", "cardinality": "One", @@ -4151,7 +4160,7 @@ export const typeModels = { "compressedHeaders": { "final": true, "name": "compressedHeaders", - "id": 1511, + "id": 1513, "since": 77, "type": "CompressedString", "cardinality": "One", @@ -4160,7 +4169,7 @@ export const typeModels = { "confidential": { "final": true, "name": "confidential", - "id": 1506, + "id": 1508, "since": 77, "type": "Boolean", "cardinality": "One", @@ -4169,7 +4178,7 @@ export const typeModels = { "date": { "final": true, "name": "date", - "id": 1499, + "id": 1501, "since": 77, "type": "Date", "cardinality": "One", @@ -4178,7 +4187,7 @@ export const typeModels = { "differentEnvelopeSender": { "final": true, "name": "differentEnvelopeSender", - "id": 1509, + "id": 1511, "since": 77, "type": "String", "cardinality": "ZeroOrOne", @@ -4187,7 +4196,7 @@ export const typeModels = { "inReplyTo": { "final": true, "name": "inReplyTo", - "id": 1503, + "id": 1505, "since": 77, "type": "String", "cardinality": "ZeroOrOne", @@ -4196,7 +4205,7 @@ export const typeModels = { "messageId": { "final": true, "name": "messageId", - "id": 1502, + "id": 1504, "since": 77, "type": "String", "cardinality": "ZeroOrOne", @@ -4205,7 +4214,7 @@ export const typeModels = { "method": { "final": true, "name": "method", - "id": 1507, + "id": 1509, "since": 77, "type": "Number", "cardinality": "One", @@ -4214,7 +4223,7 @@ export const typeModels = { "phishingStatus": { "final": true, "name": "phishingStatus", - "id": 1510, + "id": 1512, "since": 77, "type": "Number", "cardinality": "One", @@ -4223,7 +4232,7 @@ export const typeModels = { "replyType": { "final": false, "name": "replyType", - "id": 1508, + "id": 1510, "since": 77, "type": "Number", "cardinality": "One", @@ -4232,7 +4241,7 @@ export const typeModels = { "state": { "final": true, "name": "state", - "id": 1500, + "id": 1502, "since": 77, "type": "Number", "cardinality": "One", @@ -4241,7 +4250,7 @@ export const typeModels = { "subject": { "final": true, "name": "subject", - "id": 1497, + "id": 1499, "since": 77, "type": "String", "cardinality": "One", @@ -4250,7 +4259,7 @@ export const typeModels = { "unread": { "final": true, "name": "unread", - "id": 1501, + "id": 1503, "since": 77, "type": "Boolean", "cardinality": "One", @@ -4261,7 +4270,7 @@ export const typeModels = { "importedAttachments": { "final": true, "name": "importedAttachments", - "id": 1514, + "id": 1516, "since": 77, "type": "AGGREGATION", "cardinality": "Any", @@ -4271,7 +4280,7 @@ export const typeModels = { "recipients": { "final": true, "name": "recipients", - "id": 1513, + "id": 1515, "since": 77, "type": "AGGREGATION", "cardinality": "One", @@ -4281,7 +4290,7 @@ export const typeModels = { "references": { "final": true, "name": "references", - "id": 1504, + "id": 1506, "since": 77, "type": "AGGREGATION", "cardinality": "Any", @@ -4291,7 +4300,7 @@ export const typeModels = { "replyTos": { "final": false, "name": "replyTos", - "id": 1512, + "id": 1514, "since": 77, "type": "AGGREGATION", "cardinality": "Any", @@ -4301,7 +4310,7 @@ export const typeModels = { "sender": { "final": true, "name": "sender", - "id": 1505, + "id": 1507, "since": 77, "type": "AGGREGATION", "cardinality": "One", @@ -4316,15 +4325,15 @@ export const typeModels = { "name": "ImportMailDataMailReference", "since": 77, "type": "AGGREGATED_TYPE", - "id": 1479, - "rootId": "CHR1dGFub3RhAAXH", + "id": 1480, + "rootId": "CHR1dGFub3RhAAXI", "versioned": false, "encrypted": false, "values": { "_id": { "final": true, "name": "_id", - "id": 1480, + "id": 1481, "since": 77, "type": "CustomId", "cardinality": "One", @@ -4333,7 +4342,7 @@ export const typeModels = { "reference": { "final": false, "name": "reference", - "id": 1481, + "id": 1482, "since": 77, "type": "String", "cardinality": "One", @@ -4348,15 +4357,15 @@ export const typeModels = { "name": "ImportMailPostIn", "since": 77, "type": "DATA_TRANSFER_TYPE", - "id": 1515, - "rootId": "CHR1dGFub3RhAAXr", + "id": 1517, + "rootId": "CHR1dGFub3RhAAXt", "versioned": false, "encrypted": true, "values": { "_format": { "final": false, "name": "_format", - "id": 1516, + "id": 1518, "since": 77, "type": "Number", "cardinality": "One", @@ -4365,7 +4374,7 @@ export const typeModels = { "ownerEncSessionKey": { "final": false, "name": "ownerEncSessionKey", - "id": 1518, + "id": 1520, "since": 77, "type": "Bytes", "cardinality": "One", @@ -4374,7 +4383,7 @@ export const typeModels = { "ownerGroup": { "final": false, "name": "ownerGroup", - "id": 1517, + "id": 1519, "since": 77, "type": "GeneratedId", "cardinality": "One", @@ -4383,7 +4392,7 @@ export const typeModels = { "ownerKeyVersion": { "final": false, "name": "ownerKeyVersion", - "id": 1519, + "id": 1521, "since": 77, "type": "Number", "cardinality": "One", @@ -4394,7 +4403,7 @@ export const typeModels = { "imports": { "final": false, "name": "imports", - "id": 1521, + "id": 1523, "since": 77, "type": "AGGREGATION", "cardinality": "Any", @@ -4404,7 +4413,7 @@ export const typeModels = { "targetMailFolder": { "final": true, "name": "targetMailFolder", - "id": 1520, + "id": 1522, "since": 77, "type": "LIST_ELEMENT_ASSOCIATION_GENERATED", "cardinality": "One", @@ -4419,15 +4428,15 @@ export const typeModels = { "name": "ImportMailPostOut", "since": 77, "type": "DATA_TRANSFER_TYPE", - "id": 1522, - "rootId": "CHR1dGFub3RhAAXy", + "id": 1524, + "rootId": "CHR1dGFub3RhAAX0", "versioned": false, "encrypted": false, "values": { "_format": { "final": false, "name": "_format", - "id": 1523, + "id": 1525, "since": 77, "type": "Number", "cardinality": "One", @@ -4438,7 +4447,7 @@ export const typeModels = { "mails": { "final": false, "name": "mails", - "id": 1524, + "id": 1526, "since": 77, "type": "LIST_ELEMENT_ASSOCIATION_GENERATED", "cardinality": "Any", @@ -5380,6 +5389,16 @@ export const typeModels = { "refType": "MailFolderRef", "dependency": null }, + "importedAttachments": { + "final": false, + "name": "importedAttachments", + "id": 1479, + "since": 77, + "type": "LIST_ASSOCIATION", + "cardinality": "One", + "refType": "File", + "dependency": null + }, "mailDetailsDrafts": { "final": false, "name": "mailDetailsDrafts", @@ -6305,15 +6324,15 @@ export const typeModels = { "name": "NewImportAttachment", "since": 77, "type": "AGGREGATED_TYPE", - "id": 1482, - "rootId": "CHR1dGFub3RhAAXK", + "id": 1483, + "rootId": "CHR1dGFub3RhAAXL", "versioned": false, "encrypted": false, "values": { "_id": { "final": true, "name": "_id", - "id": 1483, + "id": 1484, "since": 77, "type": "CustomId", "cardinality": "One", @@ -6322,7 +6341,7 @@ export const typeModels = { "encCid": { "final": true, "name": "encCid", - "id": 1488, + "id": 1489, "since": 77, "type": "Bytes", "cardinality": "ZeroOrOne", @@ -6331,7 +6350,7 @@ export const typeModels = { "encFileHash": { "final": true, "name": "encFileHash", - "id": 1485, + "id": 1486, "since": 77, "type": "Bytes", "cardinality": "ZeroOrOne", @@ -6340,7 +6359,7 @@ export const typeModels = { "encFileName": { "final": true, "name": "encFileName", - "id": 1486, + "id": 1487, "since": 77, "type": "Bytes", "cardinality": "One", @@ -6349,7 +6368,7 @@ export const typeModels = { "encMimeType": { "final": true, "name": "encMimeType", - "id": 1487, + "id": 1488, "since": 77, "type": "Bytes", "cardinality": "One", @@ -6358,7 +6377,7 @@ export const typeModels = { "ownerEncFileHashSessionKey": { "final": true, "name": "ownerEncFileHashSessionKey", - "id": 1484, + "id": 1485, "since": 77, "type": "Bytes", "cardinality": "ZeroOrOne", @@ -6369,7 +6388,7 @@ export const typeModels = { "referenceTokens": { "final": true, "name": "referenceTokens", - "id": 1489, + "id": 1490, "since": 77, "type": "AGGREGATION", "cardinality": "Any", diff --git a/src/common/api/entities/tutanota/TypeRefs.ts b/src/common/api/entities/tutanota/TypeRefs.ts index efc8aabdce4..272db479e23 100644 --- a/src/common/api/entities/tutanota/TypeRefs.ts +++ b/src/common/api/entities/tutanota/TypeRefs.ts @@ -1020,9 +1020,10 @@ export type ImportAttachment = { _id: Id; ownerEncFileSessionKey: Uint8Array; + ownerFileKeyVersion: NumberString; - existingFile: null | IdTuple; - newFile: null | NewImportAttachment; + existingAttachmentFile: null | IdTuple; + newAttachment: null | NewImportAttachment; } export const ImportMailDataTypeRef: TypeRef = new TypeRef("tutanota", "ImportMailData") @@ -1301,6 +1302,7 @@ export type MailBox = { archivedMailBags: MailBag[]; currentMailBag: null | MailBag; folders: null | MailFolderRef; + importedAttachments: Id; mailDetailsDrafts: null | MailDetailsDraftsRef; receivedAttachments: Id; sentAttachments: Id; diff --git a/tuta-sdk/rust/sdk/src/entities/generated/accounting.rs b/tuta-sdk/rust/sdk/src/entities/generated/accounting.rs index 15d67fc4bd1..f76890de3fb 100644 --- a/tuta-sdk/rust/sdk/src/entities/generated/accounting.rs +++ b/tuta-sdk/rust/sdk/src/entities/generated/accounting.rs @@ -1,6 +1,6 @@ #![allow(non_snake_case, unused_imports)] -use super::super::*; use crate::*; +use super::super::*; use serde::{Deserialize, Serialize}; #[derive(uniffi::Record, Clone, Serialize, Deserialize)] @@ -23,6 +23,9 @@ impl Entity for CustomerAccountPosting { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CustomerAccountReturn { @@ -45,3 +48,6 @@ impl Entity for CustomerAccountReturn { } } } + + + diff --git a/tuta-sdk/rust/sdk/src/entities/generated/base.rs b/tuta-sdk/rust/sdk/src/entities/generated/base.rs index 31b50d738fb..d2dee68a9bb 100644 --- a/tuta-sdk/rust/sdk/src/entities/generated/base.rs +++ b/tuta-sdk/rust/sdk/src/entities/generated/base.rs @@ -1,6 +1,6 @@ #![allow(non_snake_case, unused_imports)] -use super::super::*; use crate::*; +use super::super::*; use serde::{Deserialize, Serialize}; #[derive(uniffi::Record, Clone, Serialize, Deserialize)] @@ -18,3 +18,6 @@ impl Entity for PersistenceResourcePostReturn { } } } + + + diff --git a/tuta-sdk/rust/sdk/src/entities/generated/monitor.rs b/tuta-sdk/rust/sdk/src/entities/generated/monitor.rs index fe5d3824578..302348da0bd 100644 --- a/tuta-sdk/rust/sdk/src/entities/generated/monitor.rs +++ b/tuta-sdk/rust/sdk/src/entities/generated/monitor.rs @@ -1,6 +1,6 @@ #![allow(non_snake_case, unused_imports)] -use super::super::*; use crate::*; +use super::super::*; use serde::{Deserialize, Serialize}; #[derive(uniffi::Record, Clone, Serialize, Deserialize)] @@ -24,6 +24,9 @@ impl Entity for ApprovalMail { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CounterValue { @@ -40,6 +43,9 @@ impl Entity for CounterValue { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ErrorReportData { @@ -63,6 +69,9 @@ impl Entity for ErrorReportData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ErrorReportFile { @@ -79,6 +88,9 @@ impl Entity for ErrorReportFile { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ReadCounterData { @@ -96,6 +108,9 @@ impl Entity for ReadCounterData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ReadCounterReturn { @@ -112,6 +127,9 @@ impl Entity for ReadCounterReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ReportErrorIn { @@ -128,6 +146,9 @@ impl Entity for ReportErrorIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct WriteCounterData { @@ -145,3 +166,6 @@ impl Entity for WriteCounterData { } } } + + + diff --git a/tuta-sdk/rust/sdk/src/entities/generated/storage.rs b/tuta-sdk/rust/sdk/src/entities/generated/storage.rs index cbcd77fb18c..e3002eeb17b 100644 --- a/tuta-sdk/rust/sdk/src/entities/generated/storage.rs +++ b/tuta-sdk/rust/sdk/src/entities/generated/storage.rs @@ -1,6 +1,6 @@ #![allow(non_snake_case, unused_imports)] -use super::super::*; use crate::*; +use super::super::*; use serde::{Deserialize, Serialize}; #[derive(uniffi::Record, Clone, Serialize, Deserialize)] @@ -20,6 +20,9 @@ impl Entity for BlobAccessTokenPostIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct BlobAccessTokenPostOut { @@ -35,6 +38,9 @@ impl Entity for BlobAccessTokenPostOut { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct BlobArchiveRef { @@ -53,6 +59,9 @@ impl Entity for BlobArchiveRef { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct BlobGetIn { @@ -70,6 +79,9 @@ impl Entity for BlobGetIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct BlobId { @@ -85,6 +97,9 @@ impl Entity for BlobId { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct BlobPostOut { @@ -100,6 +115,9 @@ impl Entity for BlobPostOut { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct BlobReadData { @@ -117,6 +135,9 @@ impl Entity for BlobReadData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct BlobReferenceDeleteIn { @@ -135,6 +156,9 @@ impl Entity for BlobReferenceDeleteIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct BlobReferencePutIn { @@ -153,6 +177,9 @@ impl Entity for BlobReferencePutIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct BlobServerAccessInfo { @@ -170,6 +197,9 @@ impl Entity for BlobServerAccessInfo { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct BlobServerUrl { @@ -185,6 +215,9 @@ impl Entity for BlobServerUrl { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct BlobWriteData { @@ -200,6 +233,9 @@ impl Entity for BlobWriteData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct InstanceId { @@ -214,3 +250,6 @@ impl Entity for InstanceId { } } } + + + diff --git a/tuta-sdk/rust/sdk/src/entities/generated/sys.rs b/tuta-sdk/rust/sdk/src/entities/generated/sys.rs index f8d6cb82610..05d4c9f4fbb 100644 --- a/tuta-sdk/rust/sdk/src/entities/generated/sys.rs +++ b/tuta-sdk/rust/sdk/src/entities/generated/sys.rs @@ -1,6 +1,6 @@ #![allow(non_snake_case, unused_imports)] -use super::super::*; use crate::*; +use super::super::*; use serde::{Deserialize, Serialize}; #[derive(uniffi::Record, Clone, Serialize, Deserialize)] @@ -41,6 +41,9 @@ impl Entity for AccountingInfo { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct AdminGroupKeyAuthenticationData { @@ -59,6 +62,9 @@ impl Entity for AdminGroupKeyAuthenticationData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct AdminGroupKeyRotationPostIn { @@ -76,6 +82,9 @@ impl Entity for AdminGroupKeyRotationPostIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct AdministratedGroup { @@ -96,6 +105,9 @@ impl Entity for AdministratedGroup { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct AdministratedGroupsRef { @@ -111,6 +123,9 @@ impl Entity for AdministratedGroupsRef { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct AffiliatePartnerKpiMonthSummary { @@ -131,6 +146,9 @@ impl Entity for AffiliatePartnerKpiMonthSummary { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct AffiliatePartnerKpiServiceGetOut { @@ -149,6 +167,9 @@ impl Entity for AffiliatePartnerKpiServiceGetOut { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct AlarmInfo { @@ -167,6 +188,9 @@ impl Entity for AlarmInfo { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct AlarmNotification { @@ -190,6 +214,9 @@ impl Entity for AlarmNotification { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct AlarmServicePost { @@ -207,6 +234,9 @@ impl Entity for AlarmServicePost { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ArchiveRef { @@ -222,6 +252,9 @@ impl Entity for ArchiveRef { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ArchiveType { @@ -240,6 +273,9 @@ impl Entity for ArchiveType { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct AuditLogEntry { @@ -269,6 +305,9 @@ impl Entity for AuditLogEntry { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct AuditLogRef { @@ -284,6 +323,9 @@ impl Entity for AuditLogRef { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct AuthenticatedDevice { @@ -302,6 +344,9 @@ impl Entity for AuthenticatedDevice { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Authentication { @@ -320,6 +365,9 @@ impl Entity for Authentication { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct AutoLoginDataDelete { @@ -335,6 +383,9 @@ impl Entity for AutoLoginDataDelete { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct AutoLoginDataGet { @@ -351,6 +402,9 @@ impl Entity for AutoLoginDataGet { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct AutoLoginDataReturn { @@ -367,6 +421,9 @@ impl Entity for AutoLoginDataReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct AutoLoginPostReturn { @@ -382,6 +439,9 @@ impl Entity for AutoLoginPostReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Blob { @@ -399,6 +459,9 @@ impl Entity for Blob { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct BlobReferenceTokenWrapper { @@ -414,6 +477,9 @@ impl Entity for BlobReferenceTokenWrapper { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Booking { @@ -439,6 +505,9 @@ impl Entity for Booking { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct BookingItem { @@ -460,6 +529,9 @@ impl Entity for BookingItem { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct BookingsRef { @@ -475,6 +547,9 @@ impl Entity for BookingsRef { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct BootstrapFeature { @@ -490,6 +565,9 @@ impl Entity for BootstrapFeature { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Braintree3ds2Request { @@ -507,6 +585,9 @@ impl Entity for Braintree3ds2Request { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Braintree3ds2Response { @@ -523,6 +604,9 @@ impl Entity for Braintree3ds2Response { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct BrandingDomainData { @@ -546,6 +630,9 @@ impl Entity for BrandingDomainData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct BrandingDomainDeleteData { @@ -561,6 +648,9 @@ impl Entity for BrandingDomainDeleteData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct BrandingDomainGetReturn { @@ -576,6 +666,9 @@ impl Entity for BrandingDomainGetReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Bucket { @@ -591,6 +684,9 @@ impl Entity for Bucket { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct BucketKey { @@ -614,6 +710,9 @@ impl Entity for BucketKey { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct BucketPermission { @@ -645,6 +744,9 @@ impl Entity for BucketPermission { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CalendarEventRef { @@ -661,6 +763,9 @@ impl Entity for CalendarEventRef { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CertificateInfo { @@ -680,6 +785,9 @@ impl Entity for CertificateInfo { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Challenge { @@ -698,6 +806,9 @@ impl Entity for Challenge { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ChangeKdfPostIn { @@ -722,6 +833,9 @@ impl Entity for ChangeKdfPostIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ChangePasswordPostIn { @@ -749,6 +863,9 @@ impl Entity for ChangePasswordPostIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Chat { @@ -766,6 +883,9 @@ impl Entity for Chat { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CloseSessionServicePost { @@ -782,6 +902,9 @@ impl Entity for CloseSessionServicePost { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CreateCustomerServerPropertiesData { @@ -799,6 +922,9 @@ impl Entity for CreateCustomerServerPropertiesData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CreateCustomerServerPropertiesReturn { @@ -814,6 +940,9 @@ impl Entity for CreateCustomerServerPropertiesReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CreateSessionData { @@ -836,6 +965,9 @@ impl Entity for CreateSessionData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CreateSessionReturn { @@ -853,6 +985,9 @@ impl Entity for CreateSessionReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CreditCard { @@ -873,6 +1008,9 @@ impl Entity for CreditCard { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CustomDomainCheckGetIn { @@ -889,6 +1027,9 @@ impl Entity for CustomDomainCheckGetIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CustomDomainCheckGetOut { @@ -907,6 +1048,9 @@ impl Entity for CustomDomainCheckGetOut { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CustomDomainData { @@ -923,6 +1067,9 @@ impl Entity for CustomDomainData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CustomDomainReturn { @@ -939,6 +1086,9 @@ impl Entity for CustomDomainReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Customer { @@ -978,6 +1128,9 @@ impl Entity for Customer { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CustomerAccountTerminationPostIn { @@ -994,6 +1147,9 @@ impl Entity for CustomerAccountTerminationPostIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CustomerAccountTerminationPostOut { @@ -1009,6 +1165,9 @@ impl Entity for CustomerAccountTerminationPostOut { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CustomerAccountTerminationRequest { @@ -1029,6 +1188,9 @@ impl Entity for CustomerAccountTerminationRequest { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CustomerInfo { @@ -1074,6 +1236,9 @@ impl Entity for CustomerInfo { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CustomerProperties { @@ -1097,6 +1262,9 @@ impl Entity for CustomerProperties { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CustomerServerProperties { @@ -1124,6 +1292,9 @@ impl Entity for CustomerServerProperties { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct DateWrapper { @@ -1140,6 +1311,9 @@ impl Entity for DateWrapper { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct DebitServicePutData { @@ -1155,6 +1329,9 @@ impl Entity for DebitServicePutData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct DeleteCustomerData { @@ -1176,6 +1353,9 @@ impl Entity for DeleteCustomerData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct DnsRecord { @@ -1194,6 +1374,9 @@ impl Entity for DnsRecord { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct DomainInfo { @@ -1212,6 +1395,9 @@ impl Entity for DomainInfo { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct DomainMailAddressAvailabilityData { @@ -1227,6 +1413,9 @@ impl Entity for DomainMailAddressAvailabilityData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct DomainMailAddressAvailabilityReturn { @@ -1242,6 +1431,9 @@ impl Entity for DomainMailAddressAvailabilityReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct EmailSenderListElement { @@ -1262,6 +1454,9 @@ impl Entity for EmailSenderListElement { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct EntityEventBatch { @@ -1280,6 +1475,9 @@ impl Entity for EntityEventBatch { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct EntityUpdate { @@ -1300,6 +1498,9 @@ impl Entity for EntityUpdate { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SysException { @@ -1317,6 +1518,9 @@ impl Entity for SysException { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ExternalPropertiesReturn { @@ -1335,6 +1539,9 @@ impl Entity for ExternalPropertiesReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ExternalUserReference { @@ -1354,6 +1561,9 @@ impl Entity for ExternalUserReference { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Feature { @@ -1369,6 +1579,9 @@ impl Entity for Feature { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct File { @@ -1387,6 +1600,9 @@ impl Entity for File { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GeneratedIdWrapper { @@ -1402,6 +1618,9 @@ impl Entity for GeneratedIdWrapper { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GiftCard { @@ -1429,6 +1648,9 @@ impl Entity for GiftCard { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GiftCardCreateData { @@ -1452,6 +1674,9 @@ impl Entity for GiftCardCreateData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GiftCardCreateReturn { @@ -1467,6 +1692,9 @@ impl Entity for GiftCardCreateReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GiftCardDeleteData { @@ -1482,6 +1710,9 @@ impl Entity for GiftCardDeleteData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GiftCardGetReturn { @@ -1499,6 +1730,9 @@ impl Entity for GiftCardGetReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GiftCardOption { @@ -1514,6 +1748,9 @@ impl Entity for GiftCardOption { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GiftCardRedeemData { @@ -1532,6 +1769,9 @@ impl Entity for GiftCardRedeemData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GiftCardRedeemGetReturn { @@ -1551,6 +1791,9 @@ impl Entity for GiftCardRedeemGetReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GiftCardsRef { @@ -1566,6 +1809,9 @@ impl Entity for GiftCardsRef { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Group { @@ -1603,6 +1849,9 @@ impl Entity for Group { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GroupInfo { @@ -1635,6 +1884,9 @@ impl Entity for GroupInfo { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GroupKey { @@ -1660,8 +1912,11 @@ impl Entity for GroupKey { } } -#[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] + + + +#[derive(uniffi::Record, Clone, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Debug))] pub struct GroupKeyRotationData { pub _id: Option, #[serde(with = "serde_bytes")] @@ -1684,6 +1939,9 @@ impl Entity for GroupKeyRotationData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GroupKeyRotationInfoGetOut { @@ -1700,6 +1958,9 @@ impl Entity for GroupKeyRotationInfoGetOut { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GroupKeyRotationPostIn { @@ -1715,6 +1976,9 @@ impl Entity for GroupKeyRotationPostIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GroupKeyUpdate { @@ -1741,6 +2005,9 @@ impl Entity for GroupKeyUpdate { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GroupKeyUpdateData { @@ -1761,6 +2028,9 @@ impl Entity for GroupKeyUpdateData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GroupKeyUpdatesRef { @@ -1776,6 +2046,9 @@ impl Entity for GroupKeyUpdatesRef { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GroupKeysRef { @@ -1791,6 +2064,9 @@ impl Entity for GroupKeysRef { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GroupMember { @@ -1812,6 +2088,9 @@ impl Entity for GroupMember { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GroupMembership { @@ -1836,6 +2115,9 @@ impl Entity for GroupMembership { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GroupMembershipKeyData { @@ -1855,6 +2137,9 @@ impl Entity for GroupMembershipKeyData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GroupMembershipUpdateData { @@ -1873,6 +2158,9 @@ impl Entity for GroupMembershipUpdateData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GroupRoot { @@ -1893,6 +2181,9 @@ impl Entity for GroupRoot { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct IdTupleWrapper { @@ -1909,6 +2200,9 @@ impl Entity for IdTupleWrapper { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct InstanceSessionKey { @@ -1931,6 +2225,9 @@ impl Entity for InstanceSessionKey { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Invoice { @@ -1970,6 +2267,9 @@ impl Entity for Invoice { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct InvoiceDataGetIn { @@ -1985,6 +2285,9 @@ impl Entity for InvoiceDataGetIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct InvoiceDataGetOut { @@ -2012,6 +2315,9 @@ impl Entity for InvoiceDataGetOut { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct InvoiceDataItem { @@ -2032,6 +2338,9 @@ impl Entity for InvoiceDataItem { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct InvoiceInfo { @@ -2063,6 +2372,9 @@ impl Entity for InvoiceInfo { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct InvoiceItem { @@ -2086,6 +2398,9 @@ impl Entity for InvoiceItem { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct KeyPair { @@ -2112,6 +2427,9 @@ impl Entity for KeyPair { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct KeyRotation { @@ -2132,6 +2450,9 @@ impl Entity for KeyRotation { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct KeyRotationsRef { @@ -2147,6 +2468,9 @@ impl Entity for KeyRotationsRef { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct LocalAdminGroupReplacementData { @@ -2166,6 +2490,9 @@ impl Entity for LocalAdminGroupReplacementData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct LocalAdminRemovalPostIn { @@ -2181,6 +2508,9 @@ impl Entity for LocalAdminRemovalPostIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct LocationServiceGetReturn { @@ -2196,6 +2526,9 @@ impl Entity for LocationServiceGetReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Login { @@ -2214,6 +2547,9 @@ impl Entity for Login { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailAddressAlias { @@ -2230,6 +2566,9 @@ impl Entity for MailAddressAlias { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailAddressAliasGetIn { @@ -2245,6 +2584,9 @@ impl Entity for MailAddressAliasGetIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailAddressAliasServiceData { @@ -2261,6 +2603,9 @@ impl Entity for MailAddressAliasServiceData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailAddressAliasServiceDataDelete { @@ -2278,6 +2623,9 @@ impl Entity for MailAddressAliasServiceDataDelete { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailAddressAliasServiceReturn { @@ -2296,6 +2644,9 @@ impl Entity for MailAddressAliasServiceReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailAddressAvailability { @@ -2312,6 +2663,9 @@ impl Entity for MailAddressAvailability { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailAddressToGroup { @@ -2330,6 +2684,9 @@ impl Entity for MailAddressToGroup { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MembershipAddData { @@ -2350,6 +2707,9 @@ impl Entity for MembershipAddData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MembershipPutIn { @@ -2365,6 +2725,9 @@ impl Entity for MembershipPutIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MembershipRemoveData { @@ -2381,6 +2744,9 @@ impl Entity for MembershipRemoveData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MissedNotification { @@ -2408,6 +2774,9 @@ impl Entity for MissedNotification { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MultipleMailAddressAvailabilityData { @@ -2423,6 +2792,9 @@ impl Entity for MultipleMailAddressAvailabilityData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MultipleMailAddressAvailabilityReturn { @@ -2438,6 +2810,9 @@ impl Entity for MultipleMailAddressAvailabilityReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct NotificationInfo { @@ -2455,6 +2830,9 @@ impl Entity for NotificationInfo { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct NotificationMailTemplate { @@ -2472,6 +2850,9 @@ impl Entity for NotificationMailTemplate { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct NotificationSessionKey { @@ -2489,6 +2870,9 @@ impl Entity for NotificationSessionKey { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct OrderProcessingAgreement { @@ -2516,6 +2900,9 @@ impl Entity for OrderProcessingAgreement { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct OtpChallenge { @@ -2531,6 +2918,9 @@ impl Entity for OtpChallenge { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PaymentDataServiceGetData { @@ -2546,6 +2936,9 @@ impl Entity for PaymentDataServiceGetData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PaymentDataServiceGetReturn { @@ -2561,6 +2954,9 @@ impl Entity for PaymentDataServiceGetReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PaymentDataServicePostData { @@ -2576,6 +2972,9 @@ impl Entity for PaymentDataServicePostData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PaymentDataServicePutData { @@ -2602,6 +3001,9 @@ impl Entity for PaymentDataServicePutData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PaymentDataServicePutReturn { @@ -2618,6 +3020,9 @@ impl Entity for PaymentDataServicePutReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PaymentErrorInfo { @@ -2635,6 +3040,9 @@ impl Entity for PaymentErrorInfo { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Permission { @@ -2667,6 +3075,9 @@ impl Entity for Permission { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PlanConfiguration { @@ -2691,6 +3102,9 @@ impl Entity for PlanConfiguration { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PlanPrices { @@ -2718,6 +3132,9 @@ impl Entity for PlanPrices { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PlanServiceGetOut { @@ -2733,6 +3150,9 @@ impl Entity for PlanServiceGetOut { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PriceData { @@ -2751,6 +3171,9 @@ impl Entity for PriceData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PriceItemData { @@ -2769,6 +3192,9 @@ impl Entity for PriceItemData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PriceRequestData { @@ -2789,6 +3215,9 @@ impl Entity for PriceRequestData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PriceServiceData { @@ -2805,6 +3234,9 @@ impl Entity for PriceServiceData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PriceServiceReturn { @@ -2824,6 +3256,9 @@ impl Entity for PriceServiceReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PubEncKeyData { @@ -2845,6 +3280,9 @@ impl Entity for PubEncKeyData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PublicKeyGetIn { @@ -2862,6 +3300,9 @@ impl Entity for PublicKeyGetIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PublicKeyGetOut { @@ -2883,6 +3324,9 @@ impl Entity for PublicKeyGetOut { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PublicKeyPutIn { @@ -2902,6 +3346,9 @@ impl Entity for PublicKeyPutIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PushIdentifier { @@ -2934,6 +3381,9 @@ impl Entity for PushIdentifier { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PushIdentifierList { @@ -2949,6 +3399,9 @@ impl Entity for PushIdentifierList { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ReceivedGroupInvitation { @@ -2982,6 +3435,9 @@ impl Entity for ReceivedGroupInvitation { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct RecoverCode { @@ -3006,6 +3462,9 @@ impl Entity for RecoverCode { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct RecoverCodeData { @@ -3027,6 +3486,9 @@ impl Entity for RecoverCodeData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ReferralCodeGetIn { @@ -3042,6 +3504,9 @@ impl Entity for ReferralCodeGetIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ReferralCodePostIn { @@ -3056,6 +3521,9 @@ impl Entity for ReferralCodePostIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ReferralCodePostOut { @@ -3071,6 +3539,9 @@ impl Entity for ReferralCodePostOut { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct RegistrationCaptchaServiceData { @@ -3087,6 +3558,9 @@ impl Entity for RegistrationCaptchaServiceData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct RegistrationCaptchaServiceGetData { @@ -3106,6 +3580,9 @@ impl Entity for RegistrationCaptchaServiceGetData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct RegistrationCaptchaServiceReturn { @@ -3123,6 +3600,9 @@ impl Entity for RegistrationCaptchaServiceReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct RegistrationReturn { @@ -3138,6 +3618,9 @@ impl Entity for RegistrationReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct RegistrationServiceData { @@ -3155,6 +3638,9 @@ impl Entity for RegistrationServiceData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct RejectedSender { @@ -3177,6 +3663,9 @@ impl Entity for RejectedSender { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct RejectedSendersRef { @@ -3192,6 +3681,9 @@ impl Entity for RejectedSendersRef { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct RepeatRule { @@ -3213,6 +3705,9 @@ impl Entity for RepeatRule { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ResetFactorsDeleteData { @@ -3230,6 +3725,9 @@ impl Entity for ResetFactorsDeleteData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ResetPasswordPostIn { @@ -3253,6 +3751,9 @@ impl Entity for ResetPasswordPostIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct RootInstance { @@ -3271,6 +3772,9 @@ impl Entity for RootInstance { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SaltData { @@ -3286,6 +3790,9 @@ impl Entity for SaltData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SaltReturn { @@ -3303,6 +3810,9 @@ impl Entity for SaltReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SecondFactor { @@ -3326,6 +3836,9 @@ impl Entity for SecondFactor { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SecondFactorAuthAllowedReturn { @@ -3341,6 +3854,9 @@ impl Entity for SecondFactorAuthAllowedReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SecondFactorAuthData { @@ -3361,6 +3877,9 @@ impl Entity for SecondFactorAuthData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SecondFactorAuthDeleteData { @@ -3376,6 +3895,9 @@ impl Entity for SecondFactorAuthDeleteData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SecondFactorAuthGetData { @@ -3391,6 +3913,9 @@ impl Entity for SecondFactorAuthGetData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SecondFactorAuthGetReturn { @@ -3406,6 +3931,9 @@ impl Entity for SecondFactorAuthGetReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SecondFactorAuthentication { @@ -3427,6 +3955,9 @@ impl Entity for SecondFactorAuthentication { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SendRegistrationCodeData { @@ -3445,6 +3976,9 @@ impl Entity for SendRegistrationCodeData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SendRegistrationCodeReturn { @@ -3460,6 +3994,9 @@ impl Entity for SendRegistrationCodeReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SentGroupInvitation { @@ -3481,6 +4018,9 @@ impl Entity for SentGroupInvitation { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Session { @@ -3512,6 +4052,9 @@ impl Entity for Session { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SignOrderProcessingAgreementData { @@ -3528,6 +4071,9 @@ impl Entity for SignOrderProcessingAgreementData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SseConnectData { @@ -3544,6 +4090,9 @@ impl Entity for SseConnectData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct StringConfigValue { @@ -3560,6 +4109,9 @@ impl Entity for StringConfigValue { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct StringWrapper { @@ -3575,6 +4127,9 @@ impl Entity for StringWrapper { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SurveyData { @@ -3593,6 +4148,9 @@ impl Entity for SurveyData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SwitchAccountTypePostIn { @@ -3614,6 +4172,9 @@ impl Entity for SwitchAccountTypePostIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SystemKeysReturn { @@ -3643,6 +4204,9 @@ impl Entity for SystemKeysReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct TakeOverDeletedAddressData { @@ -3661,6 +4225,9 @@ impl Entity for TakeOverDeletedAddressData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct TypeInfo { @@ -3677,6 +4244,9 @@ impl Entity for TypeInfo { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct U2fChallenge { @@ -3694,6 +4264,9 @@ impl Entity for U2fChallenge { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct U2fKey { @@ -3712,6 +4285,9 @@ impl Entity for U2fKey { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct U2fRegisteredDevice { @@ -3733,6 +4309,9 @@ impl Entity for U2fRegisteredDevice { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct U2fResponseData { @@ -3750,6 +4329,9 @@ impl Entity for U2fResponseData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UpdatePermissionKeyData { @@ -3769,6 +4351,9 @@ impl Entity for UpdatePermissionKeyData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UpdateSessionKeysPostIn { @@ -3784,6 +4369,9 @@ impl Entity for UpdateSessionKeysPostIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UpgradePriceServiceData { @@ -3801,6 +4389,9 @@ impl Entity for UpgradePriceServiceData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UpgradePriceServiceReturn { @@ -3830,6 +4421,9 @@ impl Entity for UpgradePriceServiceReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct User { @@ -3866,6 +4460,9 @@ impl Entity for User { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UserAlarmInfo { @@ -3889,6 +4486,9 @@ impl Entity for UserAlarmInfo { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UserAlarmInfoListType { @@ -3904,6 +4504,9 @@ impl Entity for UserAlarmInfoListType { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UserAreaGroups { @@ -3919,6 +4522,9 @@ impl Entity for UserAreaGroups { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UserAuthentication { @@ -3936,6 +4542,9 @@ impl Entity for UserAuthentication { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UserDataDelete { @@ -3953,6 +4562,9 @@ impl Entity for UserDataDelete { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UserExternalAuthInfo { @@ -3973,6 +4585,9 @@ impl Entity for UserExternalAuthInfo { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UserGroupKeyDistribution { @@ -3993,6 +4608,9 @@ impl Entity for UserGroupKeyDistribution { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UserGroupKeyRotationData { @@ -4023,6 +4641,9 @@ impl Entity for UserGroupKeyRotationData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UserGroupKeyRotationPostIn { @@ -4038,6 +4659,9 @@ impl Entity for UserGroupKeyRotationPostIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UserGroupRoot { @@ -4058,6 +4682,9 @@ impl Entity for UserGroupRoot { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct VariableExternalAuthInfo { @@ -4083,6 +4710,9 @@ impl Entity for VariableExternalAuthInfo { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct VerifyRegistrationCodeData { @@ -4099,6 +4729,9 @@ impl Entity for VerifyRegistrationCodeData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Version { @@ -4118,6 +4751,9 @@ impl Entity for Version { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct VersionData { @@ -4136,6 +4772,9 @@ impl Entity for VersionData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct VersionInfo { @@ -4163,6 +4802,9 @@ impl Entity for VersionInfo { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct VersionReturn { @@ -4178,6 +4820,9 @@ impl Entity for VersionReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct WebauthnResponseData { @@ -4200,6 +4845,9 @@ impl Entity for WebauthnResponseData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct WebsocketCounterData { @@ -4216,6 +4864,9 @@ impl Entity for WebsocketCounterData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct WebsocketCounterValue { @@ -4232,6 +4883,9 @@ impl Entity for WebsocketCounterValue { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct WebsocketEntityData { @@ -4249,6 +4903,9 @@ impl Entity for WebsocketEntityData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct WebsocketLeaderStatus { @@ -4264,6 +4921,9 @@ impl Entity for WebsocketLeaderStatus { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct WhitelabelChild { @@ -4291,6 +4951,9 @@ impl Entity for WhitelabelChild { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct WhitelabelChildrenRef { @@ -4306,6 +4969,9 @@ impl Entity for WhitelabelChildrenRef { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct WhitelabelConfig { @@ -4332,6 +4998,9 @@ impl Entity for WhitelabelConfig { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct WhitelabelParent { @@ -4347,3 +5016,6 @@ impl Entity for WhitelabelParent { } } } + + + diff --git a/tuta-sdk/rust/sdk/src/entities/generated/tutanota.rs b/tuta-sdk/rust/sdk/src/entities/generated/tutanota.rs index 6df61e5a93f..aa9fd753377 100644 --- a/tuta-sdk/rust/sdk/src/entities/generated/tutanota.rs +++ b/tuta-sdk/rust/sdk/src/entities/generated/tutanota.rs @@ -1,6 +1,6 @@ #![allow(non_snake_case, unused_imports)] -use super::super::*; use crate::*; +use super::super::*; use serde::{Deserialize, Serialize}; #[derive(uniffi::Record, Clone, Serialize, Deserialize)] @@ -22,6 +22,9 @@ impl Entity for AttachmentKeyData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Birthday { @@ -39,6 +42,9 @@ impl Entity for Birthday { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Body { @@ -56,6 +62,9 @@ impl Entity for Body { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CalendarDeleteData { @@ -71,6 +80,9 @@ impl Entity for CalendarDeleteData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CalendarEvent { @@ -108,6 +120,9 @@ impl Entity for CalendarEvent { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CalendarEventAttendee { @@ -125,6 +140,9 @@ impl Entity for CalendarEventAttendee { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CalendarEventIndexRef { @@ -140,6 +158,9 @@ impl Entity for CalendarEventIndexRef { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CalendarEventUidIndex { @@ -159,6 +180,9 @@ impl Entity for CalendarEventUidIndex { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CalendarEventUpdate { @@ -183,6 +207,9 @@ impl Entity for CalendarEventUpdate { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CalendarEventUpdateList { @@ -198,6 +225,9 @@ impl Entity for CalendarEventUpdateList { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CalendarGroupRoot { @@ -223,6 +253,9 @@ impl Entity for CalendarGroupRoot { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CalendarRepeatRule { @@ -244,6 +277,9 @@ impl Entity for CalendarRepeatRule { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Contact { @@ -293,6 +329,9 @@ impl Entity for Contact { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ContactAddress { @@ -312,6 +351,9 @@ impl Entity for ContactAddress { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ContactCustomDate { @@ -331,6 +373,9 @@ impl Entity for ContactCustomDate { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ContactList { @@ -355,6 +400,9 @@ impl Entity for ContactList { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ContactListEntry { @@ -378,6 +426,9 @@ impl Entity for ContactListEntry { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ContactListGroupRoot { @@ -401,6 +452,9 @@ impl Entity for ContactListGroupRoot { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ContactMailAddress { @@ -420,6 +474,9 @@ impl Entity for ContactMailAddress { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ContactMessengerHandle { @@ -439,6 +496,9 @@ impl Entity for ContactMessengerHandle { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ContactPhoneNumber { @@ -458,6 +518,9 @@ impl Entity for ContactPhoneNumber { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ContactPronouns { @@ -475,6 +538,9 @@ impl Entity for ContactPronouns { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ContactRelationship { @@ -494,6 +560,9 @@ impl Entity for ContactRelationship { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ContactSocialId { @@ -513,6 +582,9 @@ impl Entity for ContactSocialId { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ContactWebsite { @@ -532,6 +604,9 @@ impl Entity for ContactWebsite { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ConversationEntry { @@ -553,6 +628,9 @@ impl Entity for ConversationEntry { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CreateExternalUserGroupData { @@ -573,6 +651,9 @@ impl Entity for CreateExternalUserGroupData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CreateGroupPostReturn { @@ -590,6 +671,9 @@ impl Entity for CreateGroupPostReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CreateMailFolderData { @@ -612,6 +696,9 @@ impl Entity for CreateMailFolderData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CreateMailFolderReturn { @@ -629,6 +716,9 @@ impl Entity for CreateMailFolderReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CreateMailGroupData { @@ -649,6 +739,9 @@ impl Entity for CreateMailGroupData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct CustomerAccountCreateData { @@ -684,6 +777,9 @@ impl Entity for CustomerAccountCreateData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct DefaultAlarmInfo { @@ -700,6 +796,9 @@ impl Entity for DefaultAlarmInfo { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct DeleteGroupData { @@ -716,6 +815,9 @@ impl Entity for DeleteGroupData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct DeleteMailData { @@ -732,6 +834,9 @@ impl Entity for DeleteMailData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct DeleteMailFolderData { @@ -749,6 +854,9 @@ impl Entity for DeleteMailFolderData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct DraftAttachment { @@ -768,6 +876,9 @@ impl Entity for DraftAttachment { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct DraftCreateData { @@ -790,6 +901,9 @@ impl Entity for DraftCreateData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct DraftCreateReturn { @@ -805,6 +919,9 @@ impl Entity for DraftCreateReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct DraftData { @@ -833,6 +950,9 @@ impl Entity for DraftData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct DraftRecipient { @@ -850,6 +970,9 @@ impl Entity for DraftRecipient { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct DraftUpdateData { @@ -868,6 +991,9 @@ impl Entity for DraftUpdateData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct DraftUpdateReturn { @@ -885,6 +1011,9 @@ impl Entity for DraftUpdateReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct EmailTemplate { @@ -910,6 +1039,9 @@ impl Entity for EmailTemplate { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct EmailTemplateContent { @@ -927,6 +1059,9 @@ impl Entity for EmailTemplateContent { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct EncryptTutanotaPropertiesData { @@ -945,6 +1080,9 @@ impl Entity for EncryptTutanotaPropertiesData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct EncryptedMailAddress { @@ -962,6 +1100,9 @@ impl Entity for EncryptedMailAddress { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct EntropyData { @@ -979,6 +1120,9 @@ impl Entity for EntropyData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ExternalUserData { @@ -1014,6 +1158,9 @@ impl Entity for ExternalUserData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct TutanotaFile { @@ -1043,6 +1190,9 @@ impl Entity for TutanotaFile { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct FileSystem { @@ -1066,6 +1216,9 @@ impl Entity for FileSystem { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GroupInvitationDeleteData { @@ -1081,6 +1234,9 @@ impl Entity for GroupInvitationDeleteData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GroupInvitationPostData { @@ -1097,6 +1253,9 @@ impl Entity for GroupInvitationPostData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GroupInvitationPostReturn { @@ -1114,6 +1273,9 @@ impl Entity for GroupInvitationPostReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GroupInvitationPutData { @@ -1135,6 +1297,9 @@ impl Entity for GroupInvitationPutData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct GroupSettings { @@ -1155,6 +1320,9 @@ impl Entity for GroupSettings { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Header { @@ -1172,6 +1340,9 @@ impl Entity for Header { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ImapFolder { @@ -1190,6 +1361,9 @@ impl Entity for ImapFolder { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ImapSyncConfiguration { @@ -1209,6 +1383,9 @@ impl Entity for ImapSyncConfiguration { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ImapSyncState { @@ -1227,14 +1404,18 @@ impl Entity for ImapSyncState { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ImportAttachment { pub _id: Option, #[serde(with = "serde_bytes")] pub ownerEncFileSessionKey: Vec, - pub existingFile: Option, - pub newFile: Option, + pub ownerFileKeyVersion: i64, + pub existingAttachmentFile: Option, + pub newAttachment: Option, } impl Entity for ImportAttachment { fn type_ref() -> TypeRef { @@ -1245,6 +1426,9 @@ impl Entity for ImportAttachment { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ImportMailData { @@ -1278,6 +1462,9 @@ impl Entity for ImportMailData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ImportMailDataMailReference { @@ -1293,6 +1480,9 @@ impl Entity for ImportMailDataMailReference { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ImportMailPostIn { @@ -1315,6 +1505,9 @@ impl Entity for ImportMailPostIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ImportMailPostOut { @@ -1330,6 +1523,9 @@ impl Entity for ImportMailPostOut { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct InboxRule { @@ -1349,6 +1545,9 @@ impl Entity for InboxRule { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct InternalGroupData { @@ -1382,6 +1581,9 @@ impl Entity for InternalGroupData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct InternalRecipientKeyData { @@ -1402,6 +1604,9 @@ impl Entity for InternalRecipientKeyData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct KnowledgeBaseEntry { @@ -1427,6 +1632,9 @@ impl Entity for KnowledgeBaseEntry { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct KnowledgeBaseEntryKeyword { @@ -1443,6 +1651,9 @@ impl Entity for KnowledgeBaseEntryKeyword { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ListUnsubscribeData { @@ -1460,6 +1671,9 @@ impl Entity for ListUnsubscribeData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Mail { @@ -1504,6 +1718,9 @@ impl Entity for Mail { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailAddress { @@ -1522,6 +1739,9 @@ impl Entity for MailAddress { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailAddressProperties { @@ -1539,6 +1759,9 @@ impl Entity for MailAddressProperties { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailBag { @@ -1554,6 +1777,9 @@ impl Entity for MailBag { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailBox { @@ -1568,6 +1794,7 @@ pub struct MailBox { pub archivedMailBags: Vec, pub currentMailBag: Option, pub folders: Option, + pub importedAttachments: GeneratedId, pub mailDetailsDrafts: Option, pub receivedAttachments: GeneratedId, pub sentAttachments: GeneratedId, @@ -1584,6 +1811,9 @@ impl Entity for MailBox { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailDetails { @@ -1604,6 +1834,9 @@ impl Entity for MailDetails { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailDetailsBlob { @@ -1627,6 +1860,9 @@ impl Entity for MailDetailsBlob { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailDetailsDraft { @@ -1650,6 +1886,9 @@ impl Entity for MailDetailsDraft { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailDetailsDraftsRef { @@ -1665,6 +1904,9 @@ impl Entity for MailDetailsDraftsRef { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailFolder { @@ -1694,6 +1936,9 @@ impl Entity for MailFolder { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailFolderRef { @@ -1709,6 +1954,9 @@ impl Entity for MailFolderRef { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailSetEntry { @@ -1727,6 +1975,9 @@ impl Entity for MailSetEntry { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailboxGroupRoot { @@ -1750,6 +2001,9 @@ impl Entity for MailboxGroupRoot { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailboxProperties { @@ -1774,6 +2028,9 @@ impl Entity for MailboxProperties { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MailboxServerProperties { @@ -1792,6 +2049,9 @@ impl Entity for MailboxServerProperties { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct MoveMailData { @@ -1809,6 +2069,9 @@ impl Entity for MoveMailData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct NewDraftAttachment { @@ -1830,6 +2093,9 @@ impl Entity for NewDraftAttachment { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct NewImportAttachment { @@ -1855,6 +2121,9 @@ impl Entity for NewImportAttachment { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct NewsId { @@ -1871,6 +2140,9 @@ impl Entity for NewsId { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct NewsIn { @@ -1886,6 +2158,9 @@ impl Entity for NewsIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct NewsOut { @@ -1901,6 +2176,9 @@ impl Entity for NewsOut { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct NotificationMail { @@ -1920,6 +2198,9 @@ impl Entity for NotificationMail { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct OutOfOfficeNotification { @@ -1941,6 +2222,9 @@ impl Entity for OutOfOfficeNotification { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct OutOfOfficeNotificationMessage { @@ -1959,6 +2243,9 @@ impl Entity for OutOfOfficeNotificationMessage { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct OutOfOfficeNotificationRecipientList { @@ -1974,6 +2261,9 @@ impl Entity for OutOfOfficeNotificationRecipientList { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PhishingMarkerWebsocketData { @@ -1990,6 +2280,9 @@ impl Entity for PhishingMarkerWebsocketData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct PhotosRef { @@ -2005,6 +2298,9 @@ impl Entity for PhotosRef { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ReceiveInfoServiceData { @@ -2020,6 +2316,9 @@ impl Entity for ReceiveInfoServiceData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Recipients { @@ -2037,6 +2336,9 @@ impl Entity for Recipients { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct RemoteImapSyncInfo { @@ -2056,6 +2358,9 @@ impl Entity for RemoteImapSyncInfo { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ReportMailPostData { @@ -2074,6 +2379,9 @@ impl Entity for ReportMailPostData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ReportedMailFieldMarker { @@ -2090,6 +2398,9 @@ impl Entity for ReportedMailFieldMarker { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SecureExternalRecipientKeyData { @@ -2118,6 +2429,9 @@ impl Entity for SecureExternalRecipientKeyData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SendDraftData { @@ -2147,6 +2461,9 @@ impl Entity for SendDraftData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SendDraftReturn { @@ -2165,6 +2482,9 @@ impl Entity for SendDraftReturn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SharedGroupData { @@ -2194,6 +2514,9 @@ impl Entity for SharedGroupData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SimpleMoveMailPostIn { @@ -2210,6 +2533,9 @@ impl Entity for SimpleMoveMailPostIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SpamResults { @@ -2225,6 +2551,9 @@ impl Entity for SpamResults { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct Subfiles { @@ -2240,6 +2569,9 @@ impl Entity for Subfiles { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct SymEncInternalRecipientKeyData { @@ -2259,6 +2591,9 @@ impl Entity for SymEncInternalRecipientKeyData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct TemplateGroupRoot { @@ -2283,6 +2618,9 @@ impl Entity for TemplateGroupRoot { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct TranslationGetIn { @@ -2298,6 +2636,9 @@ impl Entity for TranslationGetIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct TranslationGetOut { @@ -2314,6 +2655,9 @@ impl Entity for TranslationGetOut { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct TutanotaProperties { @@ -2350,6 +2694,9 @@ impl Entity for TutanotaProperties { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UnreadMailStatePostIn { @@ -2366,6 +2713,9 @@ impl Entity for UnreadMailStatePostIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UpdateMailFolderData { @@ -2382,6 +2732,9 @@ impl Entity for UpdateMailFolderData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UserAccountCreateData { @@ -2399,6 +2752,9 @@ impl Entity for UserAccountCreateData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UserAccountUserData { @@ -2454,6 +2810,9 @@ impl Entity for UserAccountUserData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UserAreaGroupData { @@ -2482,6 +2841,9 @@ impl Entity for UserAreaGroupData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UserAreaGroupDeleteData { @@ -2497,6 +2859,9 @@ impl Entity for UserAreaGroupDeleteData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UserAreaGroupPostData { @@ -2512,6 +2877,9 @@ impl Entity for UserAreaGroupPostData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UserSettingsGroupRoot { @@ -2537,3 +2905,6 @@ impl Entity for UserSettingsGroupRoot { } } } + + + diff --git a/tuta-sdk/rust/sdk/src/entities/generated/usage.rs b/tuta-sdk/rust/sdk/src/entities/generated/usage.rs index 8e774c42ebd..f1266c85985 100644 --- a/tuta-sdk/rust/sdk/src/entities/generated/usage.rs +++ b/tuta-sdk/rust/sdk/src/entities/generated/usage.rs @@ -1,6 +1,6 @@ #![allow(non_snake_case, unused_imports)] -use super::super::*; use crate::*; +use super::super::*; use serde::{Deserialize, Serialize}; #[derive(uniffi::Record, Clone, Serialize, Deserialize)] @@ -22,6 +22,9 @@ impl Entity for UsageTestAssignment { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UsageTestAssignmentIn { @@ -37,6 +40,9 @@ impl Entity for UsageTestAssignmentIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UsageTestAssignmentOut { @@ -53,6 +59,9 @@ impl Entity for UsageTestAssignmentOut { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UsageTestMetricConfig { @@ -71,6 +80,9 @@ impl Entity for UsageTestMetricConfig { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UsageTestMetricConfigValue { @@ -87,6 +99,9 @@ impl Entity for UsageTestMetricConfigValue { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UsageTestMetricData { @@ -103,6 +118,9 @@ impl Entity for UsageTestMetricData { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UsageTestParticipationIn { @@ -121,6 +139,9 @@ impl Entity for UsageTestParticipationIn { } } + + + #[derive(uniffi::Record, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Debug))] pub struct UsageTestStage { @@ -138,3 +159,6 @@ impl Entity for UsageTestStage { } } } + + + diff --git a/tuta-sdk/rust/sdk/src/services/generated/accounting.rs b/tuta-sdk/rust/sdk/src/services/generated/accounting.rs index 4fbbbf8912d..8fe05e9f727 100644 --- a/tuta-sdk/rust/sdk/src/services/generated/accounting.rs +++ b/tuta-sdk/rust/sdk/src/services/generated/accounting.rs @@ -1,18 +1,11 @@ #![allow(unused_imports, dead_code, unused_variables)] -use crate::entities::generated::accounting::CustomerAccountReturn; +use crate::ApiCallError; use crate::entities::Entity; +use crate::services::{PostService, GetService, PutService, DeleteService, Service, Executor, ExtraServiceParams}; use crate::rest_client::HttpMethod; use crate::services::hidden::Nothing; -use crate::services::{ - DeleteService, Executor, ExtraServiceParams, GetService, PostService, PutService, Service, -}; -use crate::ApiCallError; +use crate::entities::generated::accounting::CustomerAccountReturn; pub struct CustomerAccountService; -crate::service_impl!( - declare, - CustomerAccountService, - "accounting/customeraccountservice", - 7 -); +crate::service_impl!(declare, CustomerAccountService, "accounting/customeraccountservice", 7); crate::service_impl!(GET, CustomerAccountService, (), CustomerAccountReturn); diff --git a/tuta-sdk/rust/sdk/src/services/generated/base.rs b/tuta-sdk/rust/sdk/src/services/generated/base.rs index 95816926713..213f090e5f5 100644 --- a/tuta-sdk/rust/sdk/src/services/generated/base.rs +++ b/tuta-sdk/rust/sdk/src/services/generated/base.rs @@ -1,8 +1,6 @@ #![allow(unused_imports, dead_code, unused_variables)] +use crate::ApiCallError; use crate::entities::Entity; +use crate::services::{PostService, GetService, PutService, DeleteService, Service, Executor, ExtraServiceParams}; use crate::rest_client::HttpMethod; -use crate::services::hidden::Nothing; -use crate::services::{ - DeleteService, Executor, ExtraServiceParams, GetService, PostService, PutService, Service, -}; -use crate::ApiCallError; +use crate::services::hidden::Nothing; \ No newline at end of file diff --git a/tuta-sdk/rust/sdk/src/services/generated/gossip.rs b/tuta-sdk/rust/sdk/src/services/generated/gossip.rs index 95816926713..213f090e5f5 100644 --- a/tuta-sdk/rust/sdk/src/services/generated/gossip.rs +++ b/tuta-sdk/rust/sdk/src/services/generated/gossip.rs @@ -1,8 +1,6 @@ #![allow(unused_imports, dead_code, unused_variables)] +use crate::ApiCallError; use crate::entities::Entity; +use crate::services::{PostService, GetService, PutService, DeleteService, Service, Executor, ExtraServiceParams}; use crate::rest_client::HttpMethod; -use crate::services::hidden::Nothing; -use crate::services::{ - DeleteService, Executor, ExtraServiceParams, GetService, PostService, PutService, Service, -}; -use crate::ApiCallError; +use crate::services::hidden::Nothing; \ No newline at end of file diff --git a/tuta-sdk/rust/sdk/src/services/generated/monitor.rs b/tuta-sdk/rust/sdk/src/services/generated/monitor.rs index dfae282e2ea..1459f3bb6dc 100644 --- a/tuta-sdk/rust/sdk/src/services/generated/monitor.rs +++ b/tuta-sdk/rust/sdk/src/services/generated/monitor.rs @@ -1,27 +1,21 @@ #![allow(unused_imports, dead_code, unused_variables)] -use crate::entities::generated::monitor::ReadCounterData; -use crate::entities::generated::monitor::ReadCounterReturn; -use crate::entities::generated::monitor::ReportErrorIn; -use crate::entities::generated::monitor::WriteCounterData; +use crate::ApiCallError; use crate::entities::Entity; +use crate::services::{PostService, GetService, PutService, DeleteService, Service, Executor, ExtraServiceParams}; use crate::rest_client::HttpMethod; use crate::services::hidden::Nothing; -use crate::services::{ - DeleteService, Executor, ExtraServiceParams, GetService, PostService, PutService, Service, -}; -use crate::ApiCallError; +use crate::entities::generated::monitor::WriteCounterData; +use crate::entities::generated::monitor::ReadCounterData; +use crate::entities::generated::monitor::ReadCounterReturn; +use crate::entities::generated::monitor::ReportErrorIn; pub struct CounterService; crate::service_impl!(declare, CounterService, "monitor/counterservice", 29); crate::service_impl!(POST, CounterService, WriteCounterData, ()); crate::service_impl!(GET, CounterService, ReadCounterData, ReadCounterReturn); + pub struct ReportErrorService; -crate::service_impl!( - declare, - ReportErrorService, - "monitor/reporterrorservice", - 29 -); +crate::service_impl!(declare, ReportErrorService, "monitor/reporterrorservice", 29); crate::service_impl!(POST, ReportErrorService, ReportErrorIn, ()); diff --git a/tuta-sdk/rust/sdk/src/services/generated/storage.rs b/tuta-sdk/rust/sdk/src/services/generated/storage.rs index f6d77b5f196..5c8a762bafc 100644 --- a/tuta-sdk/rust/sdk/src/services/generated/storage.rs +++ b/tuta-sdk/rust/sdk/src/services/generated/storage.rs @@ -1,43 +1,28 @@ #![allow(unused_imports, dead_code, unused_variables)] -use crate::entities::generated::storage::BlobAccessTokenPostIn; -use crate::entities::generated::storage::BlobAccessTokenPostOut; -use crate::entities::generated::storage::BlobGetIn; -use crate::entities::generated::storage::BlobPostOut; -use crate::entities::generated::storage::BlobReferenceDeleteIn; -use crate::entities::generated::storage::BlobReferencePutIn; +use crate::ApiCallError; use crate::entities::Entity; +use crate::services::{PostService, GetService, PutService, DeleteService, Service, Executor, ExtraServiceParams}; use crate::rest_client::HttpMethod; use crate::services::hidden::Nothing; -use crate::services::{ - DeleteService, Executor, ExtraServiceParams, GetService, PostService, PutService, Service, -}; -use crate::ApiCallError; +use crate::entities::generated::storage::BlobAccessTokenPostIn; +use crate::entities::generated::storage::BlobAccessTokenPostOut; +use crate::entities::generated::storage::BlobReferencePutIn; +use crate::entities::generated::storage::BlobReferenceDeleteIn; +use crate::entities::generated::storage::BlobPostOut; +use crate::entities::generated::storage::BlobGetIn; pub struct BlobAccessTokenService; -crate::service_impl!( - declare, - BlobAccessTokenService, - "storage/blobaccesstokenservice", - 9 -); -crate::service_impl!( - POST, - BlobAccessTokenService, - BlobAccessTokenPostIn, - BlobAccessTokenPostOut -); +crate::service_impl!(declare, BlobAccessTokenService, "storage/blobaccesstokenservice", 9); +crate::service_impl!(POST, BlobAccessTokenService, BlobAccessTokenPostIn, BlobAccessTokenPostOut); + pub struct BlobReferenceService; -crate::service_impl!( - declare, - BlobReferenceService, - "storage/blobreferenceservice", - 9 -); +crate::service_impl!(declare, BlobReferenceService, "storage/blobreferenceservice", 9); crate::service_impl!(PUT, BlobReferenceService, BlobReferencePutIn, ()); crate::service_impl!(DELETE, BlobReferenceService, BlobReferenceDeleteIn, ()); + pub struct BlobService; crate::service_impl!(declare, BlobService, "storage/blobservice", 9); diff --git a/tuta-sdk/rust/sdk/src/services/generated/sys.rs b/tuta-sdk/rust/sdk/src/services/generated/sys.rs index a8723617a75..ff1cf016efe 100644 --- a/tuta-sdk/rust/sdk/src/services/generated/sys.rs +++ b/tuta-sdk/rust/sdk/src/services/generated/sys.rs @@ -1,81 +1,86 @@ #![allow(unused_imports, dead_code, unused_variables)] +use crate::ApiCallError; +use crate::entities::Entity; +use crate::services::{PostService, GetService, PutService, DeleteService, Service, Executor, ExtraServiceParams}; +use crate::rest_client::HttpMethod; +use crate::services::hidden::Nothing; use crate::entities::generated::sys::AdminGroupKeyRotationPostIn; use crate::entities::generated::sys::AffiliatePartnerKpiServiceGetOut; use crate::entities::generated::sys::AlarmServicePost; -use crate::entities::generated::sys::AutoLoginDataDelete; -use crate::entities::generated::sys::AutoLoginDataGet; use crate::entities::generated::sys::AutoLoginDataReturn; use crate::entities::generated::sys::AutoLoginPostReturn; +use crate::entities::generated::sys::AutoLoginDataGet; +use crate::entities::generated::sys::AutoLoginDataDelete; use crate::entities::generated::sys::BrandingDomainData; -use crate::entities::generated::sys::BrandingDomainDeleteData; use crate::entities::generated::sys::BrandingDomainGetReturn; +use crate::entities::generated::sys::BrandingDomainDeleteData; use crate::entities::generated::sys::ChangeKdfPostIn; use crate::entities::generated::sys::ChangePasswordPostIn; use crate::entities::generated::sys::CloseSessionServicePost; use crate::entities::generated::sys::CreateCustomerServerPropertiesData; use crate::entities::generated::sys::CreateCustomerServerPropertiesReturn; -use crate::entities::generated::sys::CreateSessionData; -use crate::entities::generated::sys::CreateSessionReturn; use crate::entities::generated::sys::CustomDomainCheckGetIn; use crate::entities::generated::sys::CustomDomainCheckGetOut; use crate::entities::generated::sys::CustomDomainData; use crate::entities::generated::sys::CustomDomainReturn; use crate::entities::generated::sys::CustomerAccountTerminationPostIn; use crate::entities::generated::sys::CustomerAccountTerminationPostOut; -use crate::entities::generated::sys::DebitServicePutData; +use crate::entities::generated::sys::PublicKeyGetOut; use crate::entities::generated::sys::DeleteCustomerData; +use crate::entities::generated::sys::DebitServicePutData; use crate::entities::generated::sys::DomainMailAddressAvailabilityData; use crate::entities::generated::sys::DomainMailAddressAvailabilityReturn; use crate::entities::generated::sys::ExternalPropertiesReturn; +use crate::entities::generated::sys::GiftCardRedeemData; +use crate::entities::generated::sys::GiftCardRedeemGetReturn; use crate::entities::generated::sys::GiftCardCreateData; use crate::entities::generated::sys::GiftCardCreateReturn; -use crate::entities::generated::sys::GiftCardDeleteData; use crate::entities::generated::sys::GiftCardGetReturn; -use crate::entities::generated::sys::GiftCardRedeemData; -use crate::entities::generated::sys::GiftCardRedeemGetReturn; +use crate::entities::generated::sys::GiftCardDeleteData; use crate::entities::generated::sys::GroupKeyRotationInfoGetOut; use crate::entities::generated::sys::GroupKeyRotationPostIn; use crate::entities::generated::sys::InvoiceDataGetIn; use crate::entities::generated::sys::InvoiceDataGetOut; use crate::entities::generated::sys::LocalAdminRemovalPostIn; use crate::entities::generated::sys::LocationServiceGetReturn; -use crate::entities::generated::sys::MailAddressAliasGetIn; use crate::entities::generated::sys::MailAddressAliasServiceData; -use crate::entities::generated::sys::MailAddressAliasServiceDataDelete; +use crate::entities::generated::sys::MailAddressAliasGetIn; use crate::entities::generated::sys::MailAddressAliasServiceReturn; +use crate::entities::generated::sys::MailAddressAliasServiceDataDelete; use crate::entities::generated::sys::MembershipAddData; use crate::entities::generated::sys::MembershipPutIn; use crate::entities::generated::sys::MembershipRemoveData; use crate::entities::generated::sys::MultipleMailAddressAvailabilityData; use crate::entities::generated::sys::MultipleMailAddressAvailabilityReturn; +use crate::entities::generated::sys::PaymentDataServicePostData; use crate::entities::generated::sys::PaymentDataServiceGetData; use crate::entities::generated::sys::PaymentDataServiceGetReturn; -use crate::entities::generated::sys::PaymentDataServicePostData; use crate::entities::generated::sys::PaymentDataServicePutData; use crate::entities::generated::sys::PaymentDataServicePutReturn; use crate::entities::generated::sys::PlanServiceGetOut; use crate::entities::generated::sys::PriceServiceData; use crate::entities::generated::sys::PriceServiceReturn; use crate::entities::generated::sys::PublicKeyGetIn; -use crate::entities::generated::sys::PublicKeyGetOut; use crate::entities::generated::sys::PublicKeyPutIn; -use crate::entities::generated::sys::ReferralCodeGetIn; use crate::entities::generated::sys::ReferralCodePostIn; use crate::entities::generated::sys::ReferralCodePostOut; +use crate::entities::generated::sys::ReferralCodeGetIn; use crate::entities::generated::sys::RegistrationCaptchaServiceData; use crate::entities::generated::sys::RegistrationCaptchaServiceGetData; use crate::entities::generated::sys::RegistrationCaptchaServiceReturn; -use crate::entities::generated::sys::RegistrationReturn; use crate::entities::generated::sys::RegistrationServiceData; +use crate::entities::generated::sys::RegistrationReturn; use crate::entities::generated::sys::ResetFactorsDeleteData; use crate::entities::generated::sys::ResetPasswordPostIn; use crate::entities::generated::sys::SaltData; use crate::entities::generated::sys::SaltReturn; use crate::entities::generated::sys::SecondFactorAuthAllowedReturn; use crate::entities::generated::sys::SecondFactorAuthData; -use crate::entities::generated::sys::SecondFactorAuthDeleteData; use crate::entities::generated::sys::SecondFactorAuthGetData; use crate::entities::generated::sys::SecondFactorAuthGetReturn; +use crate::entities::generated::sys::SecondFactorAuthDeleteData; +use crate::entities::generated::sys::CreateSessionData; +use crate::entities::generated::sys::CreateSessionReturn; use crate::entities::generated::sys::SignOrderProcessingAgreementData; use crate::entities::generated::sys::SwitchAccountTypePostIn; use crate::entities::generated::sys::SystemKeysReturn; @@ -84,298 +89,171 @@ use crate::entities::generated::sys::UpdatePermissionKeyData; use crate::entities::generated::sys::UpdateSessionKeysPostIn; use crate::entities::generated::sys::UpgradePriceServiceData; use crate::entities::generated::sys::UpgradePriceServiceReturn; -use crate::entities::generated::sys::UserDataDelete; use crate::entities::generated::sys::UserGroupKeyRotationPostIn; +use crate::entities::generated::sys::UserDataDelete; use crate::entities::generated::sys::VersionData; use crate::entities::generated::sys::VersionReturn; -use crate::entities::Entity; -use crate::rest_client::HttpMethod; -use crate::services::hidden::Nothing; -use crate::services::{ - DeleteService, Executor, ExtraServiceParams, GetService, PostService, PutService, Service, -}; -use crate::ApiCallError; pub struct AdminGroupKeyRotationService; -crate::service_impl!( - declare, - AdminGroupKeyRotationService, - "sys/admingroupkeyrotationservice", - 113 -); -crate::service_impl!( - POST, - AdminGroupKeyRotationService, - AdminGroupKeyRotationPostIn, - () -); +crate::service_impl!(declare, AdminGroupKeyRotationService, "sys/admingroupkeyrotationservice", 113); +crate::service_impl!(POST, AdminGroupKeyRotationService, AdminGroupKeyRotationPostIn, ()); + pub struct AffiliatePartnerKpiService; -crate::service_impl!( - declare, - AffiliatePartnerKpiService, - "sys/affiliatepartnerkpiservice", - 113 -); -crate::service_impl!( - GET, - AffiliatePartnerKpiService, - (), - AffiliatePartnerKpiServiceGetOut -); +crate::service_impl!(declare, AffiliatePartnerKpiService, "sys/affiliatepartnerkpiservice", 113); +crate::service_impl!(GET, AffiliatePartnerKpiService, (), AffiliatePartnerKpiServiceGetOut); + pub struct AlarmService; crate::service_impl!(declare, AlarmService, "sys/alarmservice", 113); crate::service_impl!(POST, AlarmService, AlarmServicePost, ()); + pub struct AutoLoginService; crate::service_impl!(declare, AutoLoginService, "sys/autologinservice", 113); -crate::service_impl!( - POST, - AutoLoginService, - AutoLoginDataReturn, - AutoLoginPostReturn -); +crate::service_impl!(POST, AutoLoginService, AutoLoginDataReturn, AutoLoginPostReturn); crate::service_impl!(GET, AutoLoginService, AutoLoginDataGet, AutoLoginDataReturn); crate::service_impl!(DELETE, AutoLoginService, AutoLoginDataDelete, ()); + pub struct BrandingDomainService; -crate::service_impl!( - declare, - BrandingDomainService, - "sys/brandingdomainservice", - 113 -); +crate::service_impl!(declare, BrandingDomainService, "sys/brandingdomainservice", 113); crate::service_impl!(POST, BrandingDomainService, BrandingDomainData, ()); crate::service_impl!(GET, BrandingDomainService, (), BrandingDomainGetReturn); crate::service_impl!(PUT, BrandingDomainService, BrandingDomainData, ()); crate::service_impl!(DELETE, BrandingDomainService, BrandingDomainDeleteData, ()); + pub struct ChangeKdfService; crate::service_impl!(declare, ChangeKdfService, "sys/changekdfservice", 113); crate::service_impl!(POST, ChangeKdfService, ChangeKdfPostIn, ()); + pub struct ChangePasswordService; -crate::service_impl!( - declare, - ChangePasswordService, - "sys/changepasswordservice", - 113 -); +crate::service_impl!(declare, ChangePasswordService, "sys/changepasswordservice", 113); crate::service_impl!(POST, ChangePasswordService, ChangePasswordPostIn, ()); + pub struct CloseSessionService; crate::service_impl!(declare, CloseSessionService, "sys/closesessionservice", 113); crate::service_impl!(POST, CloseSessionService, CloseSessionServicePost, ()); + pub struct CreateCustomerServerProperties; -crate::service_impl!( - declare, - CreateCustomerServerProperties, - "sys/createcustomerserverproperties", - 113 -); -crate::service_impl!( - POST, - CreateCustomerServerProperties, - CreateCustomerServerPropertiesData, - CreateCustomerServerPropertiesReturn -); +crate::service_impl!(declare, CreateCustomerServerProperties, "sys/createcustomerserverproperties", 113); +crate::service_impl!(POST, CreateCustomerServerProperties, CreateCustomerServerPropertiesData, CreateCustomerServerPropertiesReturn); + pub struct CustomDomainCheckService; -crate::service_impl!( - declare, - CustomDomainCheckService, - "sys/customdomaincheckservice", - 113 -); -crate::service_impl!( - GET, - CustomDomainCheckService, - CustomDomainCheckGetIn, - CustomDomainCheckGetOut -); +crate::service_impl!(declare, CustomDomainCheckService, "sys/customdomaincheckservice", 113); +crate::service_impl!(GET, CustomDomainCheckService, CustomDomainCheckGetIn, CustomDomainCheckGetOut); + pub struct CustomDomainService; crate::service_impl!(declare, CustomDomainService, "sys/customdomainservice", 113); -crate::service_impl!( - POST, - CustomDomainService, - CustomDomainData, - CustomDomainReturn -); +crate::service_impl!(POST, CustomDomainService, CustomDomainData, CustomDomainReturn); crate::service_impl!(PUT, CustomDomainService, CustomDomainData, ()); crate::service_impl!(DELETE, CustomDomainService, CustomDomainData, ()); + pub struct CustomerAccountTerminationService; -crate::service_impl!( - declare, - CustomerAccountTerminationService, - "sys/customeraccountterminationservice", - 113 -); -crate::service_impl!( - POST, - CustomerAccountTerminationService, - CustomerAccountTerminationPostIn, - CustomerAccountTerminationPostOut -); +crate::service_impl!(declare, CustomerAccountTerminationService, "sys/customeraccountterminationservice", 113); +crate::service_impl!(POST, CustomerAccountTerminationService, CustomerAccountTerminationPostIn, CustomerAccountTerminationPostOut); + pub struct CustomerPublicKeyService; -crate::service_impl!( - declare, - CustomerPublicKeyService, - "sys/customerpublickeyservice", - 113 -); +crate::service_impl!(declare, CustomerPublicKeyService, "sys/customerpublickeyservice", 113); crate::service_impl!(GET, CustomerPublicKeyService, (), PublicKeyGetOut); + pub struct CustomerService; crate::service_impl!(declare, CustomerService, "sys/customerservice", 113); crate::service_impl!(DELETE, CustomerService, DeleteCustomerData, ()); + pub struct DebitService; crate::service_impl!(declare, DebitService, "sys/debitservice", 113); crate::service_impl!(PUT, DebitService, DebitServicePutData, ()); + pub struct DomainMailAddressAvailabilityService; -crate::service_impl!( - declare, - DomainMailAddressAvailabilityService, - "sys/domainmailaddressavailabilityservice", - 113 -); -crate::service_impl!( - GET, - DomainMailAddressAvailabilityService, - DomainMailAddressAvailabilityData, - DomainMailAddressAvailabilityReturn -); +crate::service_impl!(declare, DomainMailAddressAvailabilityService, "sys/domainmailaddressavailabilityservice", 113); +crate::service_impl!(GET, DomainMailAddressAvailabilityService, DomainMailAddressAvailabilityData, DomainMailAddressAvailabilityReturn); + pub struct ExternalPropertiesService; -crate::service_impl!( - declare, - ExternalPropertiesService, - "sys/externalpropertiesservice", - 113 -); +crate::service_impl!(declare, ExternalPropertiesService, "sys/externalpropertiesservice", 113); crate::service_impl!(GET, ExternalPropertiesService, (), ExternalPropertiesReturn); + pub struct GiftCardRedeemService; -crate::service_impl!( - declare, - GiftCardRedeemService, - "sys/giftcardredeemservice", - 113 -); +crate::service_impl!(declare, GiftCardRedeemService, "sys/giftcardredeemservice", 113); crate::service_impl!(POST, GiftCardRedeemService, GiftCardRedeemData, ()); -crate::service_impl!( - GET, - GiftCardRedeemService, - GiftCardRedeemData, - GiftCardRedeemGetReturn -); +crate::service_impl!(GET, GiftCardRedeemService, GiftCardRedeemData, GiftCardRedeemGetReturn); + pub struct GiftCardService; crate::service_impl!(declare, GiftCardService, "sys/giftcardservice", 113); -crate::service_impl!( - POST, - GiftCardService, - GiftCardCreateData, - GiftCardCreateReturn -); +crate::service_impl!(POST, GiftCardService, GiftCardCreateData, GiftCardCreateReturn); crate::service_impl!(GET, GiftCardService, (), GiftCardGetReturn); crate::service_impl!(DELETE, GiftCardService, GiftCardDeleteData, ()); + pub struct GroupKeyRotationInfoService; -crate::service_impl!( - declare, - GroupKeyRotationInfoService, - "sys/groupkeyrotationinfoservice", - 113 -); -crate::service_impl!( - GET, - GroupKeyRotationInfoService, - (), - GroupKeyRotationInfoGetOut -); +crate::service_impl!(declare, GroupKeyRotationInfoService, "sys/groupkeyrotationinfoservice", 113); +crate::service_impl!(GET, GroupKeyRotationInfoService, (), GroupKeyRotationInfoGetOut); + pub struct GroupKeyRotationService; -crate::service_impl!( - declare, - GroupKeyRotationService, - "sys/groupkeyrotationservice", - 113 -); +crate::service_impl!(declare, GroupKeyRotationService, "sys/groupkeyrotationservice", 113); crate::service_impl!(POST, GroupKeyRotationService, GroupKeyRotationPostIn, ()); + pub struct InvoiceDataService; crate::service_impl!(declare, InvoiceDataService, "sys/invoicedataservice", 113); crate::service_impl!(GET, InvoiceDataService, InvoiceDataGetIn, InvoiceDataGetOut); + pub struct LocalAdminRemovalService; -crate::service_impl!( - declare, - LocalAdminRemovalService, - "sys/localadminremovalservice", - 113 -); +crate::service_impl!(declare, LocalAdminRemovalService, "sys/localadminremovalservice", 113); crate::service_impl!(POST, LocalAdminRemovalService, LocalAdminRemovalPostIn, ()); + pub struct LocationService; crate::service_impl!(declare, LocationService, "sys/locationservice", 113); crate::service_impl!(GET, LocationService, (), LocationServiceGetReturn); + pub struct MailAddressAliasService; -crate::service_impl!( - declare, - MailAddressAliasService, - "sys/mailaddressaliasservice", - 113 -); -crate::service_impl!( - POST, - MailAddressAliasService, - MailAddressAliasServiceData, - () -); -crate::service_impl!( - GET, - MailAddressAliasService, - MailAddressAliasGetIn, - MailAddressAliasServiceReturn -); -crate::service_impl!( - DELETE, - MailAddressAliasService, - MailAddressAliasServiceDataDelete, - () -); +crate::service_impl!(declare, MailAddressAliasService, "sys/mailaddressaliasservice", 113); +crate::service_impl!(POST, MailAddressAliasService, MailAddressAliasServiceData, ()); +crate::service_impl!(GET, MailAddressAliasService, MailAddressAliasGetIn, MailAddressAliasServiceReturn); +crate::service_impl!(DELETE, MailAddressAliasService, MailAddressAliasServiceDataDelete, ()); + pub struct MembershipService; @@ -384,259 +262,153 @@ crate::service_impl!(POST, MembershipService, MembershipAddData, ()); crate::service_impl!(PUT, MembershipService, MembershipPutIn, ()); crate::service_impl!(DELETE, MembershipService, MembershipRemoveData, ()); + pub struct MultipleMailAddressAvailabilityService; -crate::service_impl!( - declare, - MultipleMailAddressAvailabilityService, - "sys/multiplemailaddressavailabilityservice", - 113 -); -crate::service_impl!( - GET, - MultipleMailAddressAvailabilityService, - MultipleMailAddressAvailabilityData, - MultipleMailAddressAvailabilityReturn -); +crate::service_impl!(declare, MultipleMailAddressAvailabilityService, "sys/multiplemailaddressavailabilityservice", 113); +crate::service_impl!(GET, MultipleMailAddressAvailabilityService, MultipleMailAddressAvailabilityData, MultipleMailAddressAvailabilityReturn); + pub struct PaymentDataService; crate::service_impl!(declare, PaymentDataService, "sys/paymentdataservice", 113); crate::service_impl!(POST, PaymentDataService, PaymentDataServicePostData, ()); -crate::service_impl!( - GET, - PaymentDataService, - PaymentDataServiceGetData, - PaymentDataServiceGetReturn -); -crate::service_impl!( - PUT, - PaymentDataService, - PaymentDataServicePutData, - PaymentDataServicePutReturn -); +crate::service_impl!(GET, PaymentDataService, PaymentDataServiceGetData, PaymentDataServiceGetReturn); +crate::service_impl!(PUT, PaymentDataService, PaymentDataServicePutData, PaymentDataServicePutReturn); + pub struct PlanService; crate::service_impl!(declare, PlanService, "sys/planservice", 113); crate::service_impl!(GET, PlanService, (), PlanServiceGetOut); + pub struct PriceService; crate::service_impl!(declare, PriceService, "sys/priceservice", 113); crate::service_impl!(GET, PriceService, PriceServiceData, PriceServiceReturn); + pub struct PublicKeyService; crate::service_impl!(declare, PublicKeyService, "sys/publickeyservice", 113); crate::service_impl!(GET, PublicKeyService, PublicKeyGetIn, PublicKeyGetOut); crate::service_impl!(PUT, PublicKeyService, PublicKeyPutIn, ()); + pub struct ReferralCodeService; crate::service_impl!(declare, ReferralCodeService, "sys/referralcodeservice", 113); -crate::service_impl!( - POST, - ReferralCodeService, - ReferralCodePostIn, - ReferralCodePostOut -); +crate::service_impl!(POST, ReferralCodeService, ReferralCodePostIn, ReferralCodePostOut); crate::service_impl!(GET, ReferralCodeService, ReferralCodeGetIn, ()); + pub struct RegistrationCaptchaService; -crate::service_impl!( - declare, - RegistrationCaptchaService, - "sys/registrationcaptchaservice", - 113 -); -crate::service_impl!( - POST, - RegistrationCaptchaService, - RegistrationCaptchaServiceData, - () -); -crate::service_impl!( - GET, - RegistrationCaptchaService, - RegistrationCaptchaServiceGetData, - RegistrationCaptchaServiceReturn -); +crate::service_impl!(declare, RegistrationCaptchaService, "sys/registrationcaptchaservice", 113); +crate::service_impl!(POST, RegistrationCaptchaService, RegistrationCaptchaServiceData, ()); +crate::service_impl!(GET, RegistrationCaptchaService, RegistrationCaptchaServiceGetData, RegistrationCaptchaServiceReturn); + pub struct RegistrationService; crate::service_impl!(declare, RegistrationService, "sys/registrationservice", 113); -crate::service_impl!( - POST, - RegistrationService, - RegistrationServiceData, - RegistrationReturn -); +crate::service_impl!(POST, RegistrationService, RegistrationServiceData, RegistrationReturn); crate::service_impl!(GET, RegistrationService, (), RegistrationServiceData); + pub struct ResetFactorsService; crate::service_impl!(declare, ResetFactorsService, "sys/resetfactorsservice", 113); crate::service_impl!(DELETE, ResetFactorsService, ResetFactorsDeleteData, ()); + pub struct ResetPasswordService; -crate::service_impl!( - declare, - ResetPasswordService, - "sys/resetpasswordservice", - 113 -); +crate::service_impl!(declare, ResetPasswordService, "sys/resetpasswordservice", 113); crate::service_impl!(POST, ResetPasswordService, ResetPasswordPostIn, ()); + pub struct SaltService; crate::service_impl!(declare, SaltService, "sys/saltservice", 113); crate::service_impl!(GET, SaltService, SaltData, SaltReturn); + pub struct SecondFactorAuthAllowedService; -crate::service_impl!( - declare, - SecondFactorAuthAllowedService, - "sys/secondfactorauthallowedservice", - 113 -); -crate::service_impl!( - GET, - SecondFactorAuthAllowedService, - (), - SecondFactorAuthAllowedReturn -); +crate::service_impl!(declare, SecondFactorAuthAllowedService, "sys/secondfactorauthallowedservice", 113); +crate::service_impl!(GET, SecondFactorAuthAllowedService, (), SecondFactorAuthAllowedReturn); + pub struct SecondFactorAuthService; -crate::service_impl!( - declare, - SecondFactorAuthService, - "sys/secondfactorauthservice", - 113 -); +crate::service_impl!(declare, SecondFactorAuthService, "sys/secondfactorauthservice", 113); crate::service_impl!(POST, SecondFactorAuthService, SecondFactorAuthData, ()); -crate::service_impl!( - GET, - SecondFactorAuthService, - SecondFactorAuthGetData, - SecondFactorAuthGetReturn -); -crate::service_impl!( - DELETE, - SecondFactorAuthService, - SecondFactorAuthDeleteData, - () -); +crate::service_impl!(GET, SecondFactorAuthService, SecondFactorAuthGetData, SecondFactorAuthGetReturn); +crate::service_impl!(DELETE, SecondFactorAuthService, SecondFactorAuthDeleteData, ()); + pub struct SessionService; crate::service_impl!(declare, SessionService, "sys/sessionservice", 113); crate::service_impl!(POST, SessionService, CreateSessionData, CreateSessionReturn); + pub struct SignOrderProcessingAgreementService; -crate::service_impl!( - declare, - SignOrderProcessingAgreementService, - "sys/signorderprocessingagreementservice", - 113 -); -crate::service_impl!( - POST, - SignOrderProcessingAgreementService, - SignOrderProcessingAgreementData, - () -); +crate::service_impl!(declare, SignOrderProcessingAgreementService, "sys/signorderprocessingagreementservice", 113); +crate::service_impl!(POST, SignOrderProcessingAgreementService, SignOrderProcessingAgreementData, ()); + pub struct SwitchAccountTypeService; -crate::service_impl!( - declare, - SwitchAccountTypeService, - "sys/switchaccounttypeservice", - 113 -); +crate::service_impl!(declare, SwitchAccountTypeService, "sys/switchaccounttypeservice", 113); crate::service_impl!(POST, SwitchAccountTypeService, SwitchAccountTypePostIn, ()); + pub struct SystemKeysService; crate::service_impl!(declare, SystemKeysService, "sys/systemkeysservice", 113); crate::service_impl!(GET, SystemKeysService, (), SystemKeysReturn); + pub struct TakeOverDeletedAddressService; -crate::service_impl!( - declare, - TakeOverDeletedAddressService, - "sys/takeoverdeletedaddressservice", - 113 -); -crate::service_impl!( - POST, - TakeOverDeletedAddressService, - TakeOverDeletedAddressData, - () -); +crate::service_impl!(declare, TakeOverDeletedAddressService, "sys/takeoverdeletedaddressservice", 113); +crate::service_impl!(POST, TakeOverDeletedAddressService, TakeOverDeletedAddressData, ()); + pub struct UpdatePermissionKeyService; -crate::service_impl!( - declare, - UpdatePermissionKeyService, - "sys/updatepermissionkeyservice", - 113 -); -crate::service_impl!( - POST, - UpdatePermissionKeyService, - UpdatePermissionKeyData, - () -); +crate::service_impl!(declare, UpdatePermissionKeyService, "sys/updatepermissionkeyservice", 113); +crate::service_impl!(POST, UpdatePermissionKeyService, UpdatePermissionKeyData, ()); + pub struct UpdateSessionKeysService; -crate::service_impl!( - declare, - UpdateSessionKeysService, - "sys/updatesessionkeysservice", - 113 -); +crate::service_impl!(declare, UpdateSessionKeysService, "sys/updatesessionkeysservice", 113); crate::service_impl!(POST, UpdateSessionKeysService, UpdateSessionKeysPostIn, ()); + pub struct UpgradePriceService; crate::service_impl!(declare, UpgradePriceService, "sys/upgradepriceservice", 113); -crate::service_impl!( - GET, - UpgradePriceService, - UpgradePriceServiceData, - UpgradePriceServiceReturn -); +crate::service_impl!(GET, UpgradePriceService, UpgradePriceServiceData, UpgradePriceServiceReturn); + pub struct UserGroupKeyRotationService; -crate::service_impl!( - declare, - UserGroupKeyRotationService, - "sys/usergroupkeyrotationservice", - 113 -); -crate::service_impl!( - POST, - UserGroupKeyRotationService, - UserGroupKeyRotationPostIn, - () -); +crate::service_impl!(declare, UserGroupKeyRotationService, "sys/usergroupkeyrotationservice", 113); +crate::service_impl!(POST, UserGroupKeyRotationService, UserGroupKeyRotationPostIn, ()); + pub struct UserService; crate::service_impl!(declare, UserService, "sys/userservice", 113); crate::service_impl!(DELETE, UserService, UserDataDelete, ()); + pub struct VersionService; crate::service_impl!(declare, VersionService, "sys/versionservice", 113); diff --git a/tuta-sdk/rust/sdk/src/services/generated/tutanota.rs b/tuta-sdk/rust/sdk/src/services/generated/tutanota.rs index 720a8bcd2d8..0ac293d5d74 100644 --- a/tuta-sdk/rust/sdk/src/services/generated/tutanota.rs +++ b/tuta-sdk/rust/sdk/src/services/generated/tutanota.rs @@ -1,13 +1,14 @@ #![allow(unused_imports, dead_code, unused_variables)] -use crate::entities::generated::tutanota::CalendarDeleteData; +use crate::ApiCallError; +use crate::entities::Entity; +use crate::services::{PostService, GetService, PutService, DeleteService, Service, Executor, ExtraServiceParams}; +use crate::rest_client::HttpMethod; +use crate::services::hidden::Nothing; +use crate::entities::generated::tutanota::UserAreaGroupPostData; use crate::entities::generated::tutanota::CreateGroupPostReturn; -use crate::entities::generated::tutanota::CreateMailFolderData; -use crate::entities::generated::tutanota::CreateMailFolderReturn; -use crate::entities::generated::tutanota::CreateMailGroupData; +use crate::entities::generated::tutanota::CalendarDeleteData; +use crate::entities::generated::tutanota::UserAreaGroupDeleteData; use crate::entities::generated::tutanota::CustomerAccountCreateData; -use crate::entities::generated::tutanota::DeleteGroupData; -use crate::entities::generated::tutanota::DeleteMailData; -use crate::entities::generated::tutanota::DeleteMailFolderData; use crate::entities::generated::tutanota::DraftCreateData; use crate::entities::generated::tutanota::DraftCreateReturn; use crate::entities::generated::tutanota::DraftUpdateData; @@ -15,13 +16,20 @@ use crate::entities::generated::tutanota::DraftUpdateReturn; use crate::entities::generated::tutanota::EncryptTutanotaPropertiesData; use crate::entities::generated::tutanota::EntropyData; use crate::entities::generated::tutanota::ExternalUserData; -use crate::entities::generated::tutanota::GroupInvitationDeleteData; use crate::entities::generated::tutanota::GroupInvitationPostData; use crate::entities::generated::tutanota::GroupInvitationPostReturn; use crate::entities::generated::tutanota::GroupInvitationPutData; +use crate::entities::generated::tutanota::GroupInvitationDeleteData; use crate::entities::generated::tutanota::ImportMailPostIn; use crate::entities::generated::tutanota::ImportMailPostOut; use crate::entities::generated::tutanota::ListUnsubscribeData; +use crate::entities::generated::tutanota::CreateMailFolderData; +use crate::entities::generated::tutanota::CreateMailFolderReturn; +use crate::entities::generated::tutanota::UpdateMailFolderData; +use crate::entities::generated::tutanota::DeleteMailFolderData; +use crate::entities::generated::tutanota::CreateMailGroupData; +use crate::entities::generated::tutanota::DeleteGroupData; +use crate::entities::generated::tutanota::DeleteMailData; use crate::entities::generated::tutanota::MoveMailData; use crate::entities::generated::tutanota::NewsIn; use crate::entities::generated::tutanota::NewsOut; @@ -33,233 +41,150 @@ use crate::entities::generated::tutanota::SimpleMoveMailPostIn; use crate::entities::generated::tutanota::TranslationGetIn; use crate::entities::generated::tutanota::TranslationGetOut; use crate::entities::generated::tutanota::UnreadMailStatePostIn; -use crate::entities::generated::tutanota::UpdateMailFolderData; use crate::entities::generated::tutanota::UserAccountCreateData; -use crate::entities::generated::tutanota::UserAreaGroupDeleteData; -use crate::entities::generated::tutanota::UserAreaGroupPostData; -use crate::entities::Entity; -use crate::rest_client::HttpMethod; -use crate::services::hidden::Nothing; -use crate::services::{ - DeleteService, Executor, ExtraServiceParams, GetService, PostService, PutService, Service, -}; -use crate::ApiCallError; pub struct CalendarService; crate::service_impl!(declare, CalendarService, "tutanota/calendarservice", 77); -crate::service_impl!( - POST, - CalendarService, - UserAreaGroupPostData, - CreateGroupPostReturn -); +crate::service_impl!(POST, CalendarService, UserAreaGroupPostData, CreateGroupPostReturn); crate::service_impl!(DELETE, CalendarService, CalendarDeleteData, ()); + pub struct ContactListGroupService; -crate::service_impl!( - declare, - ContactListGroupService, - "tutanota/contactlistgroupservice", - 77 -); -crate::service_impl!( - POST, - ContactListGroupService, - UserAreaGroupPostData, - CreateGroupPostReturn -); +crate::service_impl!(declare, ContactListGroupService, "tutanota/contactlistgroupservice", 77); +crate::service_impl!(POST, ContactListGroupService, UserAreaGroupPostData, CreateGroupPostReturn); crate::service_impl!(DELETE, ContactListGroupService, UserAreaGroupDeleteData, ()); + pub struct CustomerAccountService; -crate::service_impl!( - declare, - CustomerAccountService, - "tutanota/customeraccountservice", - 77 -); +crate::service_impl!(declare, CustomerAccountService, "tutanota/customeraccountservice", 77); crate::service_impl!(POST, CustomerAccountService, CustomerAccountCreateData, ()); + pub struct DraftService; crate::service_impl!(declare, DraftService, "tutanota/draftservice", 77); crate::service_impl!(POST, DraftService, DraftCreateData, DraftCreateReturn); crate::service_impl!(PUT, DraftService, DraftUpdateData, DraftUpdateReturn); + pub struct EncryptTutanotaPropertiesService; -crate::service_impl!( - declare, - EncryptTutanotaPropertiesService, - "tutanota/encrypttutanotapropertiesservice", - 77 -); -crate::service_impl!( - POST, - EncryptTutanotaPropertiesService, - EncryptTutanotaPropertiesData, - () -); +crate::service_impl!(declare, EncryptTutanotaPropertiesService, "tutanota/encrypttutanotapropertiesservice", 77); +crate::service_impl!(POST, EncryptTutanotaPropertiesService, EncryptTutanotaPropertiesData, ()); + pub struct EntropyService; crate::service_impl!(declare, EntropyService, "tutanota/entropyservice", 77); crate::service_impl!(PUT, EntropyService, EntropyData, ()); + pub struct ExternalUserService; -crate::service_impl!( - declare, - ExternalUserService, - "tutanota/externaluserservice", - 77 -); +crate::service_impl!(declare, ExternalUserService, "tutanota/externaluserservice", 77); crate::service_impl!(POST, ExternalUserService, ExternalUserData, ()); + pub struct GroupInvitationService; -crate::service_impl!( - declare, - GroupInvitationService, - "tutanota/groupinvitationservice", - 77 -); -crate::service_impl!( - POST, - GroupInvitationService, - GroupInvitationPostData, - GroupInvitationPostReturn -); +crate::service_impl!(declare, GroupInvitationService, "tutanota/groupinvitationservice", 77); +crate::service_impl!(POST, GroupInvitationService, GroupInvitationPostData, GroupInvitationPostReturn); crate::service_impl!(PUT, GroupInvitationService, GroupInvitationPutData, ()); -crate::service_impl!( - DELETE, - GroupInvitationService, - GroupInvitationDeleteData, - () -); +crate::service_impl!(DELETE, GroupInvitationService, GroupInvitationDeleteData, ()); + pub struct ImportMailService; crate::service_impl!(declare, ImportMailService, "tutanota/importmailservice", 77); crate::service_impl!(POST, ImportMailService, ImportMailPostIn, ImportMailPostOut); + pub struct ListUnsubscribeService; -crate::service_impl!( - declare, - ListUnsubscribeService, - "tutanota/listunsubscribeservice", - 77 -); +crate::service_impl!(declare, ListUnsubscribeService, "tutanota/listunsubscribeservice", 77); crate::service_impl!(POST, ListUnsubscribeService, ListUnsubscribeData, ()); + pub struct MailFolderService; crate::service_impl!(declare, MailFolderService, "tutanota/mailfolderservice", 77); -crate::service_impl!( - POST, - MailFolderService, - CreateMailFolderData, - CreateMailFolderReturn -); +crate::service_impl!(POST, MailFolderService, CreateMailFolderData, CreateMailFolderReturn); crate::service_impl!(PUT, MailFolderService, UpdateMailFolderData, ()); crate::service_impl!(DELETE, MailFolderService, DeleteMailFolderData, ()); + pub struct MailGroupService; crate::service_impl!(declare, MailGroupService, "tutanota/mailgroupservice", 77); crate::service_impl!(POST, MailGroupService, CreateMailGroupData, ()); crate::service_impl!(DELETE, MailGroupService, DeleteGroupData, ()); + pub struct MailService; crate::service_impl!(declare, MailService, "tutanota/mailservice", 77); crate::service_impl!(DELETE, MailService, DeleteMailData, ()); + pub struct MoveMailService; crate::service_impl!(declare, MoveMailService, "tutanota/movemailservice", 77); crate::service_impl!(POST, MoveMailService, MoveMailData, ()); + pub struct NewsService; crate::service_impl!(declare, NewsService, "tutanota/newsservice", 77); crate::service_impl!(POST, NewsService, NewsIn, ()); crate::service_impl!(GET, NewsService, (), NewsOut); + pub struct ReceiveInfoService; -crate::service_impl!( - declare, - ReceiveInfoService, - "tutanota/receiveinfoservice", - 77 -); +crate::service_impl!(declare, ReceiveInfoService, "tutanota/receiveinfoservice", 77); crate::service_impl!(POST, ReceiveInfoService, ReceiveInfoServiceData, ()); + pub struct ReportMailService; crate::service_impl!(declare, ReportMailService, "tutanota/reportmailservice", 77); crate::service_impl!(POST, ReportMailService, ReportMailPostData, ()); + pub struct SendDraftService; crate::service_impl!(declare, SendDraftService, "tutanota/senddraftservice", 77); crate::service_impl!(POST, SendDraftService, SendDraftData, SendDraftReturn); + pub struct SimpleMoveMailService; -crate::service_impl!( - declare, - SimpleMoveMailService, - "tutanota/simplemovemailservice", - 77 -); +crate::service_impl!(declare, SimpleMoveMailService, "tutanota/simplemovemailservice", 77); crate::service_impl!(POST, SimpleMoveMailService, SimpleMoveMailPostIn, ()); + pub struct TemplateGroupService; -crate::service_impl!( - declare, - TemplateGroupService, - "tutanota/templategroupservice", - 77 -); -crate::service_impl!( - POST, - TemplateGroupService, - UserAreaGroupPostData, - CreateGroupPostReturn -); +crate::service_impl!(declare, TemplateGroupService, "tutanota/templategroupservice", 77); +crate::service_impl!(POST, TemplateGroupService, UserAreaGroupPostData, CreateGroupPostReturn); crate::service_impl!(DELETE, TemplateGroupService, UserAreaGroupDeleteData, ()); + pub struct TranslationService; -crate::service_impl!( - declare, - TranslationService, - "tutanota/translationservice", - 77 -); +crate::service_impl!(declare, TranslationService, "tutanota/translationservice", 77); crate::service_impl!(GET, TranslationService, TranslationGetIn, TranslationGetOut); + pub struct UnreadMailStateService; -crate::service_impl!( - declare, - UnreadMailStateService, - "tutanota/unreadmailstateservice", - 77 -); +crate::service_impl!(declare, UnreadMailStateService, "tutanota/unreadmailstateservice", 77); crate::service_impl!(POST, UnreadMailStateService, UnreadMailStatePostIn, ()); + pub struct UserAccountService; -crate::service_impl!( - declare, - UserAccountService, - "tutanota/useraccountservice", - 77 -); +crate::service_impl!(declare, UserAccountService, "tutanota/useraccountservice", 77); crate::service_impl!(POST, UserAccountService, UserAccountCreateData, ()); diff --git a/tuta-sdk/rust/sdk/src/services/generated/usage.rs b/tuta-sdk/rust/sdk/src/services/generated/usage.rs index fad7315e767..2abb0f2c94d 100644 --- a/tuta-sdk/rust/sdk/src/services/generated/usage.rs +++ b/tuta-sdk/rust/sdk/src/services/generated/usage.rs @@ -1,46 +1,20 @@ #![allow(unused_imports, dead_code, unused_variables)] -use crate::entities::generated::usage::UsageTestAssignmentIn; -use crate::entities::generated::usage::UsageTestAssignmentOut; -use crate::entities::generated::usage::UsageTestParticipationIn; +use crate::ApiCallError; use crate::entities::Entity; +use crate::services::{PostService, GetService, PutService, DeleteService, Service, Executor, ExtraServiceParams}; use crate::rest_client::HttpMethod; use crate::services::hidden::Nothing; -use crate::services::{ - DeleteService, Executor, ExtraServiceParams, GetService, PostService, PutService, Service, -}; -use crate::ApiCallError; +use crate::entities::generated::usage::UsageTestAssignmentIn; +use crate::entities::generated::usage::UsageTestAssignmentOut; +use crate::entities::generated::usage::UsageTestParticipationIn; pub struct UsageTestAssignmentService; -crate::service_impl!( - declare, - UsageTestAssignmentService, - "usage/usagetestassignmentservice", - 2 -); -crate::service_impl!( - POST, - UsageTestAssignmentService, - UsageTestAssignmentIn, - UsageTestAssignmentOut -); -crate::service_impl!( - PUT, - UsageTestAssignmentService, - UsageTestAssignmentIn, - UsageTestAssignmentOut -); +crate::service_impl!(declare, UsageTestAssignmentService, "usage/usagetestassignmentservice", 2); +crate::service_impl!(POST, UsageTestAssignmentService, UsageTestAssignmentIn, UsageTestAssignmentOut); +crate::service_impl!(PUT, UsageTestAssignmentService, UsageTestAssignmentIn, UsageTestAssignmentOut); + pub struct UsageTestParticipationService; -crate::service_impl!( - declare, - UsageTestParticipationService, - "usage/usagetestparticipationservice", - 2 -); -crate::service_impl!( - POST, - UsageTestParticipationService, - UsageTestParticipationIn, - () -); +crate::service_impl!(declare, UsageTestParticipationService, "usage/usagetestparticipationservice", 2); +crate::service_impl!(POST, UsageTestParticipationService, UsageTestParticipationIn, ()); diff --git a/tuta-sdk/rust/sdk/src/type_models/tutanota.json b/tuta-sdk/rust/sdk/src/type_models/tutanota.json index cbebdee32e0..ee56996c469 100644 --- a/tuta-sdk/rust/sdk/src/type_models/tutanota.json +++ b/tuta-sdk/rust/sdk/src/type_models/tutanota.json @@ -4065,15 +4065,15 @@ "name": "ImportAttachment", "since": 77, "type": "AGGREGATED_TYPE", - "id": 1490, - "rootId": "CHR1dGFub3RhAAXS", + "id": 1491, + "rootId": "CHR1dGFub3RhAAXT", "versioned": false, "encrypted": false, "values": { "_id": { "final": true, "name": "_id", - "id": 1491, + "id": 1492, "since": 77, "type": "CustomId", "cardinality": "One", @@ -4082,28 +4082,37 @@ "ownerEncFileSessionKey": { "final": true, "name": "ownerEncFileSessionKey", - "id": 1492, + "id": 1493, "since": 77, "type": "Bytes", "cardinality": "One", "encrypted": false + }, + "ownerFileKeyVersion": { + "final": false, + "name": "ownerFileKeyVersion", + "id": 1494, + "since": 77, + "type": "Number", + "cardinality": "One", + "encrypted": false } }, "associations": { - "existingFile": { + "existingAttachmentFile": { "final": true, - "name": "existingFile", - "id": 1494, + "name": "existingAttachmentFile", + "id": 1496, "since": 77, "type": "LIST_ELEMENT_ASSOCIATION_GENERATED", "cardinality": "ZeroOrOne", "refType": "File", "dependency": null }, - "newFile": { + "newAttachment": { "final": true, - "name": "newFile", - "id": 1493, + "name": "newAttachment", + "id": 1495, "since": 77, "type": "AGGREGATION", "cardinality": "ZeroOrOne", @@ -4118,15 +4127,15 @@ "name": "ImportMailData", "since": 77, "type": "AGGREGATED_TYPE", - "id": 1495, - "rootId": "CHR1dGFub3RhAAXX", + "id": 1497, + "rootId": "CHR1dGFub3RhAAXZ", "versioned": false, "encrypted": false, "values": { "_id": { "final": true, "name": "_id", - "id": 1496, + "id": 1498, "since": 77, "type": "CustomId", "cardinality": "One", @@ -4135,7 +4144,7 @@ "compressedBodyText": { "final": true, "name": "compressedBodyText", - "id": 1498, + "id": 1500, "since": 77, "type": "CompressedString", "cardinality": "One", @@ -4144,7 +4153,7 @@ "compressedHeaders": { "final": true, "name": "compressedHeaders", - "id": 1511, + "id": 1513, "since": 77, "type": "CompressedString", "cardinality": "One", @@ -4153,7 +4162,7 @@ "confidential": { "final": true, "name": "confidential", - "id": 1506, + "id": 1508, "since": 77, "type": "Boolean", "cardinality": "One", @@ -4162,7 +4171,7 @@ "date": { "final": true, "name": "date", - "id": 1499, + "id": 1501, "since": 77, "type": "Date", "cardinality": "One", @@ -4171,7 +4180,7 @@ "differentEnvelopeSender": { "final": true, "name": "differentEnvelopeSender", - "id": 1509, + "id": 1511, "since": 77, "type": "String", "cardinality": "ZeroOrOne", @@ -4180,7 +4189,7 @@ "inReplyTo": { "final": true, "name": "inReplyTo", - "id": 1503, + "id": 1505, "since": 77, "type": "String", "cardinality": "ZeroOrOne", @@ -4189,7 +4198,7 @@ "messageId": { "final": true, "name": "messageId", - "id": 1502, + "id": 1504, "since": 77, "type": "String", "cardinality": "ZeroOrOne", @@ -4198,7 +4207,7 @@ "method": { "final": true, "name": "method", - "id": 1507, + "id": 1509, "since": 77, "type": "Number", "cardinality": "One", @@ -4207,7 +4216,7 @@ "phishingStatus": { "final": true, "name": "phishingStatus", - "id": 1510, + "id": 1512, "since": 77, "type": "Number", "cardinality": "One", @@ -4216,7 +4225,7 @@ "replyType": { "final": false, "name": "replyType", - "id": 1508, + "id": 1510, "since": 77, "type": "Number", "cardinality": "One", @@ -4225,7 +4234,7 @@ "state": { "final": true, "name": "state", - "id": 1500, + "id": 1502, "since": 77, "type": "Number", "cardinality": "One", @@ -4234,7 +4243,7 @@ "subject": { "final": true, "name": "subject", - "id": 1497, + "id": 1499, "since": 77, "type": "String", "cardinality": "One", @@ -4243,7 +4252,7 @@ "unread": { "final": true, "name": "unread", - "id": 1501, + "id": 1503, "since": 77, "type": "Boolean", "cardinality": "One", @@ -4254,7 +4263,7 @@ "importedAttachments": { "final": true, "name": "importedAttachments", - "id": 1514, + "id": 1516, "since": 77, "type": "AGGREGATION", "cardinality": "Any", @@ -4264,7 +4273,7 @@ "recipients": { "final": true, "name": "recipients", - "id": 1513, + "id": 1515, "since": 77, "type": "AGGREGATION", "cardinality": "One", @@ -4274,7 +4283,7 @@ "references": { "final": true, "name": "references", - "id": 1504, + "id": 1506, "since": 77, "type": "AGGREGATION", "cardinality": "Any", @@ -4284,7 +4293,7 @@ "replyTos": { "final": false, "name": "replyTos", - "id": 1512, + "id": 1514, "since": 77, "type": "AGGREGATION", "cardinality": "Any", @@ -4294,7 +4303,7 @@ "sender": { "final": true, "name": "sender", - "id": 1505, + "id": 1507, "since": 77, "type": "AGGREGATION", "cardinality": "One", @@ -4309,15 +4318,15 @@ "name": "ImportMailDataMailReference", "since": 77, "type": "AGGREGATED_TYPE", - "id": 1479, - "rootId": "CHR1dGFub3RhAAXH", + "id": 1480, + "rootId": "CHR1dGFub3RhAAXI", "versioned": false, "encrypted": false, "values": { "_id": { "final": true, "name": "_id", - "id": 1480, + "id": 1481, "since": 77, "type": "CustomId", "cardinality": "One", @@ -4326,7 +4335,7 @@ "reference": { "final": false, "name": "reference", - "id": 1481, + "id": 1482, "since": 77, "type": "String", "cardinality": "One", @@ -4341,15 +4350,15 @@ "name": "ImportMailPostIn", "since": 77, "type": "DATA_TRANSFER_TYPE", - "id": 1515, - "rootId": "CHR1dGFub3RhAAXr", + "id": 1517, + "rootId": "CHR1dGFub3RhAAXt", "versioned": false, "encrypted": true, "values": { "_format": { "final": false, "name": "_format", - "id": 1516, + "id": 1518, "since": 77, "type": "Number", "cardinality": "One", @@ -4358,7 +4367,7 @@ "ownerEncSessionKey": { "final": false, "name": "ownerEncSessionKey", - "id": 1518, + "id": 1520, "since": 77, "type": "Bytes", "cardinality": "One", @@ -4367,7 +4376,7 @@ "ownerGroup": { "final": false, "name": "ownerGroup", - "id": 1517, + "id": 1519, "since": 77, "type": "GeneratedId", "cardinality": "One", @@ -4376,7 +4385,7 @@ "ownerKeyVersion": { "final": false, "name": "ownerKeyVersion", - "id": 1519, + "id": 1521, "since": 77, "type": "Number", "cardinality": "One", @@ -4387,7 +4396,7 @@ "imports": { "final": false, "name": "imports", - "id": 1521, + "id": 1523, "since": 77, "type": "AGGREGATION", "cardinality": "Any", @@ -4397,7 +4406,7 @@ "targetMailFolder": { "final": true, "name": "targetMailFolder", - "id": 1520, + "id": 1522, "since": 77, "type": "LIST_ELEMENT_ASSOCIATION_GENERATED", "cardinality": "One", @@ -4412,15 +4421,15 @@ "name": "ImportMailPostOut", "since": 77, "type": "DATA_TRANSFER_TYPE", - "id": 1522, - "rootId": "CHR1dGFub3RhAAXy", + "id": 1524, + "rootId": "CHR1dGFub3RhAAX0", "versioned": false, "encrypted": false, "values": { "_format": { "final": false, "name": "_format", - "id": 1523, + "id": 1525, "since": 77, "type": "Number", "cardinality": "One", @@ -4431,7 +4440,7 @@ "mails": { "final": false, "name": "mails", - "id": 1524, + "id": 1526, "since": 77, "type": "LIST_ELEMENT_ASSOCIATION_GENERATED", "cardinality": "Any", @@ -5373,6 +5382,16 @@ "refType": "MailFolderRef", "dependency": null }, + "importedAttachments": { + "final": false, + "name": "importedAttachments", + "id": 1479, + "since": 77, + "type": "LIST_ASSOCIATION", + "cardinality": "One", + "refType": "File", + "dependency": null + }, "mailDetailsDrafts": { "final": false, "name": "mailDetailsDrafts", @@ -6298,15 +6317,15 @@ "name": "NewImportAttachment", "since": 77, "type": "AGGREGATED_TYPE", - "id": 1482, - "rootId": "CHR1dGFub3RhAAXK", + "id": 1483, + "rootId": "CHR1dGFub3RhAAXL", "versioned": false, "encrypted": false, "values": { "_id": { "final": true, "name": "_id", - "id": 1483, + "id": 1484, "since": 77, "type": "CustomId", "cardinality": "One", @@ -6315,7 +6334,7 @@ "encCid": { "final": true, "name": "encCid", - "id": 1488, + "id": 1489, "since": 77, "type": "Bytes", "cardinality": "ZeroOrOne", @@ -6324,7 +6343,7 @@ "encFileHash": { "final": true, "name": "encFileHash", - "id": 1485, + "id": 1486, "since": 77, "type": "Bytes", "cardinality": "ZeroOrOne", @@ -6333,7 +6352,7 @@ "encFileName": { "final": true, "name": "encFileName", - "id": 1486, + "id": 1487, "since": 77, "type": "Bytes", "cardinality": "One", @@ -6342,7 +6361,7 @@ "encMimeType": { "final": true, "name": "encMimeType", - "id": 1487, + "id": 1488, "since": 77, "type": "Bytes", "cardinality": "One", @@ -6351,7 +6370,7 @@ "ownerEncFileHashSessionKey": { "final": true, "name": "ownerEncFileHashSessionKey", - "id": 1484, + "id": 1485, "since": 77, "type": "Bytes", "cardinality": "ZeroOrOne", @@ -6362,7 +6381,7 @@ "referenceTokens": { "final": true, "name": "referenceTokens", - "id": 1489, + "id": 1490, "since": 77, "type": "AGGREGATION", "cardinality": "Any", From 35ab92ec0b13c494d456c7e1c59dd0b3fdec5dd7 Mon Sep 17 00:00:00 2001 From: jhm <17314077+jomapp@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:33:54 +0100 Subject: [PATCH 22/32] wip import attachments --- packages/node-mimimi/src/importer.rs | 744 ++++++----- .../src/importer/importable_mail.rs | 1190 ++++++++--------- src/common/api/entities/tutanota/ModelInfo.ts | 2 +- .../worker/offline/OfflineStorageMigrator.ts | 2 + .../worker/offline/migrations/tutanota-v77.ts | 13 + 5 files changed, 1012 insertions(+), 939 deletions(-) create mode 100644 src/common/api/worker/offline/migrations/tutanota-v77.ts diff --git a/packages/node-mimimi/src/importer.rs b/packages/node-mimimi/src/importer.rs index 54f78c7753c..a0329f88e9b 100644 --- a/packages/node-mimimi/src/importer.rs +++ b/packages/node-mimimi/src/importer.rs @@ -1,7 +1,7 @@ use crate::importer::file_reader::import_client::{FileImport, FileIterationError}; use crate::importer::imap_reader::import_client::{ImapImport, ImapIterationError}; use crate::importer::imap_reader::ImapImportConfig; -use crate::importer::importable_mail::ImportableMail; +use crate::importer::importable_mail::{ImportableMail, ImportableMailAttachment}; use crate::reduce_to_chunks::reduce_to_chunks; use crate::tuta::credentials::TutaCredentials; use napi::bindgen_prelude::Error as NapiError; @@ -9,12 +9,13 @@ use std::sync::{Arc, Mutex}; use tutasdk::crypto::aes::Iv; use tutasdk::crypto::key::GenericAesKey; use tutasdk::crypto::randomizer_facade::RandomizerFacade; -use tutasdk::entities::generated::tutanota::{ImportMailData, ImportMailPostIn}; +use tutasdk::entities::generated::tutanota::{ImportAttachment, ImportMailData, ImportMailPostIn, NewImportAttachment}; use tutasdk::entities::json_size_estimator::estimate_json_size; use tutasdk::login::Credentials; use tutasdk::net::native_rest_client::NativeRestClient; use tutasdk::services::generated::tutanota::ImportMailService; use tutasdk::services::ExtraServiceParams; +use tutasdk::tutanota_constants::ArchiveDataType; use tutasdk::GeneratedId; use tutasdk::{IdTupleGenerated, LoggedInSdk, Sdk}; @@ -26,13 +27,13 @@ mod importable_mail; #[derive(Clone, PartialEq)] pub enum ImportParams { - Imap { - imap_import_config: ImapImportConfig, - }, - LocalFile { - file_path: String, - is_mbox: bool, - }, + Imap { + imap_import_config: ImapImportConfig, + }, + LocalFile { + file_path: String, + is_mbox: bool, + }, } /// current state of the imap_reader import for this tuta account @@ -42,51 +43,51 @@ pub enum ImportParams { #[derive(PartialEq, Default)] #[cfg_attr(test, derive(Debug))] pub enum ImportState { - #[default] - NotInitialized, - Paused, - Running, - Postponed, - Finished, + #[default] + NotInitialized, + Paused, + Running, + Postponed, + Finished, } #[cfg_attr(feature = "javascript", napi_derive::napi(object))] #[derive(PartialEq, Clone, Default)] #[cfg_attr(test, derive(Debug))] pub struct ImportStatus { - pub state: ImportState, - pub imported_mails: u32, + pub state: ImportState, + pub imported_mails: u32, } struct Importer { - status: ImportStatus, - logged_in_sdk: Arc, - target_owner_group: GeneratedId, - target_mail_folder: IdTupleGenerated, - import_source: Arc>, - randomizer_facade: RandomizerFacade, + status: ImportStatus, + logged_in_sdk: Arc, + target_owner_group: GeneratedId, + target_mail_folder: IdTupleGenerated, + import_source: Arc>, + randomizer_facade: RandomizerFacade, } pub enum ImportSource { - RemoteImap { imap_import_client: ImapImport }, - LocalFile { fs_email_client: FileImport }, + RemoteImap { imap_import_client: ImapImport }, + LocalFile { fs_email_client: FileImport }, } /// Wrapper for `Importer` to be used from napi-rs interface #[cfg_attr(feature = "javascript", napi_derive::napi)] pub struct ImporterApi { - inner: Arc>, + inner: Arc>, } #[derive(Debug, PartialEq, Clone)] pub enum IterationError { - Imap(ImapIterationError), - File(FileIterationError), + Imap(ImapIterationError), + File(FileIterationError), } struct ImportSourceIterator { // it would be nice to not need the mutex, but when the importer continues the import, - // it mutates its own state and also calls mutating functions on the source. solving this + // it mutates its own state and also calls mutating functions on the source. solving this // probably requires a bigger restructure of the code (it's very OOP atm) source: Arc>, } @@ -131,49 +132,106 @@ impl Importer { Ok(self.status.clone()) } - /// once we get the ImportableMail from either of source, - /// continue to the uploading counterpart - async fn import_all_mail( - &mut self, + /// once we get the ImportableMail from either of source, + /// continue to the uploading counterpart + async fn import_all_mail( + &mut self, importable_mails: Iter, ) -> Result, ()> where Iter: Iterator, { - let new_aes_256_key = GenericAesKey::from_bytes( - self.randomizer_facade - .generate_random_array::<{ tutasdk::crypto::aes::AES_256_KEY_SIZE }>() - .as_slice(), - ) - .unwrap(); - let mail_group_key = self - .logged_in_sdk - .get_current_sym_group_key(&self.target_owner_group) - .await - .map_err(|_e| ())?; - let owner_enc_session_key = - mail_group_key.encrypt_key(&new_aes_256_key, Iv::generate(&self.randomizer_facade)); - - const MAX_REQUEST_SIZE: usize = 1024 * 1024 * 10; - let import_chunks: Vec> = reduce_to_chunks( - importable_mails.map(ImportMailData::from), - MAX_REQUEST_SIZE, - Box::new(estimate_json_size), + let new_mail_aes_256_key = GenericAesKey::from_bytes( + self.randomizer_facade + .generate_random_array::<{ tutasdk::crypto::aes::AES_256_KEY_SIZE }>() + .as_slice(), + ) + .unwrap(); + let mail_group_key = self + .logged_in_sdk + .get_current_sym_group_key(&self.target_owner_group) + .await + .map_err(|_e| ())?; + let owner_enc_mail_session_key = + mail_group_key.encrypt_key(&new_mail_aes_256_key, Iv::generate(&self.randomizer_facade)); + + const MAX_REQUEST_SIZE: usize = 1024 * 1024 * 5; + let import_mail_data_and_attachments = importable_mails.map(<(ImportMailData, Vec)>::from); + let import_chunks = reduce_to_chunks( + import_mail_data_and_attachments, + MAX_REQUEST_SIZE, + Box::new(|(imd, a)| estimate_json_size(imd)), ) - .collect(); - - let mut mails: Vec = Vec::new(); - let mut new_status = ImportStatus { - state: ImportState::Running, - imported_mails: 0, - }; - for imports in import_chunks { - let import_len = imports.len(); - let import_mail_post_in = ImportMailPostIn { - ownerEncSessionKey: owner_enc_session_key.object.clone(), + ; + + let mut mails: Vec = Vec::new(); + let mut new_status = ImportStatus { + state: ImportState::Running, + imported_mails: 0, + }; + for imports in import_chunks { + let import_len = imports.len(); + + let mut imports_with_attachments = Vec::new(); + for (import_mail_data, importable_mail_attachments) in imports.into_iter() { + let mut import_mail_data = import_mail_data; + let mut import_attachments = Vec::new(); + for importable_mail_attachment in importable_mail_attachments { + let importable_mail_attachment = importable_mail_attachment as ImportableMailAttachment; + let new_file_aes_256_key = GenericAesKey::from_bytes( + self.randomizer_facade + .generate_random_array::<{ tutasdk::crypto::aes::AES_256_KEY_SIZE }>() + .as_slice(), + ).unwrap(); + let owner_enc_file_session_key = mail_group_key + .encrypt_key(&new_file_aes_256_key, Iv::generate(&self.randomizer_facade)); + + let reference_tokens = self.logged_in_sdk + .blob_facade() + .encrypt_and_upload( + ArchiveDataType::Attachments, + &self.target_owner_group, + &new_file_aes_256_key, + importable_mail_attachment.content, + ) + .await + .unwrap(); + + // todo: do we need to upload the ivs and how? + let enc_file_name = new_file_aes_256_key.encrypt_data(importable_mail_attachment.filename.as_ref(), Iv::generate(&self.randomizer_facade)).unwrap(); + let enc_mime_type = new_file_aes_256_key.encrypt_data(importable_mail_attachment.content_type.as_ref(), Iv::generate(&self.randomizer_facade)).unwrap(); + let enc_cid: Option> = match importable_mail_attachment.content_id { + Some(cid) => Some(new_file_aes_256_key.encrypt_data(cid.as_bytes(), Iv::generate(&self.randomizer_facade)).unwrap()), + None => None, + }; + + let import_attachment = ImportAttachment { + _id: None, + ownerEncFileSessionKey: owner_enc_file_session_key.object, + ownerFileKeyVersion: owner_enc_file_session_key.version, + existingAttachmentFile: None, + newAttachment: Some(NewImportAttachment { + _id: None, + encCid: enc_cid, + encFileHash: None, + encFileName: enc_file_name, + encMimeType: enc_mime_type, + ownerEncFileHashSessionKey: None, + referenceTokens: reference_tokens, + }), + }; + + import_attachments.push(import_attachment); + } + import_mail_data.importedAttachments = import_attachments; + imports_with_attachments.push(import_mail_data); + } + + let import_mail_post_in = ImportMailPostIn { + ownerEncSessionKey: owner_enc_mail_session_key.object.clone(), ownerGroup: self.target_owner_group.clone(), - ownerKeyVersion: owner_enc_session_key.version, - imports, + ownerKeyVersion: owner_enc_mail_session_key.version, + imports: imports_with_attachments, targetMailFolder: self.target_mail_folder.clone(), _format: 0, _errors: None, @@ -181,7 +239,7 @@ impl Importer { }; let service_params = ExtraServiceParams { - session_key: Some(new_aes_256_key.clone()), + session_key: Some(new_mail_aes_256_key.clone()), ..Default::default() }; @@ -222,305 +280,305 @@ impl Importer { self.status = new_status; Ok(mails) - } + } } impl ImporterApi { - pub fn new( - logged_in_sdk: Arc, - target_owner_group: GeneratedId, - target_mail_folder: IdTupleGenerated, - import_source: Arc>, - ) -> Self { - let import_inner = Importer { - logged_in_sdk, - target_owner_group, - target_mail_folder, - import_source, - status: ImportStatus::default(), - randomizer_facade: RandomizerFacade::from_core(rand::rngs::OsRng), - }; - Self { - inner: Arc::new(NapiTokioMutex::new(import_inner)), - } - } - - pub async fn continue_import_inner(&mut self) -> Result { - self.inner.lock().await.continue_import().await - } - - pub async fn delete_import_inner(&mut self) -> Result { - todo!() - } - - pub async fn pause_import_inner(&mut self) -> Result { - todo!() - } - - pub async fn create_file_importer_inner( - tuta_credentials: TutaCredentials, - target_owner_group: String, - target_mail_folder: (String, String), - source_paths: Vec, - ) -> napi::Result { - let logged_in_sdk_future = Self::create_sdk(tuta_credentials); - - let fs_email_client = FileImport::new(source_paths) - .map_err(|_e| NapiError::from_reason("Cannot create file import"))?; - let import_source = Arc::new(Mutex::new(ImportSource::LocalFile { fs_email_client })); - let logged_in_sdk = logged_in_sdk_future - .await - .map_err(|_e| NapiError::from_reason("Cannot create logged in sdk"))?; - - Ok(ImporterApi::new( - logged_in_sdk, - GeneratedId(target_owner_group), - IdTupleGenerated::new( - GeneratedId(target_mail_folder.0), - GeneratedId(target_mail_folder.1), - ), - import_source, - )) - } - - async fn create_sdk(tuta_credentials: TutaCredentials) -> Result, String> { - let rest_client = Arc::new( - NativeRestClient::try_new() - .map_err(|e| format!("Cannot build native rest client: {e}"))?, - ); - - let logged_in_sdk = { - let sdk = Sdk::new(tuta_credentials.api_url.clone(), rest_client); - - let sdk_credentials: Credentials = tuta_credentials - .clone() - .try_into() - .map_err(|_| "Cannot convert to valid credentials".to_string())?; - sdk.login(sdk_credentials) - .await - .map_err(|e| format!("Cannot login to sdk. Error: {:?}", e))? - }; - - Ok(logged_in_sdk) - } + pub fn new( + logged_in_sdk: Arc, + target_owner_group: GeneratedId, + target_mail_folder: IdTupleGenerated, + import_source: Arc>, + ) -> Self { + let import_inner = Importer { + logged_in_sdk, + target_owner_group, + target_mail_folder, + import_source, + status: ImportStatus::default(), + randomizer_facade: RandomizerFacade::from_core(rand::rngs::OsRng), + }; + Self { + inner: Arc::new(NapiTokioMutex::new(import_inner)), + } + } + + pub async fn continue_import_inner(&mut self) -> Result { + self.inner.lock().await.continue_import().await + } + + pub async fn delete_import_inner(&mut self) -> Result { + todo!() + } + + pub async fn pause_import_inner(&mut self) -> Result { + todo!() + } + + pub async fn create_file_importer_inner( + tuta_credentials: TutaCredentials, + target_owner_group: String, + target_mail_folder: (String, String), + source_paths: Vec, + ) -> napi::Result { + let logged_in_sdk_future = Self::create_sdk(tuta_credentials); + + let fs_email_client = FileImport::new(source_paths) + .map_err(|_e| NapiError::from_reason("Cannot create file import"))?; + let import_source = Arc::new(Mutex::new(ImportSource::LocalFile { fs_email_client })); + let logged_in_sdk = logged_in_sdk_future + .await + .map_err(|_e| NapiError::from_reason("Cannot create logged in sdk"))?; + + Ok(ImporterApi::new( + logged_in_sdk, + GeneratedId(target_owner_group), + IdTupleGenerated::new( + GeneratedId(target_mail_folder.0), + GeneratedId(target_mail_folder.1), + ), + import_source, + )) + } + + async fn create_sdk(tuta_credentials: TutaCredentials) -> Result, String> { + let rest_client = Arc::new( + NativeRestClient::try_new() + .map_err(|e| format!("Cannot build native rest client: {e}"))?, + ); + + let logged_in_sdk = { + let sdk = Sdk::new(tuta_credentials.api_url.clone(), rest_client); + + let sdk_credentials: Credentials = tuta_credentials + .clone() + .try_into() + .map_err(|_| "Cannot convert to valid credentials".to_string())?; + sdk.login(sdk_credentials) + .await + .map_err(|e| format!("Cannot login to sdk. Error: {:?}", e))? + }; + + Ok(logged_in_sdk) + } } // Wrapper for napi #[cfg(feature = "javascript")] #[napi_derive::napi] impl ImporterApi { - // once Self::continue_import return custom error, - // do the error conversion here, or in trait - fn error_conversion(_err: E) -> napi::Error { - todo!() - } - - #[napi] - pub async unsafe fn continue_import(&mut self) -> napi::Result { - self.continue_import_inner() - .await - .map_err(Self::error_conversion) - } - - #[napi] - pub async unsafe fn delete_import(&mut self) -> napi::Result { - self.delete_import_inner() - .await - .map_err(Self::error_conversion) - } - - #[napi] - pub async unsafe fn pause_import(&mut self) -> napi::Result { - self.pause_import_inner() - .await - .map_err(Self::error_conversion) - } - - #[napi] - pub async fn create_file_importer( - tuta_credentials: TutaCredentials, - target_owner_group: String, - target_mail_folder: (String, String), - source_paths: Vec, - ) -> napi::Result { - Self::create_file_importer_inner( - tuta_credentials, - target_owner_group, - target_mail_folder, - source_paths, - ) - .await - } + // once Self::continue_import return custom error, + // do the error conversion here, or in trait + fn error_conversion(_err: E) -> napi::Error { + todo!() + } + + #[napi] + pub async unsafe fn continue_import(&mut self) -> napi::Result { + self.continue_import_inner() + .await + .map_err(Self::error_conversion) + } + + #[napi] + pub async unsafe fn delete_import(&mut self) -> napi::Result { + self.delete_import_inner() + .await + .map_err(Self::error_conversion) + } + + #[napi] + pub async unsafe fn pause_import(&mut self) -> napi::Result { + self.pause_import_inner() + .await + .map_err(Self::error_conversion) + } + + #[napi] + pub async fn create_file_importer( + tuta_credentials: TutaCredentials, + target_owner_group: String, + target_mail_folder: (String, String), + source_paths: Vec, + ) -> napi::Result { + Self::create_file_importer_inner( + tuta_credentials, + target_owner_group, + target_mail_folder, + source_paths, + ) + .await + } } #[cfg(test)] mod tests { - use super::*; - use crate::importer::imap_reader::{ImapCredentials, LoginMechanism}; - use crate::tuta_imap::testing::GreenMailTestServer; - use mail_builder::MessageBuilder; - use tutasdk::entities::generated::tutanota::MailFolder; - use tutasdk::folder_system::MailSetKind; - use tutasdk::net::native_rest_client::NativeRestClient; - use tutasdk::Sdk; - - fn sample_email(subject: String) -> String { - let email = MessageBuilder::new() + use super::*; + use crate::importer::imap_reader::{ImapCredentials, LoginMechanism}; + use crate::tuta_imap::testing::GreenMailTestServer; + use mail_builder::MessageBuilder; + use tutasdk::entities::generated::tutanota::MailFolder; + use tutasdk::folder_system::MailSetKind; + use tutasdk::net::native_rest_client::NativeRestClient; + use tutasdk::Sdk; + + fn sample_email(subject: String) -> String { + let email = MessageBuilder::new() .from(("Matthias", "map@example.org")) .to(("Johannes", "jhm@example.org")) .subject(subject) .text_body("Hello tutao! this is the first step to have email import.Want to see html πŸ˜€?

red

") .write_to_string() .unwrap(); - email - } - - async fn get_test_import_folder_id( - logged_in_sdk: &Arc, - kind: MailSetKind, - ) -> MailFolder { - let mail_facade = logged_in_sdk.mail_facade(); - let mailbox = mail_facade.load_user_mailbox().await.unwrap(); - let folders = mail_facade - .load_folders_for_mailbox(&mailbox) - .await - .unwrap(); - folders - .system_folder_by_type(kind) - .expect("inbox should exist") - .clone() - } - - async fn init_imap_importer() -> (Importer, GreenMailTestServer) { - let importer_mail_address = "map-free@tutanota.de".to_string(); - let logged_in_sdk = Sdk::new( - "http://localhost:9000".to_string(), - Arc::new(NativeRestClient::try_new().unwrap()), - ) - .create_session(importer_mail_address.as_str(), "map") - .await - .unwrap(); - let greenmail = GreenMailTestServer::new(); - let imap_import_config = ImapImportConfig { - root_import_mail_folder_name: "/".to_string(), - credentials: ImapCredentials { - host: "127.0.0.1".to_string(), - port: greenmail.imaps_port.try_into().unwrap(), - login_mechanism: LoginMechanism::Plain { - username: "sug@example.org".to_string(), - password: "sug".to_string(), - }, - }, - }; - - let import_source = Arc::new(Mutex::new(ImportSource::RemoteImap { - imap_import_client: ImapImport::new(imap_import_config), - })); - let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng); - let target_mail_folder = get_test_import_folder_id(&logged_in_sdk, MailSetKind::Archive) - .await - ._id - .unwrap(); - - let target_owner_group = logged_in_sdk - .mail_facade() - .get_group_id_for_mail_address(importer_mail_address.as_str()) - .await - .unwrap(); - - let importer = Importer { - target_owner_group, - target_mail_folder, - logged_in_sdk, - import_source, - randomizer_facade, - status: ImportStatus::default(), - }; + email + } + + async fn get_test_import_folder_id( + logged_in_sdk: &Arc, + kind: MailSetKind, + ) -> MailFolder { + let mail_facade = logged_in_sdk.mail_facade(); + let mailbox = mail_facade.load_user_mailbox().await.unwrap(); + let folders = mail_facade + .load_folders_for_mailbox(&mailbox) + .await + .unwrap(); + folders + .system_folder_by_type(kind) + .expect("inbox should exist") + .clone() + } + + async fn init_imap_importer() -> (Importer, GreenMailTestServer) { + let importer_mail_address = "map-free@tutanota.de".to_string(); + let logged_in_sdk = Sdk::new( + "http://localhost:9000".to_string(), + Arc::new(NativeRestClient::try_new().unwrap()), + ) + .create_session(importer_mail_address.as_str(), "map") + .await + .unwrap(); + let greenmail = GreenMailTestServer::new(); + let imap_import_config = ImapImportConfig { + root_import_mail_folder_name: "/".to_string(), + credentials: ImapCredentials { + host: "127.0.0.1".to_string(), + port: greenmail.imaps_port.try_into().unwrap(), + login_mechanism: LoginMechanism::Plain { + username: "sug@example.org".to_string(), + password: "sug".to_string(), + }, + }, + }; + + let import_source = Arc::new(Mutex::new(ImportSource::RemoteImap { + imap_import_client: ImapImport::new(imap_import_config), + })); + let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng); + let target_mail_folder = get_test_import_folder_id(&logged_in_sdk, MailSetKind::Archive) + .await + ._id + .unwrap(); - (importer, greenmail) - } + let target_owner_group = logged_in_sdk + .mail_facade() + .get_group_id_for_mail_address(importer_mail_address.as_str()) + .await + .unwrap(); - pub async fn init_file_importer(source_paths: Vec) -> Importer { - let logged_in_sdk = Sdk::new( - "http://localhost:9000".to_string(), - Arc::new(NativeRestClient::try_new().unwrap()), - ) - .create_session("map-free@tutanota.de", "map") - .await - .unwrap(); - - let import_source = Arc::new(Mutex::new(ImportSource::LocalFile { - fs_email_client: FileImport::new(source_paths).unwrap(), - })); - let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng); - let target_mail_folder = get_test_import_folder_id(&logged_in_sdk, MailSetKind::Archive) - .await - ._id - .unwrap(); - - let target_owner_group = logged_in_sdk - .mail_facade() - .get_group_id_for_mail_address("map-free@tutanota.de") - .await - .unwrap(); - - Importer { - status: ImportStatus::default(), - target_owner_group, - target_mail_folder, - logged_in_sdk, - import_source, - randomizer_facade, - } - } + let importer = Importer { + target_owner_group, + target_mail_folder, + logged_in_sdk, + import_source, + randomizer_facade, + status: ImportStatus::default(), + }; + + (importer, greenmail) + } + + pub async fn init_file_importer(source_paths: Vec) -> Importer { + let logged_in_sdk = Sdk::new( + "http://localhost:9000".to_string(), + Arc::new(NativeRestClient::try_new().unwrap()), + ) + .create_session("map-free@tutanota.de", "map") + .await + .unwrap(); - #[tokio::test] - pub async fn import_multiple_from_imap_default_folder() { - let (mut importer, greenmail) = init_imap_importer().await; - - let email_first = sample_email("Hello from imap πŸ˜€! -- Бписок.doc".to_string()); - let email_second = sample_email("Second time: hello".to_string()); - greenmail.store_mail("sug@example.org", email_first.as_str()); - greenmail.store_mail("sug@example.org", email_second.as_str()); - - let import_res = importer.continue_import().await.map_err(|_| ()); - assert_eq!( - Ok(ImportStatus { - state: ImportState::Finished, - imported_mails: 2, - }), - import_res - ); - } + let import_source = Arc::new(Mutex::new(ImportSource::LocalFile { + fs_email_client: FileImport::new(source_paths).unwrap(), + })); + let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng); + let target_mail_folder = get_test_import_folder_id(&logged_in_sdk, MailSetKind::Archive) + .await + ._id + .unwrap(); - #[tokio::test] - pub async fn import_single_from_imap_default_folder() { - let (mut importer, greenmail) = init_imap_importer().await; - - let email = sample_email("Single email".to_string()); - greenmail.store_mail("sug@example.org", email.as_str()); - - let import_res = importer.continue_import().await.map_err(|_| ()); - assert_eq!( - Ok(ImportStatus { - state: ImportState::Finished, - imported_mails: 1, - }), - import_res - ); - } + let target_owner_group = logged_in_sdk + .mail_facade() + .get_group_id_for_mail_address("map-free@tutanota.de") + .await + .unwrap(); - #[tokio::test] - async fn can_import_single_eml_file() { - let mut importer = init_file_importer(vec!["./test/sample.eml".to_string()]).await; - - let import_res = importer.continue_import().await.map_err(|_| ()); - assert_eq!( - Ok(ImportStatus { - state: ImportState::Finished, - imported_mails: 1, - }), - import_res - ); - } + Importer { + status: ImportStatus::default(), + target_owner_group, + target_mail_folder, + logged_in_sdk, + import_source, + randomizer_facade, + } + } + + #[tokio::test] + pub async fn import_multiple_from_imap_default_folder() { + let (mut importer, greenmail) = init_imap_importer().await; + + let email_first = sample_email("Hello from imap πŸ˜€! -- Бписок.doc".to_string()); + let email_second = sample_email("Second time: hello".to_string()); + greenmail.store_mail("sug@example.org", email_first.as_str()); + greenmail.store_mail("sug@example.org", email_second.as_str()); + + let import_res = importer.continue_import().await.map_err(|_| ()); + assert_eq!( + Ok(ImportStatus { + state: ImportState::Finished, + imported_mails: 2, + }), + import_res + ); + } + + #[tokio::test] + pub async fn import_single_from_imap_default_folder() { + let (mut importer, greenmail) = init_imap_importer().await; + + let email = sample_email("Single email".to_string()); + greenmail.store_mail("sug@example.org", email.as_str()); + + let import_res = importer.continue_import().await.map_err(|_| ()); + assert_eq!( + Ok(ImportStatus { + state: ImportState::Finished, + imported_mails: 1, + }), + import_res + ); + } + + #[tokio::test] + async fn can_import_single_eml_file() { + let mut importer = init_file_importer(vec!["./test/sample.eml".to_string()]).await; + + let import_res = importer.continue_import().await.map_err(|_| ()); + assert_eq!( + Ok(ImportStatus { + state: ImportState::Finished, + imported_mails: 1, + }), + import_res + ); + } } diff --git a/packages/node-mimimi/src/importer/importable_mail.rs b/packages/node-mimimi/src/importer/importable_mail.rs index 34ad965bdbc..43dac04d865 100644 --- a/packages/node-mimimi/src/importer/importable_mail.rs +++ b/packages/node-mimimi/src/importer/importable_mail.rs @@ -2,8 +2,8 @@ use crate::tuta_imap::client::types::ImapMail; use extend_mail_parser::MakeString; use mail_builder::headers::Header; use mail_parser::{ - Address, ContentType, GetHeader, HeaderValue, Message, MessageParser, MessagePart, - MessagePartId, MimeHeaders, PartType, + Address, ContentType, GetHeader, HeaderValue, Message, MessageParser, MessagePart, + MessagePartId, MimeHeaders, PartType, }; use regex::Regex; use std::borrow::Cow; @@ -11,206 +11,206 @@ use std::collections::{HashMap, HashSet}; use std::time::SystemTime; use tutasdk::date::DateTime; use tutasdk::entities::generated::tutanota::{ - EncryptedMailAddress, ImportMailData, ImportMailDataMailReference, MailAddress, Recipients, + EncryptedMailAddress, ImportMailData, ImportMailDataMailReference, MailAddress, Recipients, }; pub mod extend_mail_parser; mod plain_text_to_html_converter; // todo: this is used for DataTransferType, so id really dont have to be unique, // but have to be valid length -const FIXED_CUSTOM_ID: &str = "____"; +pub(crate) const FIXED_CUSTOM_ID: &str = "____"; #[derive(Default)] #[cfg_attr(test, derive(PartialEq, Debug))] pub(super) enum MailState { - #[default] - Received = 2, - Sent = 1, - Draft = 0, + #[default] + Received = 2, + Sent = 1, + Draft = 0, } #[repr(i64)] #[derive(Default)] #[cfg_attr(test, derive(PartialEq, Debug))] pub(super) enum ICalType { - #[default] - Nothing = 0, - ICalPublishh = 1, - ICalRequest = 2, - ICalAdd = 3, - ICalCancel = 4, - ICalRefresh = 5, - ICalCounter = 6, - ICalDeclineCounter = 7, + #[default] + Nothing = 0, + ICalPublishh = 1, + ICalRequest = 2, + ICalAdd = 3, + ICalCancel = 4, + ICalRefresh = 5, + ICalCounter = 6, + ICalDeclineCounter = 7, } #[derive(Default)] #[cfg_attr(test, derive(PartialEq, Debug))] pub(super) enum ReplyType { - #[default] - Nothing = 0, - Reply = 1, - Forward = 2, - ReplyForward = 3, + #[default] + Nothing = 0, + Reply = 1, + Forward = 2, + ReplyForward = 3, } #[cfg_attr(test, derive(PartialEq, Debug, Clone))] pub(super) struct ImportableMailAttachment { - filename: String, - content_id: Option, - content_type: String, - content: Vec, - is_inline: bool, + pub filename: String, + pub content_id: Option, + pub content_type: String, + pub content: Vec, + is_inline: bool, } #[cfg_attr(test, derive(PartialEq, Debug))] pub(super) enum BodyText { - Html(String), - Plain(String), + Html(String), + Plain(String), } #[derive(Default, PartialEq)] #[cfg_attr(test, derive(Debug))] pub(super) struct MailContact { - pub mail_address: String, - pub name: String, + pub mail_address: String, + pub name: String, } impl<'a> From> for MailContact { - fn from(value: mail_parser::Addr) -> Self { - Self { - name: value.name.unwrap_or_default().to_string(), - mail_address: value.address.unwrap_or_default().to_string(), - } - } + fn from(value: mail_parser::Addr) -> Self { + Self { + name: value.name.unwrap_or_default().to_string(), + mail_address: value.address.unwrap_or_default().to_string(), + } + } } impl From for MailAddress { - fn from(value: MailContact) -> Self { - Self { - _id: None, - address: value.mail_address, - name: value.name, - contact: None, - _finalIvs: Default::default(), - } - } + fn from(value: MailContact) -> Self { + Self { + _id: None, + address: value.mail_address, + name: value.name, + contact: None, + _finalIvs: Default::default(), + } + } } /// Input data for mail import service #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ImportableMail { - pub(super) headers_string: String, - pub(super) subject: String, - pub(super) html_body_text: String, - pub(super) attachments: Vec, - - pub(super) date: Option, - - pub(super) different_envelope_sender: Option, - pub(super) from_addresses: Vec, - pub(super) to_addresses: Vec, - pub(super) cc_addresses: Vec, - pub(super) bcc_addresses: Vec, - pub(super) reply_to_addresses: Vec, - - pub(super) ical_type: ICalType, - pub(super) reply_type: ReplyType, - - pub(super) mail_state: MailState, - pub(super) is_phishing: bool, // https://turbo.fish/::%3Cphising%3E - pub(super) unread: bool, - - pub(super) message_id: Option, - pub(super) in_reply_to: Option, - pub(super) references: Vec, + pub(super) headers_string: String, + pub(super) subject: String, + pub(super) html_body_text: String, + pub(super) attachments: Vec, + + pub(super) date: Option, + + pub(super) different_envelope_sender: Option, + pub(super) from_addresses: Vec, + pub(super) to_addresses: Vec, + pub(super) cc_addresses: Vec, + pub(super) bcc_addresses: Vec, + pub(super) reply_to_addresses: Vec, + + pub(super) ical_type: ICalType, + pub(super) reply_type: ReplyType, + + pub(super) mail_state: MailState, + pub(super) is_phishing: bool, // https://turbo.fish/::%3Cphising%3E + pub(super) unread: bool, + + pub(super) message_id: Option, + pub(super) in_reply_to: Option, + pub(super) references: Vec, } impl ImportableMail { - /// Utility function to convert mail_parser::Address - /// to a list of tutasdk::MailAddress - /// in such a way that every address must have mail-address and optional name - /// - /// returns None, if any of the address have empty mail-address - /// - /// set the _id: of all mail address to random 4-byte long customId, - /// this will only be valid in dataTransferType context - fn map_to_tuta_mail_address(mail_parser_addresses: Cow
) -> Vec { - let address_list = match mail_parser_addresses.as_ref() { - Address::List(address_list) => Cow::Borrowed(address_list), - Address::Group(group_senders) => { - let group_addresses = group_senders - .iter() - .map(|group| group.addresses.as_slice()) - .collect::>() - .concat(); - - Cow::Owned(group_addresses) - }, - }; - - address_list - .as_ref() - .iter() - .map(|address| MailContact { - mail_address: address.address().unwrap_or_default().to_string(), - name: address.name().unwrap_or_default().to_string(), - }) - .collect() - } - - fn handle_plain_text(email_body_as_html: &mut String, plain_text: &str) { - let plain_text_as_html = plain_text_to_html_converter::plain_text_to_html(plain_text); - Self::handle_html_text(email_body_as_html, plain_text_as_html.as_str()) - } - - fn handle_html_text(email_body_as_html: &mut String, html_text: &str) { - email_body_as_html.push_str(html_text); - } - - // from the parsed message - // return : - // .0 a single string that ca be display as email in html format - // .1 list of attachment found - fn process_all_parts( - parsed_message: &mail_parser::Message, - ) -> Result<(String, Vec), MailParseError> { - let mut email_body_as_html = String::new(); - let mut attachments = Vec::with_capacity(parsed_message.attachments.len()); - - // all the alternative of multipart/alternative that we chose not to include - let mut multipart_ignored_alternative = HashSet::new(); - - for (part_id, part) in parsed_message.parts.iter().enumerate() { - if multipart_ignored_alternative.contains(&part_id) { - continue; - } - match &part.body { - PartType::Binary(binary_content) => { - Self::handle_binary(part, &mut attachments, binary_content.to_vec(), false); - }, - - PartType::InlineBinary(binary_content) => { - Self::handle_binary(part, &mut attachments, binary_content.to_vec(), true); - }, - - PartType::Text(text) => { + /// Utility function to convert mail_parser::Address + /// to a list of tutasdk::MailAddress + /// in such a way that every address must have mail-address and optional name + /// + /// returns None, if any of the address have empty mail-address + /// + /// set the _id: of all mail address to random 4-byte long customId, + /// this will only be valid in dataTransferType context + fn map_to_tuta_mail_address(mail_parser_addresses: Cow
) -> Vec { + let address_list = match mail_parser_addresses.as_ref() { + Address::List(address_list) => Cow::Borrowed(address_list), + Address::Group(group_senders) => { + let group_addresses = group_senders + .iter() + .map(|group| group.addresses.as_slice()) + .collect::>() + .concat(); + + Cow::Owned(group_addresses) + } + }; + + address_list + .as_ref() + .iter() + .map(|address| MailContact { + mail_address: address.address().unwrap_or_default().to_string(), + name: address.name().unwrap_or_default().to_string(), + }) + .collect() + } + + fn handle_plain_text(email_body_as_html: &mut String, plain_text: &str) { + let plain_text_as_html = plain_text_to_html_converter::plain_text_to_html(plain_text); + Self::handle_html_text(email_body_as_html, plain_text_as_html.as_str()) + } + + fn handle_html_text(email_body_as_html: &mut String, html_text: &str) { + email_body_as_html.push_str(html_text); + } + + // from the parsed message + // return : + // .0 a single string that ca be display as email in html format + // .1 list of attachment found + fn process_all_parts( + parsed_message: &mail_parser::Message, + ) -> Result<(String, Vec), MailParseError> { + let mut email_body_as_html = String::new(); + let mut attachments = Vec::with_capacity(parsed_message.attachments.len()); + + // all the alternative of multipart/alternative that we chose not to include + let mut multipart_ignored_alternative = HashSet::new(); + + for (part_id, part) in parsed_message.parts.iter().enumerate() { + if multipart_ignored_alternative.contains(&part_id) { + continue; + } + match &part.body { + PartType::Binary(binary_content) => { + Self::handle_binary(part, &mut attachments, binary_content.to_vec(), false); + } + + PartType::InlineBinary(binary_content) => { + Self::handle_binary(part, &mut attachments, binary_content.to_vec(), true); + } + + PartType::Text(text) => { if !Self::is_attachment(&email_body_as_html, part) && Self::is_plain_text(part) { - Self::handle_plain_text(&mut email_body_as_html, text.as_ref()); - } else { - Self::handle_binary( - part, - &mut attachments, - text.as_bytes().to_vec(), - false, - ); - } - }, - - PartType::Html(html_text) => { + Self::handle_plain_text(&mut email_body_as_html, text.as_ref()); + } else { + Self::handle_binary( + part, + &mut attachments, + text.as_bytes().to_vec(), + false, + ); + } + } + + PartType::Html(html_text) => { if !Self::is_attachment(&email_body_as_html, part) { - Self::handle_html_text(&mut email_body_as_html, html_text.as_ref()) + Self::handle_html_text(&mut email_body_as_html, html_text.as_ref()) } else { Self::handle_binary( part, @@ -221,23 +221,23 @@ impl ImportableMail { } }, - PartType::Message(attached_message) => { - let ignored_result = Self::handle_message(&mut attachments, attached_message); - }, + PartType::Message(attached_message) => { + let ignored_result = Self::handle_message(&mut attachments, attached_message); + } - PartType::Multipart(multi_part_ids) => { - Self::handle_multipart( - parsed_message, - &mut multipart_ignored_alternative, - part, - multi_part_ids, - ); - }, - } - } + PartType::Multipart(multi_part_ids) => { + Self::handle_multipart( + parsed_message, + &mut multipart_ignored_alternative, + part, + multi_part_ids, + ); + } + } + } - Ok((email_body_as_html, attachments)) - } + Ok((email_body_as_html, attachments)) + } fn is_plain_text(part: &MessagePart) -> bool { part.content_type() @@ -269,456 +269,456 @@ impl ImportableMail { || (!email_body_as_html.is_empty() && part.content_id().is_some()) } - fn get_filename(part: &MessagePart, fallback_name: &str) -> String { - let content_disposition_filename = part - .content_disposition() - .map(|c| c.attribute("filename").map(ToString::to_string)) - .flatten(); - let content_type_filename = part - .content_type() - .map(|c| c.attribute("name").map(ToString::to_string)) - .flatten(); - - let file_name = content_disposition_filename.unwrap_or_else(|| { - content_type_filename.unwrap_or_else(|| { - let filename_suffix = part - .content_type() - .map(Self::get_suffix_from_content_type) - .unwrap_or_default(); - fallback_name.to_string() + filename_suffix - }) - }); - Self::escape_filename(&file_name).to_string() - } - - /// Creates a filename from the given filename that is valid on Linux and Windows. Invalid - /// characters are replaced by "_" - fn escape_filename(file_name: &str) -> Cow { - let regex = Regex::new("[\\\\/:*?<>\"|]").unwrap(); - regex.replace(file_name, "_") - } - - fn get_suffix_from_content_type(content_type: &ContentType) -> &'static str { - if content_type.c_type == "message" { - if content_type.subtype() == Some("rfc822") { - ".eml" - } else { - ".txt" - } - } else if content_type.c_type == "text" { - if content_type.subtype() == Some("calendar") { - ".ics" - } else { - ".txt" - } - } else { - "" - } - } - - fn handle_multipart( - parsed_message: &mail_parser::Message, - multipart_ignored_alternative: &mut HashSet, - part: &MessagePart, - multi_part_ids: &Vec, - ) { - let is_multipart_alternative = part - .content_type() - .map(|content_type| { + fn get_filename(part: &MessagePart, fallback_name: &str) -> String { + let content_disposition_filename = part + .content_disposition() + .map(|c| c.attribute("filename").map(ToString::to_string)) + .flatten(); + let content_type_filename = part + .content_type() + .map(|c| c.attribute("name").map(ToString::to_string)) + .flatten(); + + let file_name = content_disposition_filename.unwrap_or_else(|| { + content_type_filename.unwrap_or_else(|| { + let filename_suffix = part + .content_type() + .map(Self::get_suffix_from_content_type) + .unwrap_or_default(); + fallback_name.to_string() + filename_suffix + }) + }); + Self::escape_filename(&file_name).to_string() + } + + /// Creates a filename from the given filename that is valid on Linux and Windows. Invalid + /// characters are replaced by "_" + fn escape_filename(file_name: &str) -> Cow { + let regex = Regex::new("[\\\\/:*?<>\"|]").unwrap(); + regex.replace(file_name, "_") + } + + fn get_suffix_from_content_type(content_type: &ContentType) -> &'static str { + if content_type.c_type == "message" { + if content_type.subtype() == Some("rfc822") { + ".eml" + } else { + ".txt" + } + } else if content_type.c_type == "text" { + if content_type.subtype() == Some("calendar") { + ".ics" + } else { + ".txt" + } + } else { + "" + } + } + + fn handle_multipart( + parsed_message: &mail_parser::Message, + multipart_ignored_alternative: &mut HashSet, + part: &MessagePart, + multi_part_ids: &Vec, + ) { + let is_multipart_alternative = part + .content_type() + .map(|content_type| { content_type.c_type == "multipart" && content_type.subtype() == Some("alternative") - }) - .unwrap_or_default(); - - if !is_multipart_alternative { - // we can only take care of multipart/alternative - // what to do for other multipart/* - return; - - // edu: https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html - // The primary subtype for multipart, "mixed", is intended for use when the body parts - // are independent and intended to be displayed serially. Any multipart subtypes that - // an implementation does not recognize should be treated as being of subtype "mixed". - } - - let mut best_alternative_yet = None; - for multipart_id in multi_part_ids { - // if this part was already ignored, - if multipart_ignored_alternative.contains(multipart_id) { - continue; - } - - let alternative_part = parsed_message - .part(*multipart_id) - .expect("Expected multipart part to be there?"); - - // for now, we can only decide between alternative between text/plain and text/html - let alternative_content_type = alternative_part - .content_type() - .expect("All multipart alternative should have a Content-Type header"); - - // todo: handle other content type. example: choosing one image from list of alternatives? - let is_text_plain = alternative_content_type.c_type == "text" - && alternative_content_type.subtype() == Some("plain"); - let is_text_html = alternative_content_type.c_type == "text" - && alternative_content_type.subtype() == Some("html"); - - if is_text_plain { - // always ignore plain. we can display html everytime - multipart_ignored_alternative.insert(*multipart_id); - } else if is_text_html { - // if we found a html, this is what we will select. - // if we had found and html already, we will still choose the new one. - // and insert the last one to ignored list - if let Some(last_choice) = best_alternative_yet { - multipart_ignored_alternative.insert(last_choice); - } - best_alternative_yet = Some(*multipart_id); - } else { - // "Can only choose multipart/alternative between text/plain and text/html" - // todo: this is not a good case - if let Some(last_choice) = best_alternative_yet { - multipart_ignored_alternative.insert(last_choice); - } - best_alternative_yet = Some(*multipart_id); - } - } - - // if we did not find any alternative, we will take the last one, - // don't have to do anything with chosen multipart, - // it will anyway be included in next iteration - if best_alternative_yet.is_none() { - let last_choice = multi_part_ids - .last() - .expect("Wait. how can i choose between empty sets of alternatives?"); - - // do we remove the last_choice from ignored list? - // the problem is: - // will the same alternative part can be referenced by multiple multipart block? - // if so, if we remove last_choice now, and this was also ignored by another multipart, - // we will display it anyhow. probably this is right, right? - assert!( - multipart_ignored_alternative.remove(last_choice), - "if we did not put last_choice in ignore list. why best_alternative_yet is none?" - ); - } - - // ps: we assume that the order is: - // multipart block should always come before all it's alternative - } - - fn handle_binary( - part: &MessagePart, - attachments: &mut Vec, - content: Vec, - is_inline: bool, - ) { - let content_id = part.content_id().map(ToString::to_string); - let filename = Self::get_filename(part, "unknown"); - let content_type = part - .content_type() - .map(MakeString::make_string) - .map(Cow::into_owned) - .unwrap_or_else(|| Self::default_content_type().make_string().into_owned()) - .to_string(); - - let content = content.to_vec(); - let attachment = ImportableMailAttachment { - filename, - content_type, - content_id, - is_inline, - content, - }; - - attachments.push(attachment); - } - - fn handle_message( - attachments: &mut Vec, - message: &Message, - ) -> Result<(), MailParseError> { - let filename = - Self::get_filename(&message.parts[0], &message.subject().unwrap_or("unknown")); - let content_type = message - .content_type() + }) + .unwrap_or_default(); + + if !is_multipart_alternative { + // we can only take care of multipart/alternative + // what to do for other multipart/* + return; + + // edu: https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html + // The primary subtype for multipart, "mixed", is intended for use when the body parts + // are independent and intended to be displayed serially. Any multipart subtypes that + // an implementation does not recognize should be treated as being of subtype "mixed". + } + + let mut best_alternative_yet = None; + for multipart_id in multi_part_ids { + // if this part was already ignored, + if multipart_ignored_alternative.contains(multipart_id) { + continue; + } + + let alternative_part = parsed_message + .part(*multipart_id) + .expect("Expected multipart part to be there?"); + + // for now, we can only decide between alternative between text/plain and text/html + let alternative_content_type = alternative_part + .content_type() + .expect("All multipart alternative should have a Content-Type header"); + + // todo: handle other content type. example: choosing one image from list of alternatives? + let is_text_plain = alternative_content_type.c_type == "text" + && alternative_content_type.subtype() == Some("plain"); + let is_text_html = alternative_content_type.c_type == "text" + && alternative_content_type.subtype() == Some("html"); + + if is_text_plain { + // always ignore plain. we can display html everytime + multipart_ignored_alternative.insert(*multipart_id); + } else if is_text_html { + // if we found a html, this is what we will select. + // if we had found and html already, we will still choose the new one. + // and insert the last one to ignored list + if let Some(last_choice) = best_alternative_yet { + multipart_ignored_alternative.insert(last_choice); + } + best_alternative_yet = Some(*multipart_id); + } else { + // "Can only choose multipart/alternative between text/plain and text/html" + // todo: this is not a good case + if let Some(last_choice) = best_alternative_yet { + multipart_ignored_alternative.insert(last_choice); + } + best_alternative_yet = Some(*multipart_id); + } + } + + // if we did not find any alternative, we will take the last one, + // don't have to do anything with chosen multipart, + // it will anyway be included in next iteration + if best_alternative_yet.is_none() { + let last_choice = multi_part_ids + .last() + .expect("Wait. how can i choose between empty sets of alternatives?"); + + // do we remove the last_choice from ignored list? + // the problem is: + // will the same alternative part can be referenced by multiple multipart block? + // if so, if we remove last_choice now, and this was also ignored by another multipart, + // we will display it anyhow. probably this is right, right? + assert!( + multipart_ignored_alternative.remove(last_choice), + "if we did not put last_choice in ignore list. why best_alternative_yet is none?" + ); + } + + // ps: we assume that the order is: + // multipart block should always come before all it's alternative + } + + fn handle_binary( + part: &MessagePart, + attachments: &mut Vec, + content: Vec, + is_inline: bool, + ) { + let content_id = part.content_id().map(ToString::to_string); + let filename = Self::get_filename(part, "unknown"); + let content_type = part + .content_type() + .map(MakeString::make_string) + .map(Cow::into_owned) + .unwrap_or_else(|| Self::default_content_type().make_string().into_owned()) + .to_string(); + + let content = content.to_vec(); + let attachment = ImportableMailAttachment { + filename, + content_type, + content_id, + is_inline, + content, + }; + + attachments.push(attachment); + } + + fn handle_message( + attachments: &mut Vec, + message: &Message, + ) -> Result<(), MailParseError> { + let filename = + Self::get_filename(&message.parts[0], &message.subject().unwrap_or("unknown")); + let content_type = message + .content_type() .ok_or_else(|| Self::default_content_type()) - .map(MakeString::make_string) - .unwrap_or_default() - .to_string(); - - let nested_part = &message.parts[0]; - let content = - message.raw_message[nested_part.offset_header..nested_part.offset_end].to_vec(); - let attachment = ImportableMailAttachment { - filename, - content_type, - content, - is_inline: false, - content_id: None, - }; - attachments.push(attachment); - Ok(()) - } - - fn default_content_type() -> ContentType<'static> { - let default_content_type = ContentType { - c_type: Cow::Borrowed("text"), - c_subtype: Some(Cow::Borrowed("plain")), - attributes: Some(vec![(Cow::Borrowed("charset"), Cow::Borrowed("us-ascii"))]), - }; - default_content_type - } + .map(MakeString::make_string) + .unwrap_or_default() + .to_string(); + + let nested_part = &message.parts[0]; + let content = + message.raw_message[nested_part.offset_header..nested_part.offset_end].to_vec(); + let attachment = ImportableMailAttachment { + filename, + content_type, + content, + is_inline: false, + content_id: None, + }; + attachments.push(attachment); + Ok(()) + } + + fn default_content_type() -> ContentType<'static> { + let default_content_type = ContentType { + c_type: Cow::Borrowed("text"), + c_subtype: Some(Cow::Borrowed("plain")), + attributes: Some(vec![(Cow::Borrowed("charset"), Cow::Borrowed("us-ascii"))]), + }; + default_content_type + } } -impl From for ImportMailData { - fn from(importable_mail: ImportableMail) -> Self { - let ImportableMail { - headers_string: headers, - subject, - html_body_text, - different_envelope_sender, - from_addresses, - cc_addresses, - bcc_addresses, - to_addresses, - date, - reply_to_addresses, - ical_type, - reply_type, - mail_state, - is_phishing, - unread, - message_id, - in_reply_to, - references, - attachments: _attachments, - } = importable_mail; - - let date = date.unwrap_or_else(|| DateTime::from_system_time(SystemTime::now())); - - let reply_tos = reply_to_addresses - .into_iter() - .map(|reply_to| EncryptedMailAddress { - _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), - _finalIvs: Default::default(), - name: reply_to.name, - address: reply_to.mail_address, - }) - .collect(); - - let bcc_addresses = bcc_addresses.into_iter().map(Into::into).collect(); - let cc_addresses = cc_addresses.into_iter().map(Into::into).collect(); - let to_addresses = to_addresses.into_iter().map(Into::into).collect(); - let from_addresses: Vec = from_addresses.into_iter().map(Into::into).collect(); - - let references = references - .into_iter() - .map(|reference| ImportMailDataMailReference { - _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), - reference, - }) - .collect(); - - ImportMailData { - _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), - _finalIvs: HashMap::new(), - compressedHeaders: headers, - subject, - compressedBodyText: html_body_text, - differentEnvelopeSender: different_envelope_sender, - sender: from_addresses - .first() - .cloned() - .unwrap_or(MailContact::default().into()), - recipients: Recipients { - _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), - bccRecipients: bcc_addresses, - ccRecipients: cc_addresses, - toRecipients: to_addresses, - }, - replyTos: reply_tos, - unread, - confidential: false, - method: ical_type as i64, - phishingStatus: if is_phishing { 1 } else { 0 }, - replyType: reply_type as i64, - date, - state: mail_state as i64, - messageId: message_id, - inReplyTo: in_reply_to, - references, - importedAttachments: vec![], - } - } +impl From for (ImportMailData, Vec) { + fn from(importable_mail: ImportableMail) -> Self { + let ImportableMail { + headers_string: headers, + subject, + html_body_text, + different_envelope_sender, + from_addresses, + cc_addresses, + bcc_addresses, + to_addresses, + date, + reply_to_addresses, + ical_type, + reply_type, + mail_state, + is_phishing, + unread, + message_id, + in_reply_to, + references, + attachments, + } = importable_mail; + + let date = date.unwrap_or_else(|| DateTime::from_system_time(SystemTime::now())); + + let reply_tos = reply_to_addresses + .into_iter() + .map(|reply_to| EncryptedMailAddress { + _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), + _finalIvs: Default::default(), + name: reply_to.name, + address: reply_to.mail_address, + }) + .collect(); + + let bcc_addresses = bcc_addresses.into_iter().map(Into::into).collect(); + let cc_addresses = cc_addresses.into_iter().map(Into::into).collect(); + let to_addresses = to_addresses.into_iter().map(Into::into).collect(); + let from_addresses: Vec = from_addresses.into_iter().map(Into::into).collect(); + + let references = references + .into_iter() + .map(|reference| ImportMailDataMailReference { + _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), + reference, + }) + .collect(); + + (ImportMailData { + _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), + _finalIvs: HashMap::new(), + compressedHeaders: headers, + subject, + compressedBodyText: html_body_text, + differentEnvelopeSender: different_envelope_sender, + sender: from_addresses + .first() + .cloned() + .unwrap_or(MailContact::default().into()), + recipients: Recipients { + _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), + bccRecipients: bcc_addresses, + ccRecipients: cc_addresses, + toRecipients: to_addresses, + }, + replyTos: reply_tos, + unread, + confidential: false, + method: ical_type as i64, + phishingStatus: if is_phishing { 1 } else { 0 }, + replyType: reply_type as i64, + date, + state: mail_state as i64, + messageId: message_id, + inReplyTo: in_reply_to, + references, + importedAttachments: vec![], + }, attachments) + } } impl TryFrom for ImportableMail { - type Error = MailParseError; - fn try_from(imap_mail: ImapMail) -> Result { - let ImapMail { rfc822_full } = imap_mail; + type Error = MailParseError; + fn try_from(imap_mail: ImapMail) -> Result { + let ImapMail { rfc822_full } = imap_mail; - // parse the full mime message - let imap_mail = MessageParser::new() - .parse(rfc822_full.as_slice()) - .ok_or(MailParseError::InvalidMimeMessage)?; + // parse the full mime message + let imap_mail = MessageParser::new() + .parse(rfc822_full.as_slice()) + .ok_or(MailParseError::InvalidMimeMessage)?; - let mut importable_mail = Self::try_from(&imap_mail).unwrap(); + let mut importable_mail = Self::try_from(&imap_mail).unwrap(); - // example: - // add more details from imap if given, - importable_mail.is_phishing = false; - importable_mail.unread = true; + // example: + // add more details from imap if given, + importable_mail.is_phishing = false; + importable_mail.unread = true; - Ok(importable_mail) - } + Ok(importable_mail) + } } #[derive(Debug, Clone, PartialEq)] pub enum MailParseError { - InconsistentParts(&'static str), - NoSentDate, - NoRecipient, - NoFrom, - InvalidDate, - InvalidHtmlBody, - InvalidTextBody, - InvalidMimeMessage, - EmptyMailAddress, - Unknown(String), + InconsistentParts(&'static str), + NoSentDate, + NoRecipient, + NoFrom, + InvalidDate, + InvalidHtmlBody, + InvalidTextBody, + InvalidMimeMessage, + EmptyMailAddress, + Unknown(String), } /// allow to convert from parsed message impl<'x> TryFrom<&mail_parser::Message<'x>> for ImportableMail { - type Error = MailParseError; - - fn try_from(parsed_message: &mail_parser::Message) -> Result { - let subject = parsed_message.subject().unwrap_or_default().to_string(); - - let (html_body_text, attachments) = ImportableMail::process_all_parts(&parsed_message)?; - - let date = parsed_message - .date() - .as_ref() - .map(|date_time| DateTime::from_millis(date_time.to_timestamp() as u64 * 1000)); - - let from_addresses = ImportableMail::map_to_tuta_mail_address( - parsed_message.from().map(Cow::Borrowed).unwrap_or_else(|| { - parsed_message - .sender() - .map(Cow::Borrowed) - .unwrap_or_else(|| Cow::Owned(mail_parser::Address::List(vec![]))) - }), - ) - .into_iter() - .map(|mut address| { - // we currently use the name as address if no address was defined on server side - if address.mail_address.is_empty() { - address.mail_address = address.name; - address.name = String::new(); - } - address - }) - .collect::>(); - - let different_envelope_sender = parsed_message - .sender() - .map(|sender| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(sender))) - // sender is allowed to be empty - .unwrap_or_default() - // there should only be one different envelope sender - .pop() - .map(|mut address| { - // we currently use the name as address if no address was defined on server side - if address.mail_address.is_empty() { - address.mail_address = address.name; - address.name = String::new(); - } - address - }) - // different envelope sender should not contain address listed in from_addresses; - .filter(|diff_sender| { - from_addresses + type Error = MailParseError; + + fn try_from(parsed_message: &mail_parser::Message) -> Result { + let subject = parsed_message.subject().unwrap_or_default().to_string(); + + let (html_body_text, attachments) = ImportableMail::process_all_parts(&parsed_message)?; + + let date = parsed_message + .date() + .as_ref() + .map(|date_time| DateTime::from_millis(date_time.to_timestamp() as u64 * 1000)); + + let from_addresses = ImportableMail::map_to_tuta_mail_address( + parsed_message.from().map(Cow::Borrowed).unwrap_or_else(|| { + parsed_message + .sender() + .map(Cow::Borrowed) + .unwrap_or_else(|| Cow::Owned(mail_parser::Address::List(vec![]))) + }), + ) + .into_iter() + .map(|mut address| { + // we currently use the name as address if no address was defined on server side + if address.mail_address.is_empty() { + address.mail_address = address.name; + address.name = String::new(); + } + address + }) + .collect::>(); + + let different_envelope_sender = parsed_message + .sender() + .map(|sender| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(sender))) + // sender is allowed to be empty + .unwrap_or_default() + // there should only be one different envelope sender + .pop() + .map(|mut address| { + // we currently use the name as address if no address was defined on server side + if address.mail_address.is_empty() { + address.mail_address = address.name; + address.name = String::new(); + } + address + }) + // different envelope sender should not contain address listed in from_addresses; + .filter(|diff_sender| { + from_addresses .iter() .any(|from| from.mail_address != diff_sender.mail_address) - }) - .map(|mail_address| mail_address.mail_address); - - let to_addresses = parsed_message - .to() - .map(|to| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(to))) - .unwrap_or_default() - .into_iter() - .filter(|address| !address.mail_address.trim().is_empty()) - .collect(); - - let cc_addresses = parsed_message - .cc() - .map(|cc| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(cc))) - .unwrap_or_default() - .into_iter() - .filter(|address| !address.mail_address.trim().is_empty()) - .collect(); - - let bcc_addresses = parsed_message - .bcc() - .map(|bcc| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(bcc))) - .unwrap_or_default() - .into_iter() - .filter(|address| !address.mail_address.trim().is_empty()) - .collect(); - - let reply_to_addresses = parsed_message - .reply_to() - .map(|reply_to| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(reply_to))) - .unwrap_or_default() - .into_iter() - .filter(|address| !address.mail_address.trim().is_empty()) - .collect(); - - let headers_string = parsed_message - .headers_raw() - .map(|(name, value)| name.to_string() + ":" + value) - .collect::>() - .join(""); - - let reply_type = extend_mail_parser::get_reply_type_from_headers(parsed_message.headers()); - let message_id = parsed_message.message_id().map(String::from); - let in_reply_to = parsed_message.in_reply_to().as_text().map(String::from); - let references = match parsed_message.references() { - HeaderValue::Text(reference) => { - vec![reference.to_string()] - }, - HeaderValue::TextList(references) => { - references.iter().map(|cow| cow.to_string()).collect() - }, - _ => { - vec![] - }, - }; - - Ok(Self { - headers_string, - html_body_text, - subject, - different_envelope_sender, - from_addresses, - to_addresses, - cc_addresses, - bcc_addresses, - reply_to_addresses, - date, - reply_type, - message_id, - in_reply_to, - references, - attachments, - - ical_type: Default::default(), - unread: false, - mail_state: Default::default(), - is_phishing: false, - }) - } + }) + .map(|mail_address| mail_address.mail_address); + + let to_addresses = parsed_message + .to() + .map(|to| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(to))) + .unwrap_or_default() + .into_iter() + .filter(|address| !address.mail_address.trim().is_empty()) + .collect(); + + let cc_addresses = parsed_message + .cc() + .map(|cc| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(cc))) + .unwrap_or_default() + .into_iter() + .filter(|address| !address.mail_address.trim().is_empty()) + .collect(); + + let bcc_addresses = parsed_message + .bcc() + .map(|bcc| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(bcc))) + .unwrap_or_default() + .into_iter() + .filter(|address| !address.mail_address.trim().is_empty()) + .collect(); + + let reply_to_addresses = parsed_message + .reply_to() + .map(|reply_to| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(reply_to))) + .unwrap_or_default() + .into_iter() + .filter(|address| !address.mail_address.trim().is_empty()) + .collect(); + + let headers_string = parsed_message + .headers_raw() + .map(|(name, value)| name.to_string() + ":" + value) + .collect::>() + .join(""); + + let reply_type = extend_mail_parser::get_reply_type_from_headers(parsed_message.headers()); + let message_id = parsed_message.message_id().map(String::from); + let in_reply_to = parsed_message.in_reply_to().as_text().map(String::from); + let references = match parsed_message.references() { + HeaderValue::Text(reference) => { + vec![reference.to_string()] + } + HeaderValue::TextList(references) => { + references.iter().map(|cow| cow.to_string()).collect() + } + _ => { + vec![] + } + }; + + Ok(Self { + headers_string, + html_body_text, + subject, + different_envelope_sender, + from_addresses, + to_addresses, + cc_addresses, + bcc_addresses, + reply_to_addresses, + date, + reply_type, + message_id, + in_reply_to, + references, + attachments, + + ical_type: Default::default(), + unread: false, + mail_state: Default::default(), + is_phishing: false, + }) + } } #[cfg(test)] diff --git a/src/common/api/entities/tutanota/ModelInfo.ts b/src/common/api/entities/tutanota/ModelInfo.ts index 74c36142193..03cfc89d148 100644 --- a/src/common/api/entities/tutanota/ModelInfo.ts +++ b/src/common/api/entities/tutanota/ModelInfo.ts @@ -1,6 +1,6 @@ const modelInfo = { version: 77, - compatibleSince: 76, + compatibleSince: 77, } export default modelInfo \ No newline at end of file diff --git a/src/common/api/worker/offline/OfflineStorageMigrator.ts b/src/common/api/worker/offline/OfflineStorageMigrator.ts index 17c8be8e00b..f5d7723a743 100644 --- a/src/common/api/worker/offline/OfflineStorageMigrator.ts +++ b/src/common/api/worker/offline/OfflineStorageMigrator.ts @@ -31,6 +31,7 @@ import { tutanota75 } from "./migrations/tutanota-v75.js" import { sys111 } from "./migrations/sys-v111.js" import { tutanota76 } from "./migrations/tutanota-v76.js" import { sys112 } from "./migrations/sys-v112.js" +import { tutanotaV77 } from "./migrations/tutanota-v77.js" export interface OfflineMigration { readonly app: VersionMetadataBaseKey @@ -74,6 +75,7 @@ export const OFFLINE_STORAGE_MIGRATIONS: ReadonlyArray = [ sys111, tutanota76, sys112, + tutanotaV77, ] const CURRENT_OFFLINE_VERSION = 1 diff --git a/src/common/api/worker/offline/migrations/tutanota-v77.ts b/src/common/api/worker/offline/migrations/tutanota-v77.ts new file mode 100644 index 00000000000..32da75f31b3 --- /dev/null +++ b/src/common/api/worker/offline/migrations/tutanota-v77.ts @@ -0,0 +1,13 @@ +import { OfflineMigration } from "../OfflineStorageMigrator.js" +import { OfflineStorage } from "../OfflineStorage.js" +import { addValue, migrateAllElements } from "../StandardMigrations" +import { MailBoxTypeRef } from "../../../entities/tutanota/TypeRefs" + +export const tutanotaV77: OfflineMigration = { + app: "tutanota", + version: 77, + async migrate(storage: OfflineStorage) { + // TODO think about offline migration for Mailbox#importedAttachments + await migrateAllElements(MailBoxTypeRef, storage, [addValue("importedAttachments", "")]) + }, +} From 3ffcd6e382957ebff71f2c109ee3907c07f2413a Mon Sep 17 00:00:00 2001 From: jhm <17314077+jomapp@users.noreply.github.com> Date: Wed, 13 Nov 2024 18:32:01 +0100 Subject: [PATCH 23/32] make ChunkingIterator thread-safe --- packages/node-mimimi/src/importer.rs | 185 +++++++++---------- packages/node-mimimi/src/reduce_to_chunks.rs | 180 +++++++++--------- 2 files changed, 183 insertions(+), 182 deletions(-) diff --git a/packages/node-mimimi/src/importer.rs b/packages/node-mimimi/src/importer.rs index a0329f88e9b..094b50ae4ff 100644 --- a/packages/node-mimimi/src/importer.rs +++ b/packages/node-mimimi/src/importer.rs @@ -86,61 +86,61 @@ pub enum IterationError { } struct ImportSourceIterator { - // it would be nice to not need the mutex, but when the importer continues the import, + // it would be nice to not need the mutex, but when the importer continues the import, // it mutates its own state and also calls mutating functions on the source. solving this - // probably requires a bigger restructure of the code (it's very OOP atm) - source: Arc>, + // probably requires a bigger restructure of the code (it's very OOP atm) + source: Arc>, } impl Iterator for ImportSourceIterator { - type Item = ImportableMail; - - fn next(&mut self) -> Option { - let mut source = self.source.lock().unwrap(); - let next_importable_mail = match &mut *source { - // the other way (converting fs_source to an async_iterator) would be nicer, but that's a nightly feature - ImportSource::RemoteImap { imap_import_client } => imap_import_client - .fetch_next_mail() - .map_err(IterationError::Imap), - ImportSource::LocalFile { fs_email_client } => fs_email_client - .get_next_importable_mail() - .map_err(IterationError::File), - }; - - match next_importable_mail { - Ok(next_importable_mail) => Some(next_importable_mail), - - // source says, all the iteration have ended, - Err(IterationError::File(FileIterationError::SourceEnd)) - | Err(IterationError::Imap(ImapIterationError::SourceEnd)) => None, - - Err(e) => { - // once we handle this case we will need another iterator that filters (and logs) the - // errors so we don't have to handle the error case during the chunking + upload - panic!("Cannot get next email from source: {e:?}") - }, - } - } + type Item = ImportableMail; + + fn next(&mut self) -> Option { + let mut source = self.source.lock().unwrap(); + let next_importable_mail = match &mut *source { + // the other way (converting fs_source to an async_iterator) would be nicer, but that's a nightly feature + ImportSource::RemoteImap { imap_import_client } => imap_import_client + .fetch_next_mail() + .map_err(IterationError::Imap), + ImportSource::LocalFile { fs_email_client } => fs_email_client + .get_next_importable_mail() + .map_err(IterationError::File), + }; + + match next_importable_mail { + Ok(next_importable_mail) => Some(next_importable_mail), + + // source says, all the iteration have ended, + Err(IterationError::File(FileIterationError::SourceEnd)) + | Err(IterationError::Imap(ImapIterationError::SourceEnd)) => None, + + Err(e) => { + // once we handle this case we will need another iterator that filters (and logs) the + // errors so we don't have to handle the error case during the chunking + upload + panic!("Cannot get next email from source: {e:?}") + } + } + } } impl Importer { - pub async fn continue_import(&mut self) -> Result { - let source_iterator = ImportSourceIterator { - source: Arc::clone(&self.import_source), - }; - let _ = self.import_all_mail(source_iterator).await; - Ok(self.status.clone()) - } + pub async fn continue_import(&mut self) -> Result { + let source_iterator = ImportSourceIterator { + source: Arc::clone(&self.import_source), + }; + let _ = self.import_all_mail(source_iterator).await; + Ok(self.status.clone()) + } /// once we get the ImportableMail from either of source, /// continue to the uploading counterpart async fn import_all_mail( &mut self, - importable_mails: Iter, - ) -> Result, ()> - where - Iter: Iterator, - { + importable_mails: Iter, + ) -> Result, ()> + where + Iter: Iterator + Send + 'static, + { let new_mail_aes_256_key = GenericAesKey::from_bytes( self.randomizer_facade .generate_random_array::<{ tutasdk::crypto::aes::AES_256_KEY_SIZE }>() @@ -156,13 +156,12 @@ impl Importer { mail_group_key.encrypt_key(&new_mail_aes_256_key, Iv::generate(&self.randomizer_facade)); const MAX_REQUEST_SIZE: usize = 1024 * 1024 * 5; - let import_mail_data_and_attachments = importable_mails.map(<(ImportMailData, Vec)>::from); + let import_mail_data_and_attachments = importable_mails.map(<(ImportMailData, Vec)>::from); let import_chunks = reduce_to_chunks( import_mail_data_and_attachments, MAX_REQUEST_SIZE, Box::new(|(imd, a)| estimate_json_size(imd)), - ) - ; + ); let mut mails: Vec = Vec::new(); let mut new_status = ImportStatus { @@ -229,57 +228,57 @@ impl Importer { let import_mail_post_in = ImportMailPostIn { ownerEncSessionKey: owner_enc_mail_session_key.object.clone(), - ownerGroup: self.target_owner_group.clone(), - ownerKeyVersion: owner_enc_mail_session_key.version, - imports: imports_with_attachments, - targetMailFolder: self.target_mail_folder.clone(), - _format: 0, - _errors: None, - _finalIvs: Default::default(), - }; - - let service_params = ExtraServiceParams { - session_key: Some(new_mail_aes_256_key.clone()), - ..Default::default() - }; - - let response = self - .logged_in_sdk - .get_service_executor() - .post::(import_mail_post_in, service_params) - .await; - - match response { - // this import has been success, - Ok(mut imported_post_out) => { - mails.append(&mut imported_post_out.mails); - new_status = ImportStatus { - state: ImportState::Running, - imported_mails: self - .status - .imported_mails - .saturating_add(u32::try_from(import_len).unwrap_or(u32::MAX)), - }; + ownerGroup: self.target_owner_group.clone(), + ownerKeyVersion: owner_enc_mail_session_key.version, + imports: imports_with_attachments, + targetMailFolder: self.target_mail_folder.clone(), + _format: 0, + _errors: None, + _finalIvs: Default::default(), + }; + + let service_params = ExtraServiceParams { + session_key: Some(new_mail_aes_256_key.clone()), + ..Default::default() + }; + + let response = self + .logged_in_sdk + .get_service_executor() + .post::(import_mail_post_in, service_params) + .await; + + match response { + // this import has been success, + Ok(mut imported_post_out) => { + mails.append(&mut imported_post_out.mails); + new_status = ImportStatus { + state: ImportState::Running, + imported_mails: self + .status + .imported_mails + .saturating_add(u32::try_from(import_len).unwrap_or(u32::MAX)), + }; }, Err(_) => { - // todo: save the ImportableMails to some fail list, - // since, in this iteration the source will not give these mail again, - new_status = ImportStatus { - state: ImportState::Postponed, - imported_mails: self.status.imported_mails, - }; + // todo: save the ImportableMails to some fail list, + // since, in this iteration the source will not give these mail again, + new_status = ImportStatus { + state: ImportState::Postponed, + imported_mails: self.status.imported_mails, + }; }, - } - } - new_status.state = if new_status.state == ImportState::Postponed { - ImportState::Postponed - } else { - ImportState::Finished - }; - - self.status = new_status; - Ok(mails) + } + } + new_status.state = if new_status.state == ImportState::Postponed { + ImportState::Postponed + } else { + ImportState::Finished + }; + + self.status = new_status; + Ok(mails) } } diff --git a/packages/node-mimimi/src/reduce_to_chunks.rs b/packages/node-mimimi/src/reduce_to_chunks.rs index fd9f9ca0b02..bd31efe125c 100644 --- a/packages/node-mimimi/src/reduce_to_chunks.rs +++ b/packages/node-mimimi/src/reduce_to_chunks.rs @@ -3,113 +3,115 @@ use std::ops::Deref; struct ChunkingIterator where - Inner: Iterator, + Inner: Iterator + Send, + Element: Send, { - inner: Peekable, - max_size: usize, - sizer: Box usize>, + inner: Peekable, + max_size: usize, + sizer: Box usize + Send>, } impl Iterator for ChunkingIterator where - Inner: Iterator, + Inner: Iterator + Send, + Element: Send, { - type Item = Vec; - fn next(&mut self) -> Option { - let seq = &mut self.inner; - let mut element = seq.peek()?; + type Item = Vec; + fn next(&mut self) -> Option { + let seq = &mut self.inner; + let mut element = seq.peek()?; - let mut chunk: Vec = Vec::new(); - let mut current_chunk_size = 0_usize; - loop { - let element_size = self.sizer.deref()(element); - if element_size > self.max_size { - // this element is too big for one chunk. we might just ignore that and make a - // one-element chunk that fails to upload, or we stop iteration here. - // this discards any elements already in the chunk - return None; - } - let new_chunk_size = current_chunk_size.saturating_add(element_size); - if new_chunk_size > self.max_size { - // chunk is full - this element goes into the next chunk. - // because we used peek() it'll still be available for the next call to this function. - return Some(chunk); - } else { - current_chunk_size = new_chunk_size; - chunk.push( - seq.next() - .expect("got None from next even though peek() gave Some"), - ); - element = match seq.peek() { - None => break, - Some(e) => e, - }; - } - } - Some(chunk) - } + let mut chunk: Vec = Vec::new(); + let mut current_chunk_size = 0_usize; + loop { + let element_size = self.sizer.deref()(element); + if element_size > self.max_size { + // this element is too big for one chunk. we might just ignore that and make a + // one-element chunk that fails to upload, or we stop iteration here. + // this discards any elements already in the chunk + return None; + } + let new_chunk_size = current_chunk_size.saturating_add(element_size); + if new_chunk_size > self.max_size { + // chunk is full - this element goes into the next chunk. + // because we used peek() it'll still be available for the next call to this function. + return Some(chunk); + } else { + current_chunk_size = new_chunk_size; + chunk.push( + seq.next() + .expect("got None from next even though peek() gave Some"), + ); + element = match seq.peek() { + None => break, + Some(e) => e, + }; + } + } + Some(chunk) + } } /// split a given vector of elements into a vector of chunks not exceeding max_size, where the /// chunks size is calculated by summing up the elements sizes as given by the sizer function. /// /// the number of chunks is not guaranteed to be optimal. -pub fn reduce_to_chunks<'element, Element: 'element>( - seq: impl Iterator, - max_size: usize, - sizer: Box usize>, -) -> impl Iterator> { - ChunkingIterator { - inner: seq.peekable(), - max_size, - sizer, - } +pub fn reduce_to_chunks<'element, Element: 'element + Send>( + seq: impl Iterator + Send, + max_size: usize, + sizer: Box usize>, +) -> impl Iterator> + Send { + ChunkingIterator { + inner: seq.peekable(), + max_size, + sizer, + } } #[cfg(test)] mod tests { - use crate::reduce_to_chunks::reduce_to_chunks; + use crate::reduce_to_chunks::reduce_to_chunks; - #[test] - fn reduce_to_chunks_simple() { - assert_eq!( - vec![vec![1, 2, 3], vec![4], vec![5], vec![6]], - reduce_to_chunks::( - vec![1, 2, 3, 4, 5, 6].into_iter(), - 6, - Box::new(|item| { *item }) - ) - .collect::>>() - ); - } + #[test] + fn reduce_to_chunks_simple() { + assert_eq!( + vec![vec![1, 2, 3], vec![4], vec![5], vec![6]], + reduce_to_chunks::( + vec![1, 2, 3, 4, 5, 6].into_iter(), + 6, + Box::new(|item| { *item }) + ) + .collect::>>() + ); + } - #[test] - fn reduce_to_chunks_no_split() { - assert_eq!( - vec![vec![1, 2, 3, 4, 5, 6],], - reduce_to_chunks::( - vec![1, 2, 3, 4, 5, 6].into_iter(), - 21, - Box::new(|item| { *item }) - ) - .collect::>>() - ); - } + #[test] + fn reduce_to_chunks_no_split() { + assert_eq!( + vec![vec![1, 2, 3, 4, 5, 6], ], + reduce_to_chunks::( + vec![1, 2, 3, 4, 5, 6].into_iter(), + 21, + Box::new(|item| { *item }) + ) + .collect::>>() + ); + } - #[test] - fn reduce_to_chunks_empty() { - assert_eq!( - Vec::>::new(), - reduce_to_chunks::(vec![].into_iter(), 0, Box::new(|item| { *item })) - .collect::>>() - ); - } + #[test] + fn reduce_to_chunks_empty() { + assert_eq!( + Vec::>::new(), + reduce_to_chunks::(vec![].into_iter(), 0, Box::new(|item| { *item })) + .collect::>>() + ); + } - #[test] - fn split_too_big() { - assert_eq!( - Vec::>::new(), - reduce_to_chunks::(vec![1, 10, 11].into_iter(), 2, Box::new(|item| { *item })) - .collect::>>() - ); - } + #[test] + fn split_too_big() { + assert_eq!( + Vec::>::new(), + reduce_to_chunks::(vec![1, 10, 11].into_iter(), 2, Box::new(|item| { *item })) + .collect::>>() + ); + } } From 636d5e6a6a3617c4685e2b22680ebbd065d672e7 Mon Sep 17 00:00:00 2001 From: jhm <17314077+jomapp@users.noreply.github.com> Date: Wed, 13 Nov 2024 18:36:44 +0100 Subject: [PATCH 24/32] use typeRef from dependency if available on association In the case that a association type is from a different app than its parent, we did not use the correct typeRef. Co-authored-by: kib Co-authored-by: nig Co-authored-by: nif --- tuta-sdk/rust/sdk/src/json_serializer.rs | 1540 +++++++++++----------- 1 file changed, 771 insertions(+), 769 deletions(-) diff --git a/tuta-sdk/rust/sdk/src/json_serializer.rs b/tuta-sdk/rust/sdk/src/json_serializer.rs index 72dab00457d..9b4e5d4fed8 100644 --- a/tuta-sdk/rust/sdk/src/json_serializer.rs +++ b/tuta-sdk/rust/sdk/src/json_serializer.rs @@ -9,7 +9,7 @@ use crate::element_value::{ElementValue, ParsedEntity}; use crate::json_element::{JsonElement, RawEntity}; use crate::json_serializer::InstanceMapperError::InvalidValue; use crate::metamodel::{ - AssociationType, Cardinality, ElementType, ModelValue, TypeModel, ValueType, + AssociationType, Cardinality, ElementType, ModelValue, TypeModel, ValueType, }; use crate::type_model_provider::TypeModelProvider; use crate::CustomId; @@ -17,789 +17,791 @@ use crate::GeneratedId; use crate::{IdTupleCustom, IdTupleGenerated, TypeRef}; impl From<&TypeModel> for TypeRef { - fn from(value: &TypeModel) -> Self { - TypeRef { - app: value.app, - type_: value.name, - } - } + fn from(value: &TypeModel) -> Self { + TypeRef { + app: value.app, + type_: value.name, + } + } } /// Provides serialization and deserialization conversion between JSON representations and logical /// types for entities. /// It validates the schema along the way (to some degree). pub struct JsonSerializer { - type_model_provider: Arc, + type_model_provider: Arc, } #[derive(Error, Debug)] pub enum InstanceMapperError { - #[error("InstanceMapperError: Type not found: {type_ref}")] - TypeNotFound { type_ref: TypeRef }, - #[error("InstanceMapperError: Invalid value: {type_ref} {field}")] - InvalidValue { type_ref: TypeRef, field: String }, + #[error("InstanceMapperError: Type not found: {type_ref}")] + TypeNotFound { type_ref: TypeRef }, + #[error("InstanceMapperError: Invalid value: {type_ref} {field}")] + InvalidValue { type_ref: TypeRef, field: String }, } impl JsonSerializer { - pub fn new(type_model_provider: Arc) -> JsonSerializer { - JsonSerializer { - type_model_provider, - } - } - - /// Creates an entity from JSON data - pub fn parse( - &self, - type_ref: &TypeRef, - mut raw_entity: RawEntity, - ) -> Result { - let type_model = self.get_type_model(type_ref)?; - let mut mapped: HashMap = HashMap::new(); - for (&value_name, value_type) in &type_model.values { - // reuse the name - let (value_name, value) = - raw_entity - .remove_entry(value_name) - .ok_or_else(|| InvalidValue { - type_ref: type_ref.clone(), - field: value_name.to_owned(), - })?; - - let mapped_value = match (&value_type.cardinality, value) { - (Cardinality::ZeroOrOne, JsonElement::Null) => ElementValue::Null, - (Cardinality::One, JsonElement::String(v)) if v.is_empty() => { - // Empty string signifies default value for a field. This is primarily the - // case for encrypted fields. This includes manually encrypted fields in some - // cases. - // When the value is encrypted we need to pass on the information about the - // default value, so we keep it as an empty string (see entity_facade.rs). - // Otherwise, we resolve the field to its default value. - if value_type.encrypted { - ElementValue::String(String::new()) - } else { - value_type.value_type.get_default() - } - }, - (Cardinality::One | Cardinality::ZeroOrOne, JsonElement::String(s)) - if value_type.encrypted => - { - ElementValue::Bytes(BASE64_STANDARD.decode(s).map_err(|_| InvalidValue { - type_ref: type_ref.clone(), - field: value_name.clone(), - })?) - }, - (_, value) if !value_type.encrypted => { - self.parse_value(type_model, &value_name, value_type, value)? - }, - _ => { - return Err(InvalidValue { - type_ref: type_ref.clone(), - field: value_name.clone(), - }) - }, - }; - mapped.insert(value_name, mapped_value); - } - - for (&association_name, association_type) in &type_model.associations { - // reuse the name - let (association_name, value) = - raw_entity - .remove_entry(association_name) - .ok_or_else(|| InvalidValue { - type_ref: type_ref.clone(), - field: association_name.to_owned(), - })?; - let association_type_ref = TypeRef { - app: association_type.dependency.unwrap_or(type_ref.app), - type_: association_type.ref_type, - }; - match ( - &association_type.association_type, - &association_type.cardinality, - value, - ) { - ( - AssociationType::Aggregation, - Cardinality::One | Cardinality::ZeroOrOne, - JsonElement::Dict(dict), - ) => { - let parsed = self.parse(&association_type_ref, dict)?; - mapped.insert(association_name, ElementValue::Dict(parsed)); - }, - (AssociationType::Aggregation, Cardinality::Any, JsonElement::Array(elements)) => { - let parsed_aggregates = self.parse_aggregated_array( - &association_name, - &association_type_ref, - elements, - )?; - mapped.insert(association_name, ElementValue::Array(parsed_aggregates)); - }, - (_, Cardinality::ZeroOrOne, JsonElement::Null) => { - mapped.insert(association_name, ElementValue::Null); - }, - ( - AssociationType::ElementAssociation | AssociationType::ListAssociation, - Cardinality::One | Cardinality::ZeroOrOne, - JsonElement::String(id), - ) => { - // FIXME it's not always generated id but it's fine probably - mapped.insert( - association_name, - ElementValue::IdGeneratedId(GeneratedId(id)), - ); - }, - ( - AssociationType::ListElementAssociationGenerated, - Cardinality::One | Cardinality::ZeroOrOne, - JsonElement::Array(vec), - ) => { - let id_tuple = match Self::parse_id_tuple_generated(vec) { - None => { - return Err(InvalidValue { - type_ref: association_type_ref, - field: association_name, - }); - }, - Some(id_tuple) => id_tuple, - }; - mapped.insert( - association_name, - ElementValue::IdTupleGeneratedElementId(id_tuple), - ); - }, - ( - AssociationType::ListElementAssociationCustom, - Cardinality::One | Cardinality::ZeroOrOne, - JsonElement::Array(vec), - ) => { - let id_tuple = match Self::parse_id_tuple_custom(vec) { - None => { - return Err(InvalidValue { - type_ref: association_type_ref, - field: association_name, - }); - }, - Some(id_tuple) => id_tuple, - }; - mapped.insert( - association_name, - ElementValue::IdTupleCustomElementId(id_tuple), - ); - }, - ( - AssociationType::ListElementAssociationGenerated, - Cardinality::Any, - JsonElement::Array(vec), - ) => { - let ids = - self.parse_id_tuple_list_generated(type_ref, &association_name, vec)?; - - mapped.insert(association_name, ElementValue::Array(ids)); - }, - ( - AssociationType::ListElementAssociationCustom, - Cardinality::Any, - JsonElement::Array(vec), - ) => { - let ids = self.parse_id_tuple_list_custom(type_ref, &association_name, vec)?; - - mapped.insert(association_name, ElementValue::Array(ids)); - }, - ( - AssociationType::BlobElementAssociation, - Cardinality::One | Cardinality::ZeroOrOne, - JsonElement::Array(vec), - ) => { - let id_tuple = match Self::parse_id_tuple_generated(vec) { - None => { - return Err(InvalidValue { - type_ref: association_type_ref, - field: association_name, - }); - }, - Some(id_tuple) => id_tuple, - }; - mapped.insert( - association_name, - ElementValue::IdTupleGeneratedElementId(id_tuple), - ); - }, - _ => {}, - } - } - - Ok(mapped) - } - - /// Parses an aggregated array from a value of a JSON object containing an entity/instance - fn parse_aggregated_array( - &self, - association_name: &str, - association_type_ref: &TypeRef, - elements: Vec, - ) -> Result, InstanceMapperError> { - let mut parsed_aggregates = Vec::new(); - for element in elements { - match element { - JsonElement::Dict(a) => { - let parsed = self.parse(association_type_ref, a)?; - parsed_aggregates.push(ElementValue::Dict(parsed)); - }, - _ => { - return Err(InvalidValue { - type_ref: association_type_ref.clone(), - field: association_name.to_owned(), - }); - }, - }; - } - Ok(parsed_aggregates) - } - - fn parse_id_tuple_list_generated( - &self, - outer_type_ref: &TypeRef, - association_name: &str, - elements: Vec, - ) -> Result, InstanceMapperError> { - elements - .into_iter() - .map(|json_element| { - let JsonElement::Array(id_vec) = json_element else { - return Err(InvalidValue { - field: association_name.to_owned(), - type_ref: outer_type_ref.clone(), - }); - }; - let id_tuple = - Self::parse_id_tuple_generated(id_vec).ok_or_else(|| InvalidValue { - field: association_name.to_owned(), - type_ref: outer_type_ref.clone(), - })?; - Ok(ElementValue::IdTupleGeneratedElementId(id_tuple)) - }) - .collect() - } - - fn parse_id_tuple_list_custom( - &self, - outer_type_ref: &TypeRef, - association_name: &str, - elements: Vec, - ) -> Result, InstanceMapperError> { - elements - .into_iter() - .map(|json_element| { - let JsonElement::Array(id_vec) = json_element else { - return Err(InvalidValue { - field: association_name.to_owned(), - type_ref: outer_type_ref.clone(), - }); - }; - let id_tuple = Self::parse_id_tuple_custom(id_vec).ok_or_else(|| InvalidValue { - field: association_name.to_owned(), - type_ref: outer_type_ref.clone(), - })?; - Ok(ElementValue::IdTupleCustomElementId(id_tuple)) - }) - .collect() - } - - /// Transforms an entity/instance into JSON data - pub fn serialize( - &self, - type_ref: &TypeRef, - mut entity: ParsedEntity, - ) -> Result { - let type_model = self.get_type_model(type_ref)?; - let mut mapped: RawEntity = HashMap::new(); - for (&value_name, value_type) in &type_model.values { - // we take out of the map to reuse the names/values - let (value_name, value) = - entity - .remove_entry(value_name) - .ok_or_else(|| InvalidValue { - type_ref: type_ref.clone(), - field: value_name.to_owned(), - })?; - - let serialized_value = - self.serialize_value(type_model, &value_name, value_type, value)?; - mapped.insert(value_name, serialized_value); - } - - for (&association_name, association_type) in &type_model.associations { - let (association_name, value) = - entity - .remove_entry(association_name) - .ok_or_else(|| InvalidValue { - type_ref: type_ref.clone(), - field: association_name.to_owned(), - })?; - let association_type_ref = TypeRef { - app: type_ref.app, - type_: association_type.ref_type, - }; - match ( - &association_type.association_type, - &association_type.cardinality, - value, - ) { - ( - AssociationType::Aggregation, - Cardinality::One | Cardinality::ZeroOrOne, - ElementValue::Dict(dict), - ) => { - let serialized = self.serialize(&association_type_ref, dict)?; - mapped.insert(association_name, JsonElement::Dict(serialized)); - }, - ( - AssociationType::Aggregation - | AssociationType::ListElementAssociationGenerated - | AssociationType::ListElementAssociationCustom, - Cardinality::Any, - ElementValue::Array(elements), - ) => { - let serialized_aggregates = self.make_serialized_aggregated_array( - &association_name, - &association_type_ref, - elements, - )?; - mapped.insert(association_name, JsonElement::Array(serialized_aggregates)); - }, - (_, Cardinality::ZeroOrOne, ElementValue::Null) => { - mapped.insert(association_name, JsonElement::Null); - }, - ( - AssociationType::ElementAssociation | AssociationType::ListAssociation, - Cardinality::One | Cardinality::ZeroOrOne, - ElementValue::IdGeneratedId(id), - ) => { - // FIXME it's not always generated id but it's fine probably - mapped.insert(association_name, JsonElement::String(id.into())); - }, - ( - AssociationType::ListElementAssociationGenerated, - Cardinality::One, - ElementValue::IdTupleGeneratedElementId(id_tuple), - ) => { - mapped.insert( - association_name, - JsonElement::Array(vec![ - JsonElement::String(id_tuple.list_id.into()), - JsonElement::String(id_tuple.element_id.into()), - ]), - ); - }, - ( - AssociationType::ListElementAssociationCustom, - Cardinality::One, - ElementValue::IdTupleCustomElementId(id_tuple), - ) => { - mapped.insert( - association_name, - JsonElement::Array(vec![ - JsonElement::String(id_tuple.list_id.into()), - JsonElement::String(id_tuple.element_id.into()), - ]), - ); - }, - (AssociationType::BlobElementAssociation, _, ElementValue::Array(elements)) => { - // Blobs are copied as-is for now - let serialized_aggregates = self.make_serialized_aggregated_array( - &association_name, - &association_type_ref, - elements, - )?; - mapped.insert(association_name, JsonElement::Array(serialized_aggregates)); - }, - _ => {}, - } - } - - Ok(mapped) - } - - /// Creates a JSON array from an aggregated array - fn make_serialized_aggregated_array( - &self, - association_name: &String, - association_type_ref: &TypeRef, - elements: Vec, - ) -> Result, InstanceMapperError> { - let mut serialized_elements: Vec = Vec::new(); - for element in elements { - match element { - ElementValue::Dict(a) => { - let serialized = self.serialize(association_type_ref, a)?; - serialized_elements.push(JsonElement::Dict(serialized)); - }, - ElementValue::String(v) => { - serialized_elements.push(JsonElement::String(v)); - }, - ElementValue::IdTupleGeneratedElementId(id_tuple) => { - serialized_elements.push(JsonElement::Array(vec![ - JsonElement::String(id_tuple.list_id.into()), - JsonElement::String(id_tuple.element_id.into()), - ])) - }, - ElementValue::IdTupleCustomElementId(id_tuple) => { - serialized_elements.push(JsonElement::Array(vec![ - JsonElement::String(id_tuple.list_id.into()), - JsonElement::String(id_tuple.element_id.into()), - ])) - }, - _ => { - return Err(InvalidValue { - type_ref: association_type_ref.clone(), - field: association_name.to_owned(), - }); - }, - }; - } - Ok(serialized_elements) - } - - /// Returns the type model referenced by a `TypeRef` - /// from the `InstanceMapper`'s `TypeModelProvider` - fn get_type_model(&self, type_ref: &TypeRef) -> Result<&TypeModel, InstanceMapperError> { - self.type_model_provider - .get_type_model(type_ref.app, type_ref.type_) - .ok_or_else(|| InstanceMapperError::TypeNotFound { - type_ref: type_ref.clone(), - }) - } - - /// Transforms an `ElementValue` into a JSON Value - fn serialize_value( - &self, - type_model: &TypeModel, - value_name: &str, - model_value: &ModelValue, - element_value: ElementValue, - ) -> Result { - let invalid_value = || { - Err(InvalidValue { - type_ref: type_model.into(), - field: value_name.to_owned(), - }) - }; - - // FIXME there are more null/empty cases we need to take care of - if model_value.cardinality == Cardinality::ZeroOrOne && element_value == ElementValue::Null - { - return Ok(JsonElement::Null); - } - - if value_name == "_id" { - return match ( - &model_value.value_type, - element_value, - &type_model.element_type, - ) { - ( - ValueType::GeneratedId | ValueType::CustomId, - ElementValue::String(v), - ElementType::Element | ElementType::Aggregated, - ) => Ok(JsonElement::String(v)), - ( - ValueType::GeneratedId, - ElementValue::IdGeneratedId(v), - ElementType::Element | ElementType::Aggregated, - ) => Ok(JsonElement::String(v.to_string())), - ( - ValueType::CustomId, - ElementValue::IdCustomId(v), - ElementType::Element | ElementType::Aggregated, - ) => Ok(JsonElement::String(v.to_string())), - ( - ValueType::GeneratedId, - ElementValue::IdTupleGeneratedElementId(arr), - ElementType::ListElement, - ) => Ok(JsonElement::Array(vec![ - JsonElement::String(arr.list_id.into()), - JsonElement::String(arr.element_id.into()), - ])), - ( - ValueType::CustomId, - ElementValue::IdTupleCustomElementId(arr), - ElementType::ListElement, - ) => Ok(JsonElement::Array(vec![ - JsonElement::String(arr.list_id.into()), - JsonElement::String(arr.element_id.into()), - ])), - - _ => invalid_value(), - }; - } - - match (&model_value.value_type, element_value) { - (_, ElementValue::Bytes(v)) if model_value.encrypted => { - let str = BASE64_STANDARD.encode(v); - Ok(JsonElement::String(str)) - }, - (ValueType::String, ElementValue::String(v)) => Ok(JsonElement::String(v)), - (ValueType::Number, ElementValue::Number(v)) => Ok(JsonElement::String(v.to_string())), - (ValueType::Bytes, ElementValue::Bytes(v)) => { - let str = BASE64_STANDARD.encode(v); - Ok(JsonElement::String(str)) - }, - (ValueType::Date, ElementValue::Date(v)) => { - Ok(JsonElement::String(v.as_millis().to_string())) - }, - (ValueType::Boolean, ElementValue::Bool(v)) => { - Ok(JsonElement::String(if v { "1" } else { "0" }.to_owned())) - }, - (ValueType::GeneratedId, ElementValue::IdGeneratedId(v)) => { - Ok(JsonElement::String(v.into())) - }, - (ValueType::CustomId, ElementValue::IdCustomId(v)) => Ok(JsonElement::String(v.into())), - (ValueType::CompressedString, ElementValue::String(_)) => { - unimplemented!("compressed string") - }, - _ => invalid_value(), - } - } - - /// Transforms a JSON array into an `IdTupleGenerated` - fn parse_id_tuple_generated(vec: Vec) -> Option { - let mut it = vec.into_iter(); - match (it.next(), it.next(), it.next()) { - (Some(JsonElement::String(list_id)), Some(JsonElement::String(element_id)), None) => { - // would like to consume the array here but oh well - Some(IdTupleGenerated::new( - GeneratedId(list_id), - GeneratedId(element_id), - )) - }, - _ => None, - } - } - - /// Transforms a JSON array into an `IdTupleCustom` - fn parse_id_tuple_custom(vec: Vec) -> Option { - let mut it = vec.into_iter(); - match (it.next(), it.next(), it.next()) { - (Some(JsonElement::String(list_id)), Some(JsonElement::String(element_id)), None) => { - // would like to consume the array here but oh well - Some(IdTupleCustom::new( - GeneratedId(list_id), - CustomId(element_id), - )) - }, - _ => None, - } - } - - /// Transforms a JSON value into an `ElementValue` - fn parse_value( - &self, - type_model: &TypeModel, - value_name: &str, - model_value: &ModelValue, - json_value: JsonElement, - ) -> Result { - let invalid_value = || { - Err(InvalidValue { - type_ref: type_model.into(), - field: value_name.to_owned(), - }) - }; - - // FIXME there are more null/empty cases we need to take care of - if model_value.cardinality == Cardinality::ZeroOrOne && json_value == JsonElement::Null { - return Ok(ElementValue::Null); - } - - // Type models for ids are special. - // The actual type depends on the type of the Element. - // e.g. for ListElementType the GeneratedId actually means IdTuple.- - if value_name == "_id" { - return match ( - &model_value.value_type, - json_value, - &type_model.element_type, - ) { - ( - ValueType::GeneratedId | ValueType::CustomId, - JsonElement::String(v), - ElementType::Element | ElementType::Aggregated, - ) => Ok(ElementValue::String(v)), - ( - ValueType::GeneratedId, - JsonElement::Array(arr), - ElementType::ListElement | ElementType::BlobElement, - ) if arr.len() == 2 => match Self::parse_id_tuple_generated(arr) { - None => invalid_value(), - Some(id_tuple) => Ok(ElementValue::IdTupleGeneratedElementId(id_tuple)), - }, - (ValueType::CustomId, JsonElement::Array(arr), ElementType::ListElement) - if arr.len() == 2 => - { - match Self::parse_id_tuple_custom(arr) { - None => invalid_value(), - Some(id_tuple) => Ok(ElementValue::IdTupleCustomElementId(id_tuple)), - } - }, - _ => invalid_value(), - }; - } - - match (&model_value.value_type, json_value) { - (ValueType::String, JsonElement::String(v)) => Ok(ElementValue::String(v)), - (ValueType::Number, JsonElement::String(v)) => match v.parse::() { - Ok(num) => Ok(ElementValue::Number(num)), - Err(_) => invalid_value(), - }, - (ValueType::Bytes, JsonElement::String(v)) => { - let vec = match BASE64_STANDARD.decode(v) { - Ok(v) => Ok(v), - Err(_) => Err(InvalidValue { - type_ref: type_model.into(), - field: value_name.to_owned(), - }), - }?; - Ok(ElementValue::Bytes(vec)) - }, - (ValueType::Date, JsonElement::String(v)) => { - let system_time = v.parse::().map_err(|_| InvalidValue { - type_ref: type_model.into(), - field: value_name.to_owned(), - })?; - Ok(ElementValue::Date(DateTime::from_millis(system_time))) - }, - (ValueType::Boolean, JsonElement::String(v)) => match v.as_str() { - "0" => Ok(ElementValue::Bool(false)), - "1" => Ok(ElementValue::Bool(true)), - _ => invalid_value(), - }, - (ValueType::GeneratedId, JsonElement::String(v)) => { - Ok(ElementValue::IdGeneratedId(GeneratedId(v))) - }, - (ValueType::CustomId, JsonElement::String(v)) => { - Ok(ElementValue::IdCustomId(CustomId(v))) - }, - (ValueType::CompressedString, JsonElement::String(_)) => { - unimplemented!("compressed string") - }, - _ => invalid_value(), - } - } + pub fn new(type_model_provider: Arc) -> JsonSerializer { + JsonSerializer { + type_model_provider, + } + } + + /// Creates an entity from JSON data + pub fn parse( + &self, + type_ref: &TypeRef, + mut raw_entity: RawEntity, + ) -> Result { + let type_model = self.get_type_model(type_ref)?; + let mut mapped: HashMap = HashMap::new(); + for (&value_name, value_type) in &type_model.values { + // reuse the name + let (value_name, value) = + raw_entity + .remove_entry(value_name) + .ok_or_else(|| InvalidValue { + type_ref: type_ref.clone(), + field: value_name.to_owned(), + })?; + + let mapped_value = match (&value_type.cardinality, value) { + (Cardinality::ZeroOrOne, JsonElement::Null) => ElementValue::Null, + (Cardinality::One, JsonElement::String(v)) if v.is_empty() => { + // Empty string signifies default value for a field. This is primarily the + // case for encrypted fields. This includes manually encrypted fields in some + // cases. + // When the value is encrypted we need to pass on the information about the + // default value, so we keep it as an empty string (see entity_facade.rs). + // Otherwise, we resolve the field to its default value. + if value_type.encrypted { + ElementValue::String(String::new()) + } else { + value_type.value_type.get_default() + } + } + (Cardinality::One | Cardinality::ZeroOrOne, JsonElement::String(s)) + if value_type.encrypted => + { + ElementValue::Bytes(BASE64_STANDARD.decode(s).map_err(|_| InvalidValue { + type_ref: type_ref.clone(), + field: value_name.clone(), + })?) + } + (_, value) if !value_type.encrypted => { + self.parse_value(type_model, &value_name, value_type, value)? + } + _ => { + return Err(InvalidValue { + type_ref: type_ref.clone(), + field: value_name.clone(), + }) + } + }; + mapped.insert(value_name, mapped_value); + } + + for (&association_name, association_type) in &type_model.associations { + // reuse the name + let (association_name, value) = + raw_entity + .remove_entry(association_name) + .ok_or_else(|| InvalidValue { + type_ref: type_ref.clone(), + field: association_name.to_owned(), + })?; + let association_type_ref = TypeRef { + app: association_type.dependency.unwrap_or(type_ref.app), + type_: association_type.ref_type, + }; + match ( + &association_type.association_type, + &association_type.cardinality, + value, + ) { + ( + AssociationType::Aggregation, + Cardinality::One | Cardinality::ZeroOrOne, + JsonElement::Dict(dict), + ) => { + let parsed = self.parse(&association_type_ref, dict)?; + mapped.insert(association_name, ElementValue::Dict(parsed)); + } + (AssociationType::Aggregation, Cardinality::Any, JsonElement::Array(elements)) => { + let parsed_aggregates = self.parse_aggregated_array( + &association_name, + &association_type_ref, + elements, + )?; + mapped.insert(association_name, ElementValue::Array(parsed_aggregates)); + } + (_, Cardinality::ZeroOrOne, JsonElement::Null) => { + mapped.insert(association_name, ElementValue::Null); + } + ( + AssociationType::ElementAssociation | AssociationType::ListAssociation, + Cardinality::One | Cardinality::ZeroOrOne, + JsonElement::String(id), + ) => { + // FIXME it's not always generated id but it's fine probably + mapped.insert( + association_name, + ElementValue::IdGeneratedId(GeneratedId(id)), + ); + } + ( + AssociationType::ListElementAssociationGenerated, + Cardinality::One | Cardinality::ZeroOrOne, + JsonElement::Array(vec), + ) => { + let id_tuple = match Self::parse_id_tuple_generated(vec) { + None => { + return Err(InvalidValue { + type_ref: association_type_ref, + field: association_name, + }); + } + Some(id_tuple) => id_tuple, + }; + mapped.insert( + association_name, + ElementValue::IdTupleGeneratedElementId(id_tuple), + ); + } + ( + AssociationType::ListElementAssociationCustom, + Cardinality::One | Cardinality::ZeroOrOne, + JsonElement::Array(vec), + ) => { + let id_tuple = match Self::parse_id_tuple_custom(vec) { + None => { + return Err(InvalidValue { + type_ref: association_type_ref, + field: association_name, + }); + } + Some(id_tuple) => id_tuple, + }; + mapped.insert( + association_name, + ElementValue::IdTupleCustomElementId(id_tuple), + ); + } + ( + AssociationType::ListElementAssociationGenerated, + Cardinality::Any, + JsonElement::Array(vec), + ) => { + let ids = + self.parse_id_tuple_list_generated(type_ref, &association_name, vec)?; + + mapped.insert(association_name, ElementValue::Array(ids)); + } + ( + AssociationType::ListElementAssociationCustom, + Cardinality::Any, + JsonElement::Array(vec), + ) => { + let ids = self.parse_id_tuple_list_custom(type_ref, &association_name, vec)?; + + mapped.insert(association_name, ElementValue::Array(ids)); + } + ( + AssociationType::BlobElementAssociation, + Cardinality::One | Cardinality::ZeroOrOne, + JsonElement::Array(vec), + ) => { + let id_tuple = match Self::parse_id_tuple_generated(vec) { + None => { + return Err(InvalidValue { + type_ref: association_type_ref, + field: association_name, + }); + } + Some(id_tuple) => id_tuple, + }; + mapped.insert( + association_name, + ElementValue::IdTupleGeneratedElementId(id_tuple), + ); + } + _ => {} + } + } + + Ok(mapped) + } + + /// Parses an aggregated array from a value of a JSON object containing an entity/instance + fn parse_aggregated_array( + &self, + association_name: &str, + association_type_ref: &TypeRef, + elements: Vec, + ) -> Result, InstanceMapperError> { + let mut parsed_aggregates = Vec::new(); + for element in elements { + match element { + JsonElement::Dict(a) => { + let parsed = self.parse(association_type_ref, a)?; + parsed_aggregates.push(ElementValue::Dict(parsed)); + } + _ => { + return Err(InvalidValue { + type_ref: association_type_ref.clone(), + field: association_name.to_owned(), + }); + } + }; + } + Ok(parsed_aggregates) + } + + fn parse_id_tuple_list_generated( + &self, + outer_type_ref: &TypeRef, + association_name: &str, + elements: Vec, + ) -> Result, InstanceMapperError> { + elements + .into_iter() + .map(|json_element| { + let JsonElement::Array(id_vec) = json_element else { + return Err(InvalidValue { + field: association_name.to_owned(), + type_ref: outer_type_ref.clone(), + }); + }; + let id_tuple = + Self::parse_id_tuple_generated(id_vec).ok_or_else(|| InvalidValue { + field: association_name.to_owned(), + type_ref: outer_type_ref.clone(), + })?; + Ok(ElementValue::IdTupleGeneratedElementId(id_tuple)) + }) + .collect() + } + + fn parse_id_tuple_list_custom( + &self, + outer_type_ref: &TypeRef, + association_name: &str, + elements: Vec, + ) -> Result, InstanceMapperError> { + elements + .into_iter() + .map(|json_element| { + let JsonElement::Array(id_vec) = json_element else { + return Err(InvalidValue { + field: association_name.to_owned(), + type_ref: outer_type_ref.clone(), + }); + }; + let id_tuple = Self::parse_id_tuple_custom(id_vec).ok_or_else(|| InvalidValue { + field: association_name.to_owned(), + type_ref: outer_type_ref.clone(), + })?; + Ok(ElementValue::IdTupleCustomElementId(id_tuple)) + }) + .collect() + } + + /// Transforms an entity/instance into JSON data + pub fn serialize( + &self, + type_ref: &TypeRef, + mut entity: ParsedEntity, + ) -> Result { + let type_model = self.get_type_model(type_ref)?; + let mut mapped: RawEntity = HashMap::new(); + for (&value_name, value_type) in &type_model.values { + // we take out of the map to reuse the names/values + let (value_name, value) = + entity + .remove_entry(value_name) + .ok_or_else(|| InvalidValue { + type_ref: type_ref.clone(), + field: value_name.to_owned(), + })?; + + let serialized_value = + self.serialize_value(type_model, &value_name, value_type, value)?; + mapped.insert(value_name, serialized_value); + } + + for (&association_name, association_type) in &type_model.associations { + let (association_name, value) = + entity + .remove_entry(association_name) + .ok_or_else(|| InvalidValue { + type_ref: type_ref.clone(), + field: association_name.to_owned(), + })?; + + let association_type_ref = TypeRef { + // aggregates can be imported across apps + app: association_type.dependency.unwrap_or(type_ref.app), + type_: association_type.ref_type, + }; + match ( + &association_type.association_type, + &association_type.cardinality, + value, + ) { + ( + AssociationType::Aggregation, + Cardinality::One | Cardinality::ZeroOrOne, + ElementValue::Dict(dict), + ) => { + let serialized = self.serialize(&association_type_ref, dict)?; + mapped.insert(association_name, JsonElement::Dict(serialized)); + } + ( + AssociationType::Aggregation + | AssociationType::ListElementAssociationGenerated + | AssociationType::ListElementAssociationCustom, + Cardinality::Any, + ElementValue::Array(elements), + ) => { + let serialized_aggregates = self.make_serialized_aggregated_array( + &association_name, + &association_type_ref, + elements, + )?; + mapped.insert(association_name, JsonElement::Array(serialized_aggregates)); + } + (_, Cardinality::ZeroOrOne, ElementValue::Null) => { + mapped.insert(association_name, JsonElement::Null); + } + ( + AssociationType::ElementAssociation | AssociationType::ListAssociation, + Cardinality::One | Cardinality::ZeroOrOne, + ElementValue::IdGeneratedId(id), + ) => { + // FIXME it's not always generated id but it's fine probably + mapped.insert(association_name, JsonElement::String(id.into())); + } + ( + AssociationType::ListElementAssociationGenerated, + Cardinality::One, + ElementValue::IdTupleGeneratedElementId(id_tuple), + ) => { + mapped.insert( + association_name, + JsonElement::Array(vec![ + JsonElement::String(id_tuple.list_id.into()), + JsonElement::String(id_tuple.element_id.into()), + ]), + ); + } + ( + AssociationType::ListElementAssociationCustom, + Cardinality::One, + ElementValue::IdTupleCustomElementId(id_tuple), + ) => { + mapped.insert( + association_name, + JsonElement::Array(vec![ + JsonElement::String(id_tuple.list_id.into()), + JsonElement::String(id_tuple.element_id.into()), + ]), + ); + } + (AssociationType::BlobElementAssociation, _, ElementValue::Array(elements)) => { + // Blobs are copied as-is for now + let serialized_aggregates = self.make_serialized_aggregated_array( + &association_name, + &association_type_ref, + elements, + )?; + mapped.insert(association_name, JsonElement::Array(serialized_aggregates)); + } + _ => {} + } + } + + Ok(mapped) + } + + /// Creates a JSON array from an aggregated array + fn make_serialized_aggregated_array( + &self, + association_name: &String, + association_type_ref: &TypeRef, + elements: Vec, + ) -> Result, InstanceMapperError> { + let mut serialized_elements: Vec = Vec::new(); + for element in elements { + match element { + ElementValue::Dict(a) => { + let serialized = self.serialize(association_type_ref, a)?; + serialized_elements.push(JsonElement::Dict(serialized)); + } + ElementValue::String(v) => { + serialized_elements.push(JsonElement::String(v)); + } + ElementValue::IdTupleGeneratedElementId(id_tuple) => { + serialized_elements.push(JsonElement::Array(vec![ + JsonElement::String(id_tuple.list_id.into()), + JsonElement::String(id_tuple.element_id.into()), + ])) + } + ElementValue::IdTupleCustomElementId(id_tuple) => { + serialized_elements.push(JsonElement::Array(vec![ + JsonElement::String(id_tuple.list_id.into()), + JsonElement::String(id_tuple.element_id.into()), + ])) + } + _ => { + return Err(InvalidValue { + type_ref: association_type_ref.clone(), + field: association_name.to_owned(), + }); + } + }; + } + Ok(serialized_elements) + } + + /// Returns the type model referenced by a `TypeRef` + /// from the `InstanceMapper`'s `TypeModelProvider` + fn get_type_model(&self, type_ref: &TypeRef) -> Result<&TypeModel, InstanceMapperError> { + self.type_model_provider + .get_type_model(type_ref.app, type_ref.type_) + .ok_or_else(|| InstanceMapperError::TypeNotFound { + type_ref: type_ref.clone(), + }) + } + + /// Transforms an `ElementValue` into a JSON Value + fn serialize_value( + &self, + type_model: &TypeModel, + value_name: &str, + model_value: &ModelValue, + element_value: ElementValue, + ) -> Result { + let invalid_value = || { + Err(InvalidValue { + type_ref: type_model.into(), + field: value_name.to_owned(), + }) + }; + + // FIXME there are more null/empty cases we need to take care of + if model_value.cardinality == Cardinality::ZeroOrOne && element_value == ElementValue::Null + { + return Ok(JsonElement::Null); + } + + if value_name == "_id" { + return match ( + &model_value.value_type, + element_value, + &type_model.element_type, + ) { + ( + ValueType::GeneratedId | ValueType::CustomId, + ElementValue::String(v), + ElementType::Element | ElementType::Aggregated, + ) => Ok(JsonElement::String(v)), + ( + ValueType::GeneratedId, + ElementValue::IdGeneratedId(v), + ElementType::Element | ElementType::Aggregated, + ) => Ok(JsonElement::String(v.to_string())), + ( + ValueType::CustomId, + ElementValue::IdCustomId(v), + ElementType::Element | ElementType::Aggregated, + ) => Ok(JsonElement::String(v.to_string())), + ( + ValueType::GeneratedId, + ElementValue::IdTupleGeneratedElementId(arr), + ElementType::ListElement, + ) => Ok(JsonElement::Array(vec![ + JsonElement::String(arr.list_id.into()), + JsonElement::String(arr.element_id.into()), + ])), + ( + ValueType::CustomId, + ElementValue::IdTupleCustomElementId(arr), + ElementType::ListElement, + ) => Ok(JsonElement::Array(vec![ + JsonElement::String(arr.list_id.into()), + JsonElement::String(arr.element_id.into()), + ])), + + _ => invalid_value(), + }; + } + + match (&model_value.value_type, element_value) { + (_, ElementValue::Bytes(v)) if model_value.encrypted => { + let str = BASE64_STANDARD.encode(v); + Ok(JsonElement::String(str)) + } + (ValueType::String, ElementValue::String(v)) => Ok(JsonElement::String(v)), + (ValueType::Number, ElementValue::Number(v)) => Ok(JsonElement::String(v.to_string())), + (ValueType::Bytes, ElementValue::Bytes(v)) => { + let str = BASE64_STANDARD.encode(v); + Ok(JsonElement::String(str)) + } + (ValueType::Date, ElementValue::Date(v)) => { + Ok(JsonElement::String(v.as_millis().to_string())) + } + (ValueType::Boolean, ElementValue::Bool(v)) => { + Ok(JsonElement::String(if v { "1" } else { "0" }.to_owned())) + } + (ValueType::GeneratedId, ElementValue::IdGeneratedId(v)) => { + Ok(JsonElement::String(v.into())) + } + (ValueType::CustomId, ElementValue::IdCustomId(v)) => Ok(JsonElement::String(v.into())), + (ValueType::CompressedString, ElementValue::String(_)) => { + unimplemented!("compressed string") + } + _ => invalid_value(), + } + } + + /// Transforms a JSON array into an `IdTupleGenerated` + fn parse_id_tuple_generated(vec: Vec) -> Option { + let mut it = vec.into_iter(); + match (it.next(), it.next(), it.next()) { + (Some(JsonElement::String(list_id)), Some(JsonElement::String(element_id)), None) => { + // would like to consume the array here but oh well + Some(IdTupleGenerated::new( + GeneratedId(list_id), + GeneratedId(element_id), + )) + } + _ => None, + } + } + + /// Transforms a JSON array into an `IdTupleCustom` + fn parse_id_tuple_custom(vec: Vec) -> Option { + let mut it = vec.into_iter(); + match (it.next(), it.next(), it.next()) { + (Some(JsonElement::String(list_id)), Some(JsonElement::String(element_id)), None) => { + // would like to consume the array here but oh well + Some(IdTupleCustom::new( + GeneratedId(list_id), + CustomId(element_id), + )) + } + _ => None, + } + } + + /// Transforms a JSON value into an `ElementValue` + fn parse_value( + &self, + type_model: &TypeModel, + value_name: &str, + model_value: &ModelValue, + json_value: JsonElement, + ) -> Result { + let invalid_value = || { + Err(InvalidValue { + type_ref: type_model.into(), + field: value_name.to_owned(), + }) + }; + + // FIXME there are more null/empty cases we need to take care of + if model_value.cardinality == Cardinality::ZeroOrOne && json_value == JsonElement::Null { + return Ok(ElementValue::Null); + } + + // Type models for ids are special. + // The actual type depends on the type of the Element. + // e.g. for ListElementType the GeneratedId actually means IdTuple.- + if value_name == "_id" { + return match ( + &model_value.value_type, + json_value, + &type_model.element_type, + ) { + ( + ValueType::GeneratedId | ValueType::CustomId, + JsonElement::String(v), + ElementType::Element | ElementType::Aggregated, + ) => Ok(ElementValue::String(v)), + ( + ValueType::GeneratedId, + JsonElement::Array(arr), + ElementType::ListElement | ElementType::BlobElement, + ) if arr.len() == 2 => match Self::parse_id_tuple_generated(arr) { + None => invalid_value(), + Some(id_tuple) => Ok(ElementValue::IdTupleGeneratedElementId(id_tuple)), + }, + (ValueType::CustomId, JsonElement::Array(arr), ElementType::ListElement) + if arr.len() == 2 => + { + match Self::parse_id_tuple_custom(arr) { + None => invalid_value(), + Some(id_tuple) => Ok(ElementValue::IdTupleCustomElementId(id_tuple)), + } + } + _ => invalid_value(), + }; + } + + match (&model_value.value_type, json_value) { + (ValueType::String, JsonElement::String(v)) => Ok(ElementValue::String(v)), + (ValueType::Number, JsonElement::String(v)) => match v.parse::() { + Ok(num) => Ok(ElementValue::Number(num)), + Err(_) => invalid_value(), + }, + (ValueType::Bytes, JsonElement::String(v)) => { + let vec = match BASE64_STANDARD.decode(v) { + Ok(v) => Ok(v), + Err(_) => Err(InvalidValue { + type_ref: type_model.into(), + field: value_name.to_owned(), + }), + }?; + Ok(ElementValue::Bytes(vec)) + } + (ValueType::Date, JsonElement::String(v)) => { + let system_time = v.parse::().map_err(|_| InvalidValue { + type_ref: type_model.into(), + field: value_name.to_owned(), + })?; + Ok(ElementValue::Date(DateTime::from_millis(system_time))) + } + (ValueType::Boolean, JsonElement::String(v)) => match v.as_str() { + "0" => Ok(ElementValue::Bool(false)), + "1" => Ok(ElementValue::Bool(true)), + _ => invalid_value(), + }, + (ValueType::GeneratedId, JsonElement::String(v)) => { + Ok(ElementValue::IdGeneratedId(GeneratedId(v))) + } + (ValueType::CustomId, JsonElement::String(v)) => { + Ok(ElementValue::IdCustomId(CustomId(v))) + } + (ValueType::CompressedString, JsonElement::String(_)) => { + unimplemented!("compressed string") + } + _ => invalid_value(), + } + } } #[cfg(test)] mod tests { - use super::*; - use crate::crypto::key::GenericAesKey; - use crate::crypto::randomizer_facade::RandomizerFacade; - use crate::entities::entity_facade::EntityFacadeImpl; - use crate::entities::generated::sys::User; - use crate::entities::Entity; - use crate::instance_mapper::InstanceMapper; - use crate::services::test_services::HelloEncOutput; - use crate::type_model_provider::{init_type_model_provider, AppName, TypeName}; - - #[test] - fn test_parse_mail() { - let type_model_provider = Arc::new(init_type_model_provider()); - let mapper = JsonSerializer { - type_model_provider, - }; - // TODO: Expand this test to cover bucket keys in mail - let email_json = include_str!("../test_data/email_response.json"); - let raw_entity = serde_json::from_str::(email_json).unwrap(); - let type_ref = TypeRef { - app: "tutanota", - type_: "Mail", - }; - mapper.parse(&type_ref, raw_entity).unwrap(); - } - - #[test] - fn test_parse_mail_with_attachments() { - let type_model_provider = Arc::new(init_type_model_provider()); - let mapper = JsonSerializer { - type_model_provider, - }; - let email_json = include_str!("../test_data/email_response_attachments.json"); - let raw_entity = serde_json::from_str::(email_json).unwrap(); - let type_ref = TypeRef { - app: "tutanota", - type_: "Mail", - }; - let parsed = mapper.parse(&type_ref, raw_entity).unwrap(); - assert_eq!( - &ElementValue::Array(vec![ElementValue::IdTupleGeneratedElementId( - IdTupleGenerated::new( - GeneratedId("O3lYN71--J-0".to_owned()), - GeneratedId("O3lYUQI----0".to_owned()), - ) - )]), - parsed.get("attachments").expect("has attachments") - ) - } - - #[test] - fn test_parse_user_with_empty_group_key() { - let type_model_provider = Arc::new(init_type_model_provider()); - let mapper = JsonSerializer { - type_model_provider, - }; - let user_json = include_str!("../test_data/user_response_empty_group_key.json"); - let raw_entity = serde_json::from_str::(user_json).unwrap(); - let type_ref = User::type_ref(); - let parsed = mapper.parse(&type_ref, raw_entity).unwrap(); - let ship = parsed - .get("memberships") - .unwrap() - .assert_array() - .iter() - .find(|m| m.assert_dict().get("groupType").unwrap().assert_number() == 2) - .unwrap() - .assert_dict(); - assert_eq!( - ship.get("symEncGKey").unwrap().assert_bytes(), - Vec::::new() - ); - } - - #[test] - fn serialization_for_encrypted_works() { - use crate::entities::entity_facade::EntityFacade; - - let mut type_provider: HashMap> = HashMap::new(); - crate::services::test_services::extend_model_resolver(&mut type_provider); - let type_provider = Arc::new(TypeModelProvider::new(type_provider)); - - let entity_to_serialize = HelloEncOutput { - answer: "".to_string(), - timestamp: Default::default(), - _finalIvs: Default::default(), - }; - - let instance_mapper = InstanceMapper::new(); - let parsed_unencrypted = instance_mapper - .serialize_entity(entity_to_serialize) - .unwrap(); - let entity_facade = EntityFacadeImpl::new( - type_provider.clone(), - RandomizerFacade::from_core(rand_core::OsRng), - ); - let type_model = type_provider - .get_type_model( - HelloEncOutput::type_ref().app, - HelloEncOutput::type_ref().type_, - ) - .unwrap(); - let session_key = GenericAesKey::from_bytes(&[rand::random(); 32]).unwrap(); - let parsed_encrypted = entity_facade - .encrypt_and_map(type_model, &parsed_unencrypted, &session_key) - .unwrap(); - - let json_serializer = JsonSerializer::new(type_provider); - json_serializer - .serialize(&HelloEncOutput::type_ref(), parsed_encrypted) - .unwrap(); - } + use super::*; + use crate::crypto::key::GenericAesKey; + use crate::crypto::randomizer_facade::RandomizerFacade; + use crate::entities::entity_facade::EntityFacadeImpl; + use crate::entities::generated::sys::User; + use crate::entities::Entity; + use crate::instance_mapper::InstanceMapper; + use crate::services::test_services::HelloEncOutput; + use crate::type_model_provider::{init_type_model_provider, AppName, TypeName}; + + #[test] + fn test_parse_mail() { + let type_model_provider = Arc::new(init_type_model_provider()); + let mapper = JsonSerializer { + type_model_provider, + }; + // TODO: Expand this test to cover bucket keys in mail + let email_json = include_str!("../test_data/email_response.json"); + let raw_entity = serde_json::from_str::(email_json).unwrap(); + let type_ref = TypeRef { + app: "tutanota", + type_: "Mail", + }; + mapper.parse(&type_ref, raw_entity).unwrap(); + } + + #[test] + fn test_parse_mail_with_attachments() { + let type_model_provider = Arc::new(init_type_model_provider()); + let mapper = JsonSerializer { + type_model_provider, + }; + let email_json = include_str!("../test_data/email_response_attachments.json"); + let raw_entity = serde_json::from_str::(email_json).unwrap(); + let type_ref = TypeRef { + app: "tutanota", + type_: "Mail", + }; + let parsed = mapper.parse(&type_ref, raw_entity).unwrap(); + assert_eq!( + &ElementValue::Array(vec![ElementValue::IdTupleGeneratedElementId( + IdTupleGenerated::new( + GeneratedId("O3lYN71--J-0".to_owned()), + GeneratedId("O3lYUQI----0".to_owned()), + ) + )]), + parsed.get("attachments").expect("has attachments") + ) + } + + #[test] + fn test_parse_user_with_empty_group_key() { + let type_model_provider = Arc::new(init_type_model_provider()); + let mapper = JsonSerializer { + type_model_provider, + }; + let user_json = include_str!("../test_data/user_response_empty_group_key.json"); + let raw_entity = serde_json::from_str::(user_json).unwrap(); + let type_ref = User::type_ref(); + let parsed = mapper.parse(&type_ref, raw_entity).unwrap(); + let ship = parsed + .get("memberships") + .unwrap() + .assert_array() + .iter() + .find(|m| m.assert_dict().get("groupType").unwrap().assert_number() == 2) + .unwrap() + .assert_dict(); + assert_eq!( + ship.get("symEncGKey").unwrap().assert_bytes(), + Vec::::new() + ); + } + + #[test] + fn serialization_for_encrypted_works() { + use crate::entities::entity_facade::EntityFacade; + + let mut type_provider: HashMap> = HashMap::new(); + crate::services::test_services::extend_model_resolver(&mut type_provider); + let type_provider = Arc::new(TypeModelProvider::new(type_provider)); + + let entity_to_serialize = HelloEncOutput { + answer: "".to_string(), + timestamp: Default::default(), + _finalIvs: Default::default(), + }; + + let instance_mapper = InstanceMapper::new(); + let parsed_unencrypted = instance_mapper + .serialize_entity(entity_to_serialize) + .unwrap(); + let entity_facade = EntityFacadeImpl::new( + type_provider.clone(), + RandomizerFacade::from_core(rand_core::OsRng), + ); + let type_model = type_provider + .get_type_model( + HelloEncOutput::type_ref().app, + HelloEncOutput::type_ref().type_, + ) + .unwrap(); + let session_key = GenericAesKey::from_bytes(&[rand::random(); 32]).unwrap(); + let parsed_encrypted = entity_facade + .encrypt_and_map(type_model, &parsed_unencrypted, &session_key) + .unwrap(); + + let json_serializer = JsonSerializer::new(type_provider); + json_serializer + .serialize(&HelloEncOutput::type_ref(), parsed_encrypted) + .unwrap(); + } } From 81355ffc976ecbc22547d7ddc6ae0a1d4c97091b Mon Sep 17 00:00:00 2001 From: nig Date: Wed, 13 Nov 2024 15:44:23 +0100 Subject: [PATCH 25/32] use ParsedEntity type alias where appropriate --- tuta-sdk/rust/sdk/src/element_value.rs | 12 ++++++------ tuta-sdk/rust/sdk/src/entities/entity_facade.rs | 6 +++--- tuta-sdk/rust/sdk/src/entity_client.rs | 8 ++++---- tuta-sdk/rust/sdk/src/instance_mapper.rs | 4 ++-- tuta-sdk/rust/sdk/src/json_serializer.rs | 2 +- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tuta-sdk/rust/sdk/src/element_value.rs b/tuta-sdk/rust/sdk/src/element_value.rs index f2e19d07768..7b4f3b6c5bd 100644 --- a/tuta-sdk/rust/sdk/src/element_value.rs +++ b/tuta-sdk/rust/sdk/src/element_value.rs @@ -20,7 +20,7 @@ pub enum ElementValue { IdGeneratedId(GeneratedId), IdCustomId(CustomId), IdTupleGeneratedElementId(IdTupleGenerated), - Dict(HashMap), + Dict(ParsedEntity), Array(Vec), IdTupleCustomElementId(IdTupleCustom), } @@ -56,7 +56,7 @@ impl ElementValue { } } - pub fn assert_dict(&self) -> HashMap { + pub fn assert_dict(&self) -> ParsedEntity { match self { ElementValue::Dict(value) => value.clone(), _ => panic!("Invalid type"), @@ -70,14 +70,14 @@ impl ElementValue { } } - pub fn assert_dict_ref(&self) -> &HashMap { + pub fn assert_dict_ref(&self) -> &ParsedEntity { match self { ElementValue::Dict(value) => value, _ => panic!("Invalid type"), } } - pub fn assert_dict_mut_ref(&mut self) -> &mut HashMap { + pub fn assert_dict_mut_ref(&mut self) -> &mut ParsedEntity { match self { ElementValue::Dict(value) => value, _ => panic!("Invalid type"), @@ -210,8 +210,8 @@ impl From for ElementValue { } } -impl From> for ElementValue { - fn from(value: HashMap) -> Self { +impl From for ElementValue { + fn from(value: ParsedEntity) -> Self { Self::Dict(value) } } diff --git a/tuta-sdk/rust/sdk/src/entities/entity_facade.rs b/tuta-sdk/rust/sdk/src/entities/entity_facade.rs index dec73fb0688..47802fa62b6 100644 --- a/tuta-sdk/rust/sdk/src/entities/entity_facade.rs +++ b/tuta-sdk/rust/sdk/src/entities/entity_facade.rs @@ -289,7 +289,7 @@ impl EntityFacadeImpl { mut entity: ParsedEntity, session_key: &GenericAesKey, ) -> Result { - let mut mapped_decrypted: HashMap = Default::default(); + let mut mapped_decrypted: ParsedEntity = Default::default(); let mut mapped_errors: Errors = Default::default(); let mut mapped_ivs: HashMap = Default::default(); @@ -1070,7 +1070,7 @@ mod tests { } #[test] - fn encrypt_instance() { + fn encrypt_and_map_instance() { let sk = GenericAesKey::Aes256(Aes256Key::from_bytes(KNOWN_SK.as_slice()).unwrap()); let owner_enc_session_key = [0, 1, 2]; @@ -1293,7 +1293,7 @@ mod tests { assert_eq!(default_subject.as_bytes(), encrypted_subject.as_slice()); } - fn map_to_string(map: &HashMap) -> String { + fn map_to_string(map: &ParsedEntity) -> String { let mut out = String::new(); let sorted_map: BTreeMap = map.clone().into_iter().collect(); for (key, value) in &sorted_map { diff --git a/tuta-sdk/rust/sdk/src/entity_client.rs b/tuta-sdk/rust/sdk/src/entity_client.rs index d8d161a6e65..1f929793e08 100644 --- a/tuta-sdk/rust/sdk/src/entity_client.rs +++ b/tuta-sdk/rust/sdk/src/entity_client.rs @@ -307,7 +307,7 @@ mod tests { let type_model_provider = mock_type_model_provider(); let list_id = GeneratedId("list_id".to_owned()); - let entity_map: HashMap = collection! { + let entity_map: ParsedEntity = collection! { "_id" => ElementValue::IdTupleGeneratedElementId(IdTupleGenerated::new(list_id.clone(), GeneratedId("element_id".to_owned()))), "field" => ElementValue::Bytes(vec![1, 2, 3]) }; @@ -357,7 +357,7 @@ mod tests { let type_model_provider = mock_type_model_provider(); let list_id = GeneratedId("list_id".to_owned()); - let entity_map: HashMap = collection! { + let entity_map: ParsedEntity = collection! { "_id" => ElementValue::IdTupleCustomElementId(IdTupleCustom::new(list_id.clone(), CustomId("element_id".to_owned()))), "field" => ElementValue::Bytes(vec![1, 2, 3]) }; @@ -407,7 +407,7 @@ mod tests { let type_model_provider = mock_type_model_provider(); let list_id = GeneratedId("list_id".to_owned()); - let entity_map: HashMap = collection! { + let entity_map: ParsedEntity = collection! { "_id" => ElementValue::IdTupleGeneratedElementId(IdTupleGenerated::new(list_id.clone(), GeneratedId("element_id".to_owned()))), "field" => ElementValue::Bytes(vec![1, 2, 3]) }; @@ -457,7 +457,7 @@ mod tests { let type_model_provider = mock_type_model_provider(); let list_id = GeneratedId("list_id".to_owned()); - let entity_map: HashMap = collection! { + let entity_map: ParsedEntity = collection! { "_id" => ElementValue::IdTupleCustomElementId(IdTupleCustom::new(list_id.clone(), CustomId("element_id".to_owned()))), "field" => ElementValue::Bytes(vec![1, 2, 3]) }; diff --git a/tuta-sdk/rust/sdk/src/instance_mapper.rs b/tuta-sdk/rust/sdk/src/instance_mapper.rs index 8b58b1955f4..c94e570299f 100644 --- a/tuta-sdk/rust/sdk/src/instance_mapper.rs +++ b/tuta-sdk/rust/sdk/src/instance_mapper.rs @@ -548,7 +548,7 @@ struct ElementValueSerializer; enum ElementValueStructSerializer { Struct { - map: HashMap, + map: ParsedEntity, }, IdTupleGenerated { list_id: Option, @@ -888,7 +888,7 @@ impl SerializeStruct for ElementValueStructSerializer { /// Yet Another Serializer, this one serializes a map with dynamic keys. struct ElementValueMapSerializer { next_key: Option, - map: HashMap, + map: ParsedEntity, } impl SerializeMap for ElementValueMapSerializer { diff --git a/tuta-sdk/rust/sdk/src/json_serializer.rs b/tuta-sdk/rust/sdk/src/json_serializer.rs index 9b4e5d4fed8..ceff90383fa 100644 --- a/tuta-sdk/rust/sdk/src/json_serializer.rs +++ b/tuta-sdk/rust/sdk/src/json_serializer.rs @@ -54,7 +54,7 @@ impl JsonSerializer { mut raw_entity: RawEntity, ) -> Result { let type_model = self.get_type_model(type_ref)?; - let mut mapped: HashMap = HashMap::new(); + let mut mapped: ParsedEntity = HashMap::new(); for (&value_name, value_type) in &type_model.values { // reuse the name let (value_name, value) = From 040ac51abddeeadcbfbb2663dc6369aabdafe612 Mon Sep 17 00:00:00 2001 From: map Date: Thu, 14 Nov 2024 16:40:57 +0100 Subject: [PATCH 26/32] Test against ImportMailData and not ImportableMail in compatibility_test --- buildSrc/RustGenerator.js | 2 +- packages/node-mimimi/Cargo.toml | 1 + .../src/importer/importable_mail.rs | 6 +- .../msg_file_compatibility_test.rs | 226 ++++----- tuta-sdk/rust/sdk/Cargo.toml | 3 +- .../sdk/src/entities/generated/accounting.rs | 4 +- .../rust/sdk/src/entities/generated/base.rs | 2 +- .../sdk/src/entities/generated/monitor.rs | 16 +- .../sdk/src/entities/generated/storage.rs | 26 +- .../rust/sdk/src/entities/generated/sys.rs | 448 +++++++++--------- .../sdk/src/entities/generated/tutanota.rs | 246 +++++----- .../rust/sdk/src/entities/generated/usage.rs | 16 +- 12 files changed, 481 insertions(+), 515 deletions(-) diff --git a/buildSrc/RustGenerator.js b/buildSrc/RustGenerator.js index c608123a3bd..c4d0e6bcdaf 100644 --- a/buildSrc/RustGenerator.js +++ b/buildSrc/RustGenerator.js @@ -14,7 +14,7 @@ import { AssociationType, Type, ValueType } from "../src/common/api/common/Entit export function generateRustType({ type, modelName }) { let typeName = mapTypeName(type.name, modelName) let buf = `#[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ${typeName} {\n` for (let [valueName, valueProperties] of Object.entries(type.values)) { const rustType = rustValueType(valueName, type, valueProperties) diff --git a/packages/node-mimimi/Cargo.toml b/packages/node-mimimi/Cargo.toml index 02774e7857a..90a8a688fed 100644 --- a/packages/node-mimimi/Cargo.toml +++ b/packages/node-mimimi/Cargo.toml @@ -42,6 +42,7 @@ napi-build = { version = "2.1.3" } base64 = "0.22.1" tokio = { version = "1", features = ["full"] } serde_json = "1" +tuta-sdk = { path = "../../tuta-sdk/rust/sdk", features = ["net", "testing"] } # tuta-imap j4rs = { version = "0.20.0" } diff --git a/packages/node-mimimi/src/importer/importable_mail.rs b/packages/node-mimimi/src/importer/importable_mail.rs index 43dac04d865..b8d5e2d73ea 100644 --- a/packages/node-mimimi/src/importer/importable_mail.rs +++ b/packages/node-mimimi/src/importer/importable_mail.rs @@ -8,7 +8,6 @@ use mail_parser::{ use regex::Regex; use std::borrow::Cow; use std::collections::{HashMap, HashSet}; -use std::time::SystemTime; use tutasdk::date::DateTime; use tutasdk::entities::generated::tutanota::{ EncryptedMailAddress, ImportMailData, ImportMailDataMailReference, MailAddress, Recipients, @@ -494,8 +493,6 @@ impl From for (ImportMailData, Vec) { attachments, } = importable_mail; - let date = date.unwrap_or_else(|| DateTime::from_system_time(SystemTime::now())); - let reply_tos = reply_to_addresses .into_iter() .map(|reply_to| EncryptedMailAddress { @@ -542,7 +539,8 @@ impl From for (ImportMailData, Vec) { method: ical_type as i64, phishingStatus: if is_phishing { 1 } else { 0 }, replyType: reply_type as i64, - date, + // if no date is provided, use UNIX_EPOCH (01.01.1970) as fallback + date: date.unwrap_or_default(), state: mail_state as i64, messageId: message_id, inReplyTo: in_reply_to, diff --git a/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs b/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs index 1b810ccf8a3..c7d32d0e043 100644 --- a/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs +++ b/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs @@ -1,10 +1,13 @@ //! keep in sync with MimeToolsTestMessages.java -use crate::importer::importable_mail::{ImportableMail, MailContact}; +use crate::importer::importable_mail::plain_text_to_html_converter::plain_text_to_html; +use crate::importer::importable_mail::{ImportableMail, ImportableMailAttachment, MailContact}; +use mail_parser::decoders::base64::base64_decode; use serde::Deserialize; -use std::borrow::Cow; use std::collections::HashSet; use std::io::Read; +use tutasdk::date::DateTime; +use tutasdk::entities::generated::tutanota::ImportMailData; #[test] fn mime_tools_test_messages() { @@ -21,16 +24,19 @@ fn mime_tools_test_messages() { }); let ignored_files = [ - "infinite.msg", // encoding not specified so we are falling back to us-ascii but message contains chars encoded in different charset - "multi-digest.msg", // body correctly interpreted as message/rfc822 (due to multipart/digest) whereas the server seems to default to plain/text even for multipart/digest - "multi-bad.msg", // first part is not ignored because of duplicate content-type header, java parser opts for first content-type whereas rust mime-parser uses second content-type header + // encoding not specified so we are falling back to us-ascii but message contains chars encoded in different charset + "infinite.msg", + // body correctly interpreted as message/rfc822 (due to multipart/digest) whereas the server seems to default to plain/text even for multipart/digest + "multi-digest.msg", + // first part is not ignored because of duplicate content-type header, java parser opts for first content-type whereas rust mime-parser uses second content-type header + "multi-bad.msg", ] .into_iter() .collect::>(); for message_file_path in source_message_paths { - eprintln!("File: {:?}", message_file_path); let message_filename = message_file_path.file_name().into_string().unwrap(); + eprintln!("File: {message_filename}"); if ignored_files.contains(message_filename.as_str()) { eprintln!("ignored.."); continue; @@ -52,148 +58,108 @@ fn mime_tools_test_messages() { ); let FileContent { result: expected_result, - exception: expected_exception, + exception: _, } = FileContent::read_from_file(expected_json_file_name.as_str()).unwrap(); let parsed_message_result = ImportableMail::try_from(&parsed_message); - if expected_result.is_some() && expected_exception.is_none() { - let mut importable_mail = parsed_message_result.unwrap(); - let mut expected_importable_mail = ImportableMail::from(expected_result.unwrap()); + if expected_result.is_none() { + eprintln!("has error......"); + continue; + } - importable_mail.attachments.clear(); - expected_importable_mail.attachments.clear(); + let parsed_message = parsed_message_result.unwrap(); + let mut importable_mail: ImportMailData = parsed_message.into(); + let mut expected_importable_mail: ImportMailData = expected_result.unwrap().into(); - // we import raw headers and there is no need to compare them - importable_mail.headers_string = "".to_string(); - expected_importable_mail.headers_string = "".to_string(); + // importable_mail.attachments.clear(); + // expected_importable_mail.attachments.clear(); - assert_eq!(importable_mail, expected_importable_mail); - } else if expected_exception.is_some() && expected_result.is_none() { - // check that the parsing have failed, - // but we cannot check for the actual reason in `expected_exception` - // - // - // todo: should not badbound.msg fail on mail_parser::parse thing? why is it failing in ImportableMail::try_from()? - //assert!(parsed_message_result.is_err()); - } else if expected_result.is_none() && expected_exception.is_none() { - unreachable!() - } else if expected_exception.is_some() && expected_exception.is_some() { - unreachable!() - } else { - unreachable!() - } + // we import raw headers and there is no need to compare them + importable_mail.compressedHeaders.clear(); + expected_importable_mail.compressedHeaders.clear(); + + // we don't cover date headers in server as well. + // .msg and -expected.json do not share same date seems like + importable_mail.date = DateTime::default(); + expected_importable_mail.date = DateTime::default(); + + // todo: + // we don't have different envelope sender in -expected.json + importable_mail.differentEnvelopeSender = None; + + assert_eq!(importable_mail, expected_importable_mail); } } impl From for MailContact { fn from(value: TestMailAddress) -> Self { let TestMailAddress { - name, mail_address, .. + name, + mail_address, + valid: _, } = value; Self { mail_address, name } } } -impl From for ImportableMail { - fn from(mut expected_message: ExpectedMessage) -> Self { - // add a new line at end of headers for mail_parser::MessageParser::parse_headers - expected_message.mail_headers.push_str("\n"); - - let headers_string_clone = expected_message.mail_headers.clone(); - - let mut body_parts = vec![]; - let mut plain_body_ids = vec![]; - let mut html_body_ids = vec![]; - let mut attachment_ids = vec![]; - - let parsed_headers_res = mail_parser::MessageParser::default() - .parse_headers(expected_message.mail_headers.as_str()) - .unwrap(); - - let root_part = mail_parser::MessagePart { - headers: parsed_headers_res.headers().to_vec(), - is_encoding_problem: false, - body: mail_parser::PartType::Text(Cow::Borrowed("")), - encoding: Default::default(), - offset_header: 0, - offset_body: 0, - offset_end: 0, - }; - body_parts.push(root_part); - - if let Some(html_body_part) = expected_message.html_body_text { - let html_body_converted = mail_parser::MessagePart { - headers: vec![], - is_encoding_problem: false, - body: mail_parser::PartType::Html(Cow::Owned(html_body_part)), - encoding: Default::default(), - offset_header: 0, - offset_body: 0, - offset_end: 0, - }; - html_body_ids.push(body_parts.len()); - body_parts.push(html_body_converted); - } - // if there is both plain text and html in json file, - // probably that json if to test multipart/alternative ( todo: is this true? ) - // and since we always select html in multipart/alternative, - // we can skip adding plain text if html text was set. - // hence the `else if let` instead of `if let` - else if let Some(plain_body_part) = expected_message.plain_body_text { - let plain_body_converted = mail_parser::MessagePart { - headers: vec![], - is_encoding_problem: false, - body: mail_parser::PartType::Text(Cow::Owned(plain_body_part)), - encoding: Default::default(), - offset_header: 0, - offset_body: 0, - offset_end: 0, - }; - plain_body_ids.push(body_parts.len()); - body_parts.push(plain_body_converted); - } - - for _attached_message in expected_message.attached_messages { - let attached_message_converted = mail_parser::MessagePart { - headers: vec![], - is_encoding_problem: false, - body: Default::default(), - encoding: Default::default(), - offset_header: 0, - offset_body: 0, - offset_end: 0, - }; - attachment_ids.push(body_parts.len()); - body_parts.push(attached_message_converted); +impl From for ImportMailData { + fn from(expected_message: ExpectedMessage) -> Self { + ImportableMail { + headers_string: expected_message.mail_headers, + subject: expected_message.subject, + html_body_text: expected_message.html_body_text.clone().unwrap_or( + expected_message + .plain_body_text + .map(|plain| plain_text_to_html(&plain)) + .unwrap_or_default(), + ), + attachments: expected_message + .attached_files + .into_iter() + .map(|f| ImportableMailAttachment { + filename: f.name, + content_id: Some(f.content_id), + content_type: "".to_string(), + content: base64_decode(f.data.as_bytes()).unwrap(), + is_inline: false, + }) + .collect(), + date: expected_message + .sent_date + .map(|timestamp| DateTime::from_millis(timestamp as u64)), + different_envelope_sender: None, + from_addresses: vec![expected_message.sender.into()], + to_addresses: expected_message + .to_recipients + .into_iter() + .map(Into::into) + .collect(), + cc_addresses: expected_message + .cc_recipients + .into_iter() + .map(Into::into) + .collect(), + bcc_addresses: expected_message + .bcc_recipients + .into_iter() + .map(Into::into) + .collect(), + reply_to_addresses: expected_message + .reply_to + .into_iter() + .map(Into::into) + .collect(), + ical_type: Default::default(), + reply_type: Default::default(), + mail_state: Default::default(), + is_phishing: false, + unread: false, + message_id: expected_message.id, + in_reply_to: expected_message.in_reply_to, + references: expected_message.references, } - - for _attached_file in expected_message.attached_files { - let attached_file_converted = mail_parser::MessagePart { - headers: vec![], - is_encoding_problem: false, - body: Default::default(), - encoding: Default::default(), - offset_header: 0, - offset_body: 0, - offset_end: 0, - }; - attachment_ids.push(body_parts.len()); - body_parts.push(attached_file_converted); - } - - let parsed_mail = mail_parser::Message { - html_body: html_body_ids, - text_body: plain_body_ids, - attachments: attachment_ids, - parts: body_parts, - // todo: - // will only work for .raw_header(), if we use other _raw function or - // try to access .raw_message in From: ImportableMail, - // this won't work - raw_message: Cow::Owned(headers_string_clone.as_bytes().to_vec()), - }; - - ImportableMail::try_from(&parsed_mail).unwrap() + .try_into() + .unwrap() } } diff --git a/tuta-sdk/rust/sdk/Cargo.toml b/tuta-sdk/rust/sdk/Cargo.toml index 487e2309928..802af135afb 100644 --- a/tuta-sdk/rust/sdk/Cargo.toml +++ b/tuta-sdk/rust/sdk/Cargo.toml @@ -58,9 +58,10 @@ uniffi = { git = "https://github.com/mozilla/uniffi-rs.git", rev = "13a1c559cb37 [features] net = ["dep:hyper", "dep:hyper-util", "dep:http-body-util", "dep:hyper-rustls", "dep:rustls"] +testing = [] [dev-dependencies] -tuta-sdk = { path = ".", features = ["net"] } +tuta-sdk = { path = ".", features = ["net", "testing"] } mockall = { version = "0.13.0" } mockall_double = { version = "0.3.1" } rand = { version = "0.8.5" } diff --git a/tuta-sdk/rust/sdk/src/entities/generated/accounting.rs b/tuta-sdk/rust/sdk/src/entities/generated/accounting.rs index f76890de3fb..e938c8789d4 100644 --- a/tuta-sdk/rust/sdk/src/entities/generated/accounting.rs +++ b/tuta-sdk/rust/sdk/src/entities/generated/accounting.rs @@ -4,7 +4,7 @@ use super::super::*; use serde::{Deserialize, Serialize}; #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CustomerAccountPosting { pub _id: Option, pub amount: i64, @@ -27,7 +27,7 @@ impl Entity for CustomerAccountPosting { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CustomerAccountReturn { pub _format: i64, pub _ownerGroup: Option, diff --git a/tuta-sdk/rust/sdk/src/entities/generated/base.rs b/tuta-sdk/rust/sdk/src/entities/generated/base.rs index d2dee68a9bb..2de8f8900c9 100644 --- a/tuta-sdk/rust/sdk/src/entities/generated/base.rs +++ b/tuta-sdk/rust/sdk/src/entities/generated/base.rs @@ -4,7 +4,7 @@ use super::super::*; use serde::{Deserialize, Serialize}; #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PersistenceResourcePostReturn { pub _format: i64, pub generatedId: Option, diff --git a/tuta-sdk/rust/sdk/src/entities/generated/monitor.rs b/tuta-sdk/rust/sdk/src/entities/generated/monitor.rs index 302348da0bd..d2608822366 100644 --- a/tuta-sdk/rust/sdk/src/entities/generated/monitor.rs +++ b/tuta-sdk/rust/sdk/src/entities/generated/monitor.rs @@ -4,7 +4,7 @@ use super::super::*; use serde::{Deserialize, Serialize}; #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ApprovalMail { pub _format: i64, pub _id: Option, @@ -28,7 +28,7 @@ impl Entity for ApprovalMail { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CounterValue { pub _id: Option, pub counterId: GeneratedId, @@ -47,7 +47,7 @@ impl Entity for CounterValue { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ErrorReportData { pub _id: Option, pub additionalInfo: String, @@ -73,7 +73,7 @@ impl Entity for ErrorReportData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ErrorReportFile { pub _id: Option, pub content: String, @@ -92,7 +92,7 @@ impl Entity for ErrorReportFile { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ReadCounterData { pub _format: i64, pub columnName: Option, @@ -112,7 +112,7 @@ impl Entity for ReadCounterData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ReadCounterReturn { pub _format: i64, pub value: Option, @@ -131,7 +131,7 @@ impl Entity for ReadCounterReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ReportErrorIn { pub _format: i64, pub data: ErrorReportData, @@ -150,7 +150,7 @@ impl Entity for ReportErrorIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct WriteCounterData { pub _format: i64, pub column: GeneratedId, diff --git a/tuta-sdk/rust/sdk/src/entities/generated/storage.rs b/tuta-sdk/rust/sdk/src/entities/generated/storage.rs index e3002eeb17b..9f726ba1cdd 100644 --- a/tuta-sdk/rust/sdk/src/entities/generated/storage.rs +++ b/tuta-sdk/rust/sdk/src/entities/generated/storage.rs @@ -4,7 +4,7 @@ use super::super::*; use serde::{Deserialize, Serialize}; #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BlobAccessTokenPostIn { pub _format: i64, pub archiveDataType: Option, @@ -24,7 +24,7 @@ impl Entity for BlobAccessTokenPostIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BlobAccessTokenPostOut { pub _format: i64, pub blobAccessInfo: BlobServerAccessInfo, @@ -42,7 +42,7 @@ impl Entity for BlobAccessTokenPostOut { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BlobArchiveRef { pub _format: i64, pub _id: Option, @@ -63,7 +63,7 @@ impl Entity for BlobArchiveRef { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BlobGetIn { pub _format: i64, pub archiveId: GeneratedId, @@ -83,7 +83,7 @@ impl Entity for BlobGetIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BlobId { pub _id: Option, pub blobId: GeneratedId, @@ -101,7 +101,7 @@ impl Entity for BlobId { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BlobPostOut { pub _format: i64, pub blobReferenceToken: String, @@ -119,7 +119,7 @@ impl Entity for BlobPostOut { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BlobReadData { pub _id: Option, pub archiveId: GeneratedId, @@ -139,7 +139,7 @@ impl Entity for BlobReadData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BlobReferenceDeleteIn { pub _format: i64, pub archiveDataType: i64, @@ -160,7 +160,7 @@ impl Entity for BlobReferenceDeleteIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BlobReferencePutIn { pub _format: i64, pub archiveDataType: i64, @@ -181,7 +181,7 @@ impl Entity for BlobReferencePutIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BlobServerAccessInfo { pub _id: Option, pub blobAccessToken: String, @@ -201,7 +201,7 @@ impl Entity for BlobServerAccessInfo { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BlobServerUrl { pub _id: Option, pub url: String, @@ -219,7 +219,7 @@ impl Entity for BlobServerUrl { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BlobWriteData { pub _id: Option, pub archiveOwnerGroup: GeneratedId, @@ -237,7 +237,7 @@ impl Entity for BlobWriteData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct InstanceId { pub _id: Option, pub instanceId: Option, diff --git a/tuta-sdk/rust/sdk/src/entities/generated/sys.rs b/tuta-sdk/rust/sdk/src/entities/generated/sys.rs index 05d4c9f4fbb..d27d53b1ab8 100644 --- a/tuta-sdk/rust/sdk/src/entities/generated/sys.rs +++ b/tuta-sdk/rust/sdk/src/entities/generated/sys.rs @@ -4,7 +4,7 @@ use super::super::*; use serde::{Deserialize, Serialize}; #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct AccountingInfo { pub _format: i64, pub _id: Option, @@ -45,7 +45,7 @@ impl Entity for AccountingInfo { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct AdminGroupKeyAuthenticationData { pub _id: Option, #[serde(with = "serde_bytes")] @@ -66,7 +66,7 @@ impl Entity for AdminGroupKeyAuthenticationData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct AdminGroupKeyRotationPostIn { pub _format: i64, pub adminGroupKeyAuthenticationDataList: Vec, @@ -86,7 +86,7 @@ impl Entity for AdminGroupKeyRotationPostIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct AdministratedGroup { pub _format: i64, pub _id: Option, @@ -109,7 +109,7 @@ impl Entity for AdministratedGroup { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct AdministratedGroupsRef { pub _id: Option, pub items: GeneratedId, @@ -127,7 +127,7 @@ impl Entity for AdministratedGroupsRef { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct AffiliatePartnerKpiMonthSummary { pub _id: Option, pub commission: i64, @@ -150,7 +150,7 @@ impl Entity for AffiliatePartnerKpiMonthSummary { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct AffiliatePartnerKpiServiceGetOut { pub _format: i64, pub accumulatedCommission: i64, @@ -171,7 +171,7 @@ impl Entity for AffiliatePartnerKpiServiceGetOut { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct AlarmInfo { pub _id: Option, pub alarmIdentifier: String, @@ -192,7 +192,7 @@ impl Entity for AlarmInfo { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct AlarmNotification { pub _id: Option, pub eventEnd: DateTime, @@ -218,7 +218,7 @@ impl Entity for AlarmNotification { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct AlarmServicePost { pub _format: i64, pub alarmNotifications: Vec, @@ -238,7 +238,7 @@ impl Entity for AlarmServicePost { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ArchiveRef { pub _id: Option, pub archiveId: GeneratedId, @@ -256,7 +256,7 @@ impl Entity for ArchiveRef { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ArchiveType { pub _id: Option, pub active: ArchiveRef, @@ -277,7 +277,7 @@ impl Entity for ArchiveType { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct AuditLogEntry { pub _format: i64, pub _id: Option, @@ -309,7 +309,7 @@ impl Entity for AuditLogEntry { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct AuditLogRef { pub _id: Option, pub items: GeneratedId, @@ -327,7 +327,7 @@ impl Entity for AuditLogRef { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct AuthenticatedDevice { pub _id: Option, pub authType: i64, @@ -348,7 +348,7 @@ impl Entity for AuthenticatedDevice { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Authentication { pub _id: Option, pub accessToken: Option, @@ -369,7 +369,7 @@ impl Entity for Authentication { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct AutoLoginDataDelete { pub _format: i64, pub deviceToken: String, @@ -387,7 +387,7 @@ impl Entity for AutoLoginDataDelete { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct AutoLoginDataGet { pub _format: i64, pub deviceToken: String, @@ -406,7 +406,7 @@ impl Entity for AutoLoginDataGet { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct AutoLoginDataReturn { pub _format: i64, #[serde(with = "serde_bytes")] @@ -425,7 +425,7 @@ impl Entity for AutoLoginDataReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct AutoLoginPostReturn { pub _format: i64, pub deviceToken: String, @@ -443,7 +443,7 @@ impl Entity for AutoLoginPostReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Blob { pub _id: Option, pub archiveId: GeneratedId, @@ -463,7 +463,7 @@ impl Entity for Blob { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BlobReferenceTokenWrapper { pub _id: Option, pub blobReferenceToken: String, @@ -481,7 +481,7 @@ impl Entity for BlobReferenceTokenWrapper { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Booking { pub _area: i64, pub _format: i64, @@ -509,7 +509,7 @@ impl Entity for Booking { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BookingItem { pub _id: Option, pub currentCount: i64, @@ -533,7 +533,7 @@ impl Entity for BookingItem { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BookingsRef { pub _id: Option, pub items: GeneratedId, @@ -551,7 +551,7 @@ impl Entity for BookingsRef { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BootstrapFeature { pub _id: Option, pub feature: i64, @@ -569,7 +569,7 @@ impl Entity for BootstrapFeature { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Braintree3ds2Request { pub _id: Option, pub bin: String, @@ -589,7 +589,7 @@ impl Entity for Braintree3ds2Request { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Braintree3ds2Response { pub _id: Option, pub clientToken: String, @@ -608,7 +608,7 @@ impl Entity for Braintree3ds2Response { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BrandingDomainData { pub _format: i64, pub domain: String, @@ -634,7 +634,7 @@ impl Entity for BrandingDomainData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BrandingDomainDeleteData { pub _format: i64, pub domain: String, @@ -652,7 +652,7 @@ impl Entity for BrandingDomainDeleteData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BrandingDomainGetReturn { pub _format: i64, pub certificateInfo: Option, @@ -670,7 +670,7 @@ impl Entity for BrandingDomainGetReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Bucket { pub _id: Option, pub bucketPermissions: GeneratedId, @@ -688,7 +688,7 @@ impl Entity for Bucket { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BucketKey { pub _id: Option, #[serde(with = "serde_bytes")] @@ -714,7 +714,7 @@ impl Entity for BucketKey { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct BucketPermission { pub _format: i64, pub _id: Option, @@ -748,7 +748,7 @@ impl Entity for BucketPermission { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CalendarEventRef { pub _id: Option, pub elementId: CustomId, @@ -767,7 +767,7 @@ impl Entity for CalendarEventRef { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CertificateInfo { pub _id: Option, pub expiryDate: Option, @@ -789,7 +789,7 @@ impl Entity for CertificateInfo { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Challenge { pub _id: Option, #[serde(rename = "type")] @@ -810,7 +810,7 @@ impl Entity for Challenge { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ChangeKdfPostIn { pub _format: i64, pub kdfVersion: i64, @@ -837,7 +837,7 @@ impl Entity for ChangeKdfPostIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ChangePasswordPostIn { pub _format: i64, pub code: Option, @@ -867,7 +867,7 @@ impl Entity for ChangePasswordPostIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Chat { pub _id: Option, pub recipient: GeneratedId, @@ -887,7 +887,7 @@ impl Entity for Chat { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CloseSessionServicePost { pub _format: i64, pub accessToken: String, @@ -906,7 +906,7 @@ impl Entity for CloseSessionServicePost { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CreateCustomerServerPropertiesData { pub _format: i64, #[serde(with = "serde_bytes")] @@ -926,7 +926,7 @@ impl Entity for CreateCustomerServerPropertiesData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CreateCustomerServerPropertiesReturn { pub _format: i64, pub id: GeneratedId, @@ -944,7 +944,7 @@ impl Entity for CreateCustomerServerPropertiesReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CreateSessionData { pub _format: i64, #[serde(with = "serde_bytes")] @@ -969,7 +969,7 @@ impl Entity for CreateSessionData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CreateSessionReturn { pub _format: i64, pub accessToken: String, @@ -989,7 +989,7 @@ impl Entity for CreateSessionReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CreditCard { pub _id: Option, pub cardHolderName: String, @@ -1012,7 +1012,7 @@ impl Entity for CreditCard { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CustomDomainCheckGetIn { pub _format: i64, pub domain: String, @@ -1031,7 +1031,7 @@ impl Entity for CustomDomainCheckGetIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CustomDomainCheckGetOut { pub _format: i64, pub checkResult: i64, @@ -1052,7 +1052,7 @@ impl Entity for CustomDomainCheckGetOut { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CustomDomainData { pub _format: i64, pub domain: String, @@ -1071,7 +1071,7 @@ impl Entity for CustomDomainData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CustomDomainReturn { pub _format: i64, pub validationResult: i64, @@ -1090,7 +1090,7 @@ impl Entity for CustomDomainReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Customer { pub _format: i64, pub _id: Option, @@ -1132,7 +1132,7 @@ impl Entity for Customer { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CustomerAccountTerminationPostIn { pub _format: i64, pub terminationDate: Option, @@ -1151,7 +1151,7 @@ impl Entity for CustomerAccountTerminationPostIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CustomerAccountTerminationPostOut { pub _format: i64, pub terminationRequest: IdTupleGenerated, @@ -1169,7 +1169,7 @@ impl Entity for CustomerAccountTerminationPostOut { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CustomerAccountTerminationRequest { pub _format: i64, pub _id: Option, @@ -1192,7 +1192,7 @@ impl Entity for CustomerAccountTerminationRequest { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CustomerInfo { pub _format: i64, pub _id: Option, @@ -1240,7 +1240,7 @@ impl Entity for CustomerInfo { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CustomerProperties { pub _format: i64, pub _id: Option, @@ -1266,7 +1266,7 @@ impl Entity for CustomerProperties { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CustomerServerProperties { pub _format: i64, pub _id: Option, @@ -1296,7 +1296,7 @@ impl Entity for CustomerServerProperties { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct DateWrapper { pub _id: Option, pub date: DateTime, @@ -1315,7 +1315,7 @@ impl Entity for DateWrapper { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct DebitServicePutData { pub _format: i64, pub invoice: Option, @@ -1333,7 +1333,7 @@ impl Entity for DebitServicePutData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct DeleteCustomerData { pub _format: i64, #[serde(with = "serde_bytes")] @@ -1357,7 +1357,7 @@ impl Entity for DeleteCustomerData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct DnsRecord { pub _id: Option, pub subdomain: Option, @@ -1378,7 +1378,7 @@ impl Entity for DnsRecord { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct DomainInfo { pub _id: Option, pub domain: String, @@ -1399,7 +1399,7 @@ impl Entity for DomainInfo { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct DomainMailAddressAvailabilityData { pub _format: i64, pub mailAddress: String, @@ -1417,7 +1417,7 @@ impl Entity for DomainMailAddressAvailabilityData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct DomainMailAddressAvailabilityReturn { pub _format: i64, pub available: bool, @@ -1435,7 +1435,7 @@ impl Entity for DomainMailAddressAvailabilityReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct EmailSenderListElement { pub _id: Option, pub field: i64, @@ -1458,7 +1458,7 @@ impl Entity for EmailSenderListElement { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct EntityEventBatch { pub _format: i64, pub _id: Option, @@ -1479,7 +1479,7 @@ impl Entity for EntityEventBatch { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct EntityUpdate { pub _id: Option, pub application: String, @@ -1502,7 +1502,7 @@ impl Entity for EntityUpdate { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SysException { pub _id: Option, pub msg: String, @@ -1522,7 +1522,7 @@ impl Entity for SysException { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ExternalPropertiesReturn { pub _format: i64, pub accountType: i64, @@ -1543,7 +1543,7 @@ impl Entity for ExternalPropertiesReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ExternalUserReference { pub _format: i64, pub _id: Option, @@ -1565,7 +1565,7 @@ impl Entity for ExternalUserReference { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Feature { pub _id: Option, pub feature: i64, @@ -1583,7 +1583,7 @@ impl Entity for Feature { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct File { pub _id: Option, #[serde(with = "serde_bytes")] @@ -1604,7 +1604,7 @@ impl Entity for File { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GeneratedIdWrapper { pub _id: Option, pub value: GeneratedId, @@ -1622,7 +1622,7 @@ impl Entity for GeneratedIdWrapper { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GiftCard { pub _format: i64, pub _id: Option, @@ -1652,7 +1652,7 @@ impl Entity for GiftCard { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GiftCardCreateData { pub _format: i64, #[serde(with = "serde_bytes")] @@ -1678,7 +1678,7 @@ impl Entity for GiftCardCreateData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GiftCardCreateReturn { pub _format: i64, pub giftCard: IdTupleGenerated, @@ -1696,7 +1696,7 @@ impl Entity for GiftCardCreateReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GiftCardDeleteData { pub _format: i64, pub giftCard: IdTupleGenerated, @@ -1714,7 +1714,7 @@ impl Entity for GiftCardDeleteData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GiftCardGetReturn { pub _format: i64, pub maxPerPeriod: i64, @@ -1734,7 +1734,7 @@ impl Entity for GiftCardGetReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GiftCardOption { pub _id: Option, pub value: i64, @@ -1752,7 +1752,7 @@ impl Entity for GiftCardOption { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GiftCardRedeemData { pub _format: i64, pub countryCode: String, @@ -1773,7 +1773,7 @@ impl Entity for GiftCardRedeemData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GiftCardRedeemGetReturn { pub _format: i64, pub message: String, @@ -1795,7 +1795,7 @@ impl Entity for GiftCardRedeemGetReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GiftCardsRef { pub _id: Option, pub items: GeneratedId, @@ -1813,7 +1813,7 @@ impl Entity for GiftCardsRef { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Group { pub _format: i64, pub _id: Option, @@ -1853,7 +1853,7 @@ impl Entity for Group { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GroupInfo { pub _format: i64, pub _id: Option, @@ -1888,7 +1888,7 @@ impl Entity for GroupInfo { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GroupKey { pub _format: i64, pub _id: Option, @@ -1916,7 +1916,7 @@ impl Entity for GroupKey { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GroupKeyRotationData { pub _id: Option, #[serde(with = "serde_bytes")] @@ -1943,7 +1943,7 @@ impl Entity for GroupKeyRotationData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GroupKeyRotationInfoGetOut { pub _format: i64, pub userOrAdminGroupKeyRotationScheduled: bool, @@ -1962,7 +1962,7 @@ impl Entity for GroupKeyRotationInfoGetOut { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GroupKeyRotationPostIn { pub _format: i64, pub groupKeyUpdates: Vec, @@ -1980,7 +1980,7 @@ impl Entity for GroupKeyRotationPostIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GroupKeyUpdate { pub _format: i64, pub _id: Option, @@ -2009,7 +2009,7 @@ impl Entity for GroupKeyUpdate { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GroupKeyUpdateData { pub _id: Option, #[serde(with = "serde_bytes")] @@ -2032,7 +2032,7 @@ impl Entity for GroupKeyUpdateData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GroupKeyUpdatesRef { pub _id: Option, pub list: GeneratedId, @@ -2050,7 +2050,7 @@ impl Entity for GroupKeyUpdatesRef { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GroupKeysRef { pub _id: Option, pub list: GeneratedId, @@ -2068,7 +2068,7 @@ impl Entity for GroupKeysRef { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GroupMember { pub _format: i64, pub _id: Option, @@ -2092,7 +2092,7 @@ impl Entity for GroupMember { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GroupMembership { pub _id: Option, pub admin: bool, @@ -2119,7 +2119,7 @@ impl Entity for GroupMembership { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GroupMembershipKeyData { pub _id: Option, pub groupKeyVersion: i64, @@ -2141,7 +2141,7 @@ impl Entity for GroupMembershipKeyData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GroupMembershipUpdateData { pub _id: Option, #[serde(with = "serde_bytes")] @@ -2162,7 +2162,7 @@ impl Entity for GroupMembershipUpdateData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GroupRoot { pub _format: i64, pub _id: Option, @@ -2185,7 +2185,7 @@ impl Entity for GroupRoot { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct IdTupleWrapper { pub _id: Option, pub listElementId: GeneratedId, @@ -2204,7 +2204,7 @@ impl Entity for IdTupleWrapper { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct InstanceSessionKey { pub _id: Option, #[serde(with = "serde_bytes")] @@ -2229,7 +2229,7 @@ impl Entity for InstanceSessionKey { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Invoice { pub _format: i64, pub _id: Option, @@ -2271,7 +2271,7 @@ impl Entity for Invoice { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct InvoiceDataGetIn { pub _format: i64, pub invoiceNumber: String, @@ -2289,7 +2289,7 @@ impl Entity for InvoiceDataGetIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct InvoiceDataGetOut { pub _format: i64, pub address: String, @@ -2319,7 +2319,7 @@ impl Entity for InvoiceDataGetOut { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct InvoiceDataItem { pub _id: Option, pub amount: i64, @@ -2342,7 +2342,7 @@ impl Entity for InvoiceDataItem { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct InvoiceInfo { pub _format: i64, pub _id: Option, @@ -2376,7 +2376,7 @@ impl Entity for InvoiceInfo { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct InvoiceItem { pub _id: Option, pub amount: i64, @@ -2402,7 +2402,7 @@ impl Entity for InvoiceItem { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct KeyPair { pub _id: Option, #[serde(with = "serde_bytes")] @@ -2431,7 +2431,7 @@ impl Entity for KeyPair { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct KeyRotation { pub _format: i64, pub _id: Option, @@ -2454,7 +2454,7 @@ impl Entity for KeyRotation { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct KeyRotationsRef { pub _id: Option, pub list: GeneratedId, @@ -2472,7 +2472,7 @@ impl Entity for KeyRotationsRef { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct LocalAdminGroupReplacementData { pub _id: Option, #[serde(with = "serde_bytes")] @@ -2494,7 +2494,7 @@ impl Entity for LocalAdminGroupReplacementData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct LocalAdminRemovalPostIn { pub _format: i64, pub groupUpdates: Vec, @@ -2512,7 +2512,7 @@ impl Entity for LocalAdminRemovalPostIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct LocationServiceGetReturn { pub _format: i64, pub country: String, @@ -2530,7 +2530,7 @@ impl Entity for LocationServiceGetReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Login { pub _format: i64, pub _id: Option, @@ -2551,7 +2551,7 @@ impl Entity for Login { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailAddressAlias { pub _id: Option, pub enabled: bool, @@ -2570,7 +2570,7 @@ impl Entity for MailAddressAlias { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailAddressAliasGetIn { pub _format: i64, pub targetGroup: GeneratedId, @@ -2588,7 +2588,7 @@ impl Entity for MailAddressAliasGetIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailAddressAliasServiceData { pub _format: i64, pub mailAddress: String, @@ -2607,7 +2607,7 @@ impl Entity for MailAddressAliasServiceData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailAddressAliasServiceDataDelete { pub _format: i64, pub mailAddress: String, @@ -2627,7 +2627,7 @@ impl Entity for MailAddressAliasServiceDataDelete { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailAddressAliasServiceReturn { pub _format: i64, pub enabledAliases: i64, @@ -2648,7 +2648,7 @@ impl Entity for MailAddressAliasServiceReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailAddressAvailability { pub _id: Option, pub available: bool, @@ -2667,7 +2667,7 @@ impl Entity for MailAddressAvailability { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailAddressToGroup { pub _format: i64, pub _id: Option, @@ -2688,7 +2688,7 @@ impl Entity for MailAddressToGroup { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MembershipAddData { pub _format: i64, pub groupKeyVersion: i64, @@ -2711,7 +2711,7 @@ impl Entity for MembershipAddData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MembershipPutIn { pub _format: i64, pub groupKeyUpdates: Vec, @@ -2729,7 +2729,7 @@ impl Entity for MembershipPutIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MembershipRemoveData { pub _format: i64, pub group: GeneratedId, @@ -2748,7 +2748,7 @@ impl Entity for MembershipRemoveData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MissedNotification { pub _format: i64, pub _id: Option, @@ -2778,7 +2778,7 @@ impl Entity for MissedNotification { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MultipleMailAddressAvailabilityData { pub _format: i64, pub mailAddresses: Vec, @@ -2796,7 +2796,7 @@ impl Entity for MultipleMailAddressAvailabilityData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MultipleMailAddressAvailabilityReturn { pub _format: i64, pub availabilities: Vec, @@ -2814,7 +2814,7 @@ impl Entity for MultipleMailAddressAvailabilityReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct NotificationInfo { pub _id: Option, pub mailAddress: String, @@ -2834,7 +2834,7 @@ impl Entity for NotificationInfo { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct NotificationMailTemplate { pub _id: Option, pub body: String, @@ -2854,7 +2854,7 @@ impl Entity for NotificationMailTemplate { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct NotificationSessionKey { pub _id: Option, #[serde(with = "serde_bytes")] @@ -2874,7 +2874,7 @@ impl Entity for NotificationSessionKey { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct OrderProcessingAgreement { pub _format: i64, pub _id: Option, @@ -2904,7 +2904,7 @@ impl Entity for OrderProcessingAgreement { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct OtpChallenge { pub _id: Option, pub secondFactors: Vec, @@ -2922,7 +2922,7 @@ impl Entity for OtpChallenge { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PaymentDataServiceGetData { pub _format: i64, pub clientType: Option, @@ -2940,7 +2940,7 @@ impl Entity for PaymentDataServiceGetData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PaymentDataServiceGetReturn { pub _format: i64, pub loginUrl: String, @@ -2958,7 +2958,7 @@ impl Entity for PaymentDataServiceGetReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PaymentDataServicePostData { pub _format: i64, pub braintree3dsResponse: Braintree3ds2Response, @@ -2976,7 +2976,7 @@ impl Entity for PaymentDataServicePostData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PaymentDataServicePutData { pub _format: i64, pub confirmedCountry: Option, @@ -3005,7 +3005,7 @@ impl Entity for PaymentDataServicePutData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PaymentDataServicePutReturn { pub _format: i64, pub result: i64, @@ -3024,7 +3024,7 @@ impl Entity for PaymentDataServicePutReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PaymentErrorInfo { pub _id: Option, pub errorCode: String, @@ -3044,7 +3044,7 @@ impl Entity for PaymentErrorInfo { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Permission { pub _format: i64, pub _id: Option, @@ -3079,7 +3079,7 @@ impl Entity for Permission { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PlanConfiguration { pub _id: Option, pub autoResponder: bool, @@ -3106,7 +3106,7 @@ impl Entity for PlanConfiguration { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PlanPrices { pub _id: Option, pub additionalUserPriceMonthly: i64, @@ -3136,7 +3136,7 @@ impl Entity for PlanPrices { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PlanServiceGetOut { pub _format: i64, pub config: PlanConfiguration, @@ -3154,7 +3154,7 @@ impl Entity for PlanServiceGetOut { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PriceData { pub _id: Option, pub paymentInterval: i64, @@ -3175,7 +3175,7 @@ impl Entity for PriceData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PriceItemData { pub _id: Option, pub count: i64, @@ -3196,7 +3196,7 @@ impl Entity for PriceItemData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PriceRequestData { pub _id: Option, pub accountType: Option, @@ -3219,7 +3219,7 @@ impl Entity for PriceRequestData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PriceServiceData { pub _format: i64, pub date: Option, @@ -3238,7 +3238,7 @@ impl Entity for PriceServiceData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PriceServiceReturn { pub _format: i64, pub currentPeriodAddedPrice: Option, @@ -3260,7 +3260,7 @@ impl Entity for PriceServiceReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PubEncKeyData { pub _id: Option, pub protocolVersion: i64, @@ -3284,7 +3284,7 @@ impl Entity for PubEncKeyData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PublicKeyGetIn { pub _format: i64, pub identifier: String, @@ -3304,7 +3304,7 @@ impl Entity for PublicKeyGetIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PublicKeyGetOut { pub _format: i64, #[serde(with = "serde_bytes")] @@ -3328,7 +3328,7 @@ impl Entity for PublicKeyGetOut { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PublicKeyPutIn { pub _format: i64, #[serde(with = "serde_bytes")] @@ -3350,7 +3350,7 @@ impl Entity for PublicKeyPutIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PushIdentifier { pub _area: i64, pub _format: i64, @@ -3385,7 +3385,7 @@ impl Entity for PushIdentifier { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PushIdentifierList { pub _id: Option, pub list: GeneratedId, @@ -3403,7 +3403,7 @@ impl Entity for PushIdentifierList { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ReceivedGroupInvitation { pub _format: i64, pub _id: Option, @@ -3439,7 +3439,7 @@ impl Entity for ReceivedGroupInvitation { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct RecoverCode { pub _format: i64, pub _id: Option, @@ -3466,7 +3466,7 @@ impl Entity for RecoverCode { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct RecoverCodeData { pub _id: Option, #[serde(with = "serde_bytes")] @@ -3490,7 +3490,7 @@ impl Entity for RecoverCodeData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ReferralCodeGetIn { pub _format: i64, pub referralCode: GeneratedId, @@ -3508,7 +3508,7 @@ impl Entity for ReferralCodeGetIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ReferralCodePostIn { pub _format: i64, } @@ -3525,7 +3525,7 @@ impl Entity for ReferralCodePostIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ReferralCodePostOut { pub _format: i64, pub referralCode: GeneratedId, @@ -3543,7 +3543,7 @@ impl Entity for ReferralCodePostOut { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct RegistrationCaptchaServiceData { pub _format: i64, pub response: String, @@ -3562,7 +3562,7 @@ impl Entity for RegistrationCaptchaServiceData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct RegistrationCaptchaServiceGetData { pub _format: i64, pub businessUseSelected: bool, @@ -3584,7 +3584,7 @@ impl Entity for RegistrationCaptchaServiceGetData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct RegistrationCaptchaServiceReturn { pub _format: i64, #[serde(with = "serde_bytes")] @@ -3604,7 +3604,7 @@ impl Entity for RegistrationCaptchaServiceReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct RegistrationReturn { pub _format: i64, pub authToken: String, @@ -3622,7 +3622,7 @@ impl Entity for RegistrationReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct RegistrationServiceData { pub _format: i64, pub source: Option, @@ -3642,7 +3642,7 @@ impl Entity for RegistrationServiceData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct RejectedSender { pub _format: i64, pub _id: Option, @@ -3667,7 +3667,7 @@ impl Entity for RejectedSender { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct RejectedSendersRef { pub _id: Option, pub items: GeneratedId, @@ -3685,7 +3685,7 @@ impl Entity for RejectedSendersRef { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct RepeatRule { pub _id: Option, pub endType: i64, @@ -3709,7 +3709,7 @@ impl Entity for RepeatRule { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ResetFactorsDeleteData { pub _format: i64, pub authVerifier: String, @@ -3729,7 +3729,7 @@ impl Entity for ResetFactorsDeleteData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ResetPasswordPostIn { pub _format: i64, pub kdfVersion: i64, @@ -3755,7 +3755,7 @@ impl Entity for ResetPasswordPostIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct RootInstance { pub _format: i64, pub _id: Option, @@ -3776,7 +3776,7 @@ impl Entity for RootInstance { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SaltData { pub _format: i64, pub mailAddress: String, @@ -3794,7 +3794,7 @@ impl Entity for SaltData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SaltReturn { pub _format: i64, pub kdfVersion: i64, @@ -3814,7 +3814,7 @@ impl Entity for SaltReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SecondFactor { pub _format: i64, pub _id: Option, @@ -3840,7 +3840,7 @@ impl Entity for SecondFactor { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SecondFactorAuthAllowedReturn { pub _format: i64, pub allowed: bool, @@ -3858,7 +3858,7 @@ impl Entity for SecondFactorAuthAllowedReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SecondFactorAuthData { pub _format: i64, pub otpCode: Option, @@ -3881,7 +3881,7 @@ impl Entity for SecondFactorAuthData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SecondFactorAuthDeleteData { pub _format: i64, pub session: IdTupleCustom, @@ -3899,7 +3899,7 @@ impl Entity for SecondFactorAuthDeleteData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SecondFactorAuthGetData { pub _format: i64, pub accessToken: String, @@ -3917,7 +3917,7 @@ impl Entity for SecondFactorAuthGetData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SecondFactorAuthGetReturn { pub _format: i64, pub secondFactorPending: bool, @@ -3935,7 +3935,7 @@ impl Entity for SecondFactorAuthGetReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SecondFactorAuthentication { pub _format: i64, pub _id: Option, @@ -3959,7 +3959,7 @@ impl Entity for SecondFactorAuthentication { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SendRegistrationCodeData { pub _format: i64, pub accountType: i64, @@ -3980,7 +3980,7 @@ impl Entity for SendRegistrationCodeData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SendRegistrationCodeReturn { pub _format: i64, pub authToken: String, @@ -3998,7 +3998,7 @@ impl Entity for SendRegistrationCodeReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SentGroupInvitation { pub _format: i64, pub _id: Option, @@ -4022,7 +4022,7 @@ impl Entity for SentGroupInvitation { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Session { pub _format: i64, pub _id: Option, @@ -4056,7 +4056,7 @@ impl Entity for Session { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SignOrderProcessingAgreementData { pub _format: i64, pub customerAddress: String, @@ -4075,7 +4075,7 @@ impl Entity for SignOrderProcessingAgreementData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SseConnectData { pub _format: i64, pub identifier: String, @@ -4094,7 +4094,7 @@ impl Entity for SseConnectData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct StringConfigValue { pub _id: Option, pub name: String, @@ -4113,7 +4113,7 @@ impl Entity for StringConfigValue { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct StringWrapper { pub _id: Option, pub value: String, @@ -4131,7 +4131,7 @@ impl Entity for StringWrapper { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SurveyData { pub _id: Option, pub category: i64, @@ -4152,7 +4152,7 @@ impl Entity for SurveyData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SwitchAccountTypePostIn { pub _format: i64, pub accountType: i64, @@ -4176,7 +4176,7 @@ impl Entity for SwitchAccountTypePostIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SystemKeysReturn { pub _format: i64, #[serde(with = "serde_bytes")] @@ -4208,7 +4208,7 @@ impl Entity for SystemKeysReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct TakeOverDeletedAddressData { pub _format: i64, pub authVerifier: String, @@ -4229,7 +4229,7 @@ impl Entity for TakeOverDeletedAddressData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct TypeInfo { pub _id: Option, pub application: String, @@ -4248,7 +4248,7 @@ impl Entity for TypeInfo { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct U2fChallenge { pub _id: Option, #[serde(with = "serde_bytes")] @@ -4268,7 +4268,7 @@ impl Entity for U2fChallenge { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct U2fKey { pub _id: Option, pub appId: String, @@ -4289,7 +4289,7 @@ impl Entity for U2fKey { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct U2fRegisteredDevice { pub _id: Option, pub appId: String, @@ -4313,7 +4313,7 @@ impl Entity for U2fRegisteredDevice { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct U2fResponseData { pub _id: Option, pub clientData: String, @@ -4333,7 +4333,7 @@ impl Entity for U2fResponseData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UpdatePermissionKeyData { pub _format: i64, #[serde(with = "serde_bytes")] @@ -4355,7 +4355,7 @@ impl Entity for UpdatePermissionKeyData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UpdateSessionKeysPostIn { pub _format: i64, pub ownerEncSessionKeys: Vec, @@ -4373,7 +4373,7 @@ impl Entity for UpdateSessionKeysPostIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UpgradePriceServiceData { pub _format: i64, pub campaign: Option, @@ -4393,7 +4393,7 @@ impl Entity for UpgradePriceServiceData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UpgradePriceServiceReturn { pub _format: i64, pub bonusMonthsForYearlyPlan: i64, @@ -4425,7 +4425,7 @@ impl Entity for UpgradePriceServiceReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct User { pub _format: i64, pub _id: Option, @@ -4464,7 +4464,7 @@ impl Entity for User { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UserAlarmInfo { pub _format: i64, pub _id: Option, @@ -4490,7 +4490,7 @@ impl Entity for UserAlarmInfo { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UserAlarmInfoListType { pub _id: Option, pub alarms: GeneratedId, @@ -4508,7 +4508,7 @@ impl Entity for UserAlarmInfoListType { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UserAreaGroups { pub _id: Option, pub list: GeneratedId, @@ -4526,7 +4526,7 @@ impl Entity for UserAreaGroups { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UserAuthentication { pub _id: Option, pub recoverCode: Option, @@ -4546,7 +4546,7 @@ impl Entity for UserAuthentication { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UserDataDelete { pub _format: i64, pub date: Option, @@ -4566,7 +4566,7 @@ impl Entity for UserDataDelete { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UserExternalAuthInfo { pub _id: Option, pub authUpdateCounter: i64, @@ -4589,7 +4589,7 @@ impl Entity for UserExternalAuthInfo { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UserGroupKeyDistribution { pub _format: i64, pub _id: Option, @@ -4612,7 +4612,7 @@ impl Entity for UserGroupKeyDistribution { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UserGroupKeyRotationData { pub _id: Option, #[serde(with = "serde_bytes")] @@ -4645,7 +4645,7 @@ impl Entity for UserGroupKeyRotationData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UserGroupKeyRotationPostIn { pub _format: i64, pub userGroupKeyData: UserGroupKeyRotationData, @@ -4663,7 +4663,7 @@ impl Entity for UserGroupKeyRotationPostIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UserGroupRoot { pub _format: i64, pub _id: Option, @@ -4686,7 +4686,7 @@ impl Entity for UserGroupRoot { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct VariableExternalAuthInfo { pub _format: i64, pub _id: Option, @@ -4714,7 +4714,7 @@ impl Entity for VariableExternalAuthInfo { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct VerifyRegistrationCodeData { pub _format: i64, pub authToken: String, @@ -4733,7 +4733,7 @@ impl Entity for VerifyRegistrationCodeData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Version { pub _id: Option, pub operation: String, @@ -4755,7 +4755,7 @@ impl Entity for Version { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct VersionData { pub _format: i64, pub application: String, @@ -4776,7 +4776,7 @@ impl Entity for VersionData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct VersionInfo { pub _format: i64, pub _id: Option, @@ -4806,7 +4806,7 @@ impl Entity for VersionInfo { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct VersionReturn { pub _format: i64, pub versions: Vec, @@ -4824,7 +4824,7 @@ impl Entity for VersionReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct WebauthnResponseData { pub _id: Option, #[serde(with = "serde_bytes")] @@ -4849,7 +4849,7 @@ impl Entity for WebauthnResponseData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct WebsocketCounterData { pub _format: i64, pub mailGroup: GeneratedId, @@ -4868,7 +4868,7 @@ impl Entity for WebsocketCounterData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct WebsocketCounterValue { pub _id: Option, pub count: i64, @@ -4887,7 +4887,7 @@ impl Entity for WebsocketCounterValue { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct WebsocketEntityData { pub _format: i64, pub eventBatchId: GeneratedId, @@ -4907,7 +4907,7 @@ impl Entity for WebsocketEntityData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct WebsocketLeaderStatus { pub _format: i64, pub leaderStatus: bool, @@ -4925,7 +4925,7 @@ impl Entity for WebsocketLeaderStatus { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct WhitelabelChild { pub _format: i64, pub _id: Option, @@ -4955,7 +4955,7 @@ impl Entity for WhitelabelChild { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct WhitelabelChildrenRef { pub _id: Option, pub items: GeneratedId, @@ -4973,7 +4973,7 @@ impl Entity for WhitelabelChildrenRef { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct WhitelabelConfig { pub _format: i64, pub _id: Option, @@ -5002,7 +5002,7 @@ impl Entity for WhitelabelConfig { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct WhitelabelParent { pub _id: Option, pub customer: GeneratedId, diff --git a/tuta-sdk/rust/sdk/src/entities/generated/tutanota.rs b/tuta-sdk/rust/sdk/src/entities/generated/tutanota.rs index aa9fd753377..f7392c5bd5f 100644 --- a/tuta-sdk/rust/sdk/src/entities/generated/tutanota.rs +++ b/tuta-sdk/rust/sdk/src/entities/generated/tutanota.rs @@ -4,7 +4,7 @@ use super::super::*; use serde::{Deserialize, Serialize}; #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct AttachmentKeyData { pub _id: Option, #[serde(with = "serde_bytes")] @@ -26,7 +26,7 @@ impl Entity for AttachmentKeyData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Birthday { pub _id: Option, pub day: i64, @@ -46,7 +46,7 @@ impl Entity for Birthday { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Body { pub _id: Option, pub compressedText: Option, @@ -66,7 +66,7 @@ impl Entity for Body { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CalendarDeleteData { pub _format: i64, pub groupRootId: GeneratedId, @@ -84,7 +84,7 @@ impl Entity for CalendarDeleteData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CalendarEvent { pub _format: i64, pub _id: Option, @@ -124,7 +124,7 @@ impl Entity for CalendarEvent { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CalendarEventAttendee { pub _id: Option, pub status: i64, @@ -144,7 +144,7 @@ impl Entity for CalendarEventAttendee { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CalendarEventIndexRef { pub _id: Option, pub list: GeneratedId, @@ -162,7 +162,7 @@ impl Entity for CalendarEventIndexRef { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CalendarEventUidIndex { pub _format: i64, pub _id: Option, @@ -184,7 +184,7 @@ impl Entity for CalendarEventUidIndex { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CalendarEventUpdate { pub _format: i64, pub _id: Option, @@ -211,7 +211,7 @@ impl Entity for CalendarEventUpdate { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CalendarEventUpdateList { pub _id: Option, pub list: GeneratedId, @@ -229,7 +229,7 @@ impl Entity for CalendarEventUpdateList { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CalendarGroupRoot { pub _format: i64, pub _id: Option, @@ -257,7 +257,7 @@ impl Entity for CalendarGroupRoot { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CalendarRepeatRule { pub _id: Option, pub endType: i64, @@ -281,7 +281,7 @@ impl Entity for CalendarRepeatRule { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Contact { pub _format: i64, pub _id: Option, @@ -333,7 +333,7 @@ impl Entity for Contact { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ContactAddress { pub _id: Option, pub address: String, @@ -355,7 +355,7 @@ impl Entity for ContactAddress { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ContactCustomDate { pub _id: Option, pub customTypeName: String, @@ -377,7 +377,7 @@ impl Entity for ContactCustomDate { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ContactList { pub _format: i64, pub _id: Option, @@ -404,7 +404,7 @@ impl Entity for ContactList { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ContactListEntry { pub _format: i64, pub _id: Option, @@ -430,7 +430,7 @@ impl Entity for ContactListEntry { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ContactListGroupRoot { pub _format: i64, pub _id: Option, @@ -456,7 +456,7 @@ impl Entity for ContactListGroupRoot { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ContactMailAddress { pub _id: Option, pub address: String, @@ -478,7 +478,7 @@ impl Entity for ContactMailAddress { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ContactMessengerHandle { pub _id: Option, pub customTypeName: String, @@ -500,7 +500,7 @@ impl Entity for ContactMessengerHandle { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ContactPhoneNumber { pub _id: Option, pub customTypeName: String, @@ -522,7 +522,7 @@ impl Entity for ContactPhoneNumber { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ContactPronouns { pub _id: Option, pub language: String, @@ -542,7 +542,7 @@ impl Entity for ContactPronouns { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ContactRelationship { pub _id: Option, pub customTypeName: String, @@ -564,7 +564,7 @@ impl Entity for ContactRelationship { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ContactSocialId { pub _id: Option, pub customTypeName: String, @@ -586,7 +586,7 @@ impl Entity for ContactSocialId { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ContactWebsite { pub _id: Option, pub customTypeName: String, @@ -608,7 +608,7 @@ impl Entity for ContactWebsite { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ConversationEntry { pub _format: i64, pub _id: Option, @@ -632,7 +632,7 @@ impl Entity for ConversationEntry { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CreateExternalUserGroupData { pub _id: Option, #[serde(with = "serde_bytes")] @@ -655,7 +655,7 @@ impl Entity for CreateExternalUserGroupData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CreateGroupPostReturn { pub _format: i64, pub group: GeneratedId, @@ -675,7 +675,7 @@ impl Entity for CreateGroupPostReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CreateMailFolderData { pub _format: i64, pub folderName: String, @@ -700,7 +700,7 @@ impl Entity for CreateMailFolderData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CreateMailFolderReturn { pub _format: i64, pub newFolder: IdTupleGenerated, @@ -720,7 +720,7 @@ impl Entity for CreateMailFolderReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CreateMailGroupData { pub _format: i64, #[serde(with = "serde_bytes")] @@ -743,7 +743,7 @@ impl Entity for CreateMailGroupData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct CustomerAccountCreateData { pub _format: i64, pub accountGroupKeyVersion: i64, @@ -781,7 +781,7 @@ impl Entity for CustomerAccountCreateData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct DefaultAlarmInfo { pub _id: Option, pub trigger: String, @@ -800,7 +800,7 @@ impl Entity for DefaultAlarmInfo { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct DeleteGroupData { pub _format: i64, pub restore: bool, @@ -819,7 +819,7 @@ impl Entity for DeleteGroupData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct DeleteMailData { pub _format: i64, pub folder: Option, @@ -838,7 +838,7 @@ impl Entity for DeleteMailData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct DeleteMailFolderData { pub _format: i64, pub folders: Vec, @@ -858,7 +858,7 @@ impl Entity for DeleteMailFolderData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct DraftAttachment { pub _id: Option, #[serde(with = "serde_bytes")] @@ -880,7 +880,7 @@ impl Entity for DraftAttachment { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct DraftCreateData { pub _format: i64, pub conversationType: i64, @@ -905,7 +905,7 @@ impl Entity for DraftCreateData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct DraftCreateReturn { pub _format: i64, pub draft: IdTupleGenerated, @@ -923,7 +923,7 @@ impl Entity for DraftCreateReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct DraftData { pub _id: Option, pub bodyText: String, @@ -954,7 +954,7 @@ impl Entity for DraftData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct DraftRecipient { pub _id: Option, pub mailAddress: String, @@ -974,7 +974,7 @@ impl Entity for DraftRecipient { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct DraftUpdateData { pub _format: i64, pub draft: IdTupleGenerated, @@ -995,7 +995,7 @@ impl Entity for DraftUpdateData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct DraftUpdateReturn { pub _format: i64, pub attachments: Vec, @@ -1015,7 +1015,7 @@ impl Entity for DraftUpdateReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct EmailTemplate { pub _format: i64, pub _id: Option, @@ -1043,7 +1043,7 @@ impl Entity for EmailTemplate { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct EmailTemplateContent { pub _id: Option, pub languageCode: String, @@ -1063,7 +1063,7 @@ impl Entity for EmailTemplateContent { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct EncryptTutanotaPropertiesData { pub _format: i64, #[serde(with = "serde_bytes")] @@ -1084,7 +1084,7 @@ impl Entity for EncryptTutanotaPropertiesData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct EncryptedMailAddress { pub _id: Option, pub address: String, @@ -1104,7 +1104,7 @@ impl Entity for EncryptedMailAddress { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct EntropyData { pub _format: i64, #[serde(with = "serde_bytes")] @@ -1124,7 +1124,7 @@ impl Entity for EntropyData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ExternalUserData { pub _format: i64, #[serde(with = "serde_bytes")] @@ -1162,7 +1162,7 @@ impl Entity for ExternalUserData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct TutanotaFile { pub _format: i64, pub _id: Option, @@ -1194,7 +1194,7 @@ impl Entity for TutanotaFile { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct FileSystem { pub _format: i64, pub _id: Option, @@ -1220,7 +1220,7 @@ impl Entity for FileSystem { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GroupInvitationDeleteData { pub _format: i64, pub receivedInvitation: IdTupleGenerated, @@ -1238,7 +1238,7 @@ impl Entity for GroupInvitationDeleteData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GroupInvitationPostData { pub _format: i64, pub internalKeyData: Vec, @@ -1257,7 +1257,7 @@ impl Entity for GroupInvitationPostData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GroupInvitationPostReturn { pub _format: i64, pub existingMailAddresses: Vec, @@ -1277,7 +1277,7 @@ impl Entity for GroupInvitationPostReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GroupInvitationPutData { pub _format: i64, #[serde(with = "serde_bytes")] @@ -1301,7 +1301,7 @@ impl Entity for GroupInvitationPutData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct GroupSettings { pub _id: Option, pub color: String, @@ -1324,7 +1324,7 @@ impl Entity for GroupSettings { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Header { pub _id: Option, pub compressedHeaders: Option, @@ -1344,7 +1344,7 @@ impl Entity for Header { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ImapFolder { pub _id: Option, pub lastseenuid: String, @@ -1365,7 +1365,7 @@ impl Entity for ImapFolder { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ImapSyncConfiguration { pub _id: Option, pub host: String, @@ -1387,7 +1387,7 @@ impl Entity for ImapSyncConfiguration { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ImapSyncState { pub _format: i64, pub _id: Option, @@ -1408,7 +1408,7 @@ impl Entity for ImapSyncState { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ImportAttachment { pub _id: Option, #[serde(with = "serde_bytes")] @@ -1430,7 +1430,7 @@ impl Entity for ImportAttachment { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ImportMailData { pub _id: Option, pub compressedBodyText: String, @@ -1466,7 +1466,7 @@ impl Entity for ImportMailData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ImportMailDataMailReference { pub _id: Option, pub reference: String, @@ -1484,7 +1484,7 @@ impl Entity for ImportMailDataMailReference { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ImportMailPostIn { pub _format: i64, #[serde(with = "serde_bytes")] @@ -1509,7 +1509,7 @@ impl Entity for ImportMailPostIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ImportMailPostOut { pub _format: i64, pub mails: Vec, @@ -1527,7 +1527,7 @@ impl Entity for ImportMailPostOut { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct InboxRule { pub _id: Option, #[serde(rename = "type")] @@ -1549,7 +1549,7 @@ impl Entity for InboxRule { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct InternalGroupData { pub _id: Option, #[serde(with = "serde_bytes")] @@ -1585,7 +1585,7 @@ impl Entity for InternalGroupData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct InternalRecipientKeyData { pub _id: Option, pub mailAddress: String, @@ -1608,7 +1608,7 @@ impl Entity for InternalRecipientKeyData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct KnowledgeBaseEntry { pub _format: i64, pub _id: Option, @@ -1636,7 +1636,7 @@ impl Entity for KnowledgeBaseEntry { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct KnowledgeBaseEntryKeyword { pub _id: Option, pub keyword: String, @@ -1655,7 +1655,7 @@ impl Entity for KnowledgeBaseEntryKeyword { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ListUnsubscribeData { pub _format: i64, pub headers: String, @@ -1675,7 +1675,7 @@ impl Entity for ListUnsubscribeData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Mail { pub _format: i64, pub _id: Option, @@ -1722,7 +1722,7 @@ impl Entity for Mail { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailAddress { pub _id: Option, pub address: String, @@ -1743,7 +1743,7 @@ impl Entity for MailAddress { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailAddressProperties { pub _id: Option, pub mailAddress: String, @@ -1763,7 +1763,7 @@ impl Entity for MailAddressProperties { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailBag { pub _id: Option, pub mails: GeneratedId, @@ -1781,7 +1781,7 @@ impl Entity for MailBag { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailBox { pub _format: i64, pub _id: Option, @@ -1815,7 +1815,7 @@ impl Entity for MailBox { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailDetails { pub _id: Option, pub authStatus: i64, @@ -1838,7 +1838,7 @@ impl Entity for MailDetails { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailDetailsBlob { pub _format: i64, pub _id: Option, @@ -1864,7 +1864,7 @@ impl Entity for MailDetailsBlob { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailDetailsDraft { pub _format: i64, pub _id: Option, @@ -1890,7 +1890,7 @@ impl Entity for MailDetailsDraft { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailDetailsDraftsRef { pub _id: Option, pub list: GeneratedId, @@ -1908,7 +1908,7 @@ impl Entity for MailDetailsDraftsRef { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailFolder { pub _format: i64, pub _id: Option, @@ -1940,7 +1940,7 @@ impl Entity for MailFolder { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailFolderRef { pub _id: Option, pub folders: GeneratedId, @@ -1958,7 +1958,7 @@ impl Entity for MailFolderRef { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailSetEntry { pub _format: i64, pub _id: Option, @@ -1979,7 +1979,7 @@ impl Entity for MailSetEntry { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailboxGroupRoot { pub _format: i64, pub _id: Option, @@ -2005,7 +2005,7 @@ impl Entity for MailboxGroupRoot { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailboxProperties { pub _format: i64, pub _id: Option, @@ -2032,7 +2032,7 @@ impl Entity for MailboxProperties { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MailboxServerProperties { pub _format: i64, pub _id: Option, @@ -2053,7 +2053,7 @@ impl Entity for MailboxServerProperties { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct MoveMailData { pub _format: i64, pub mails: Vec, @@ -2073,7 +2073,7 @@ impl Entity for MoveMailData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct NewDraftAttachment { pub _id: Option, #[serde(with = "serde_bytes")] @@ -2097,7 +2097,7 @@ impl Entity for NewDraftAttachment { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct NewImportAttachment { pub _id: Option, #[serde(with = "serde_bytes")] @@ -2125,7 +2125,7 @@ impl Entity for NewImportAttachment { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct NewsId { pub _id: Option, pub newsItemId: GeneratedId, @@ -2144,7 +2144,7 @@ impl Entity for NewsId { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct NewsIn { pub _format: i64, pub newsItemId: Option, @@ -2162,7 +2162,7 @@ impl Entity for NewsIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct NewsOut { pub _format: i64, pub newsItemIds: Vec, @@ -2180,7 +2180,7 @@ impl Entity for NewsOut { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct NotificationMail { pub _id: Option, pub bodyText: String, @@ -2202,7 +2202,7 @@ impl Entity for NotificationMail { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct OutOfOfficeNotification { pub _format: i64, pub _id: Option, @@ -2226,7 +2226,7 @@ impl Entity for OutOfOfficeNotification { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct OutOfOfficeNotificationMessage { pub _id: Option, pub message: String, @@ -2247,7 +2247,7 @@ impl Entity for OutOfOfficeNotificationMessage { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct OutOfOfficeNotificationRecipientList { pub _id: Option, pub list: GeneratedId, @@ -2265,7 +2265,7 @@ impl Entity for OutOfOfficeNotificationRecipientList { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PhishingMarkerWebsocketData { pub _format: i64, pub lastId: GeneratedId, @@ -2284,7 +2284,7 @@ impl Entity for PhishingMarkerWebsocketData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct PhotosRef { pub _id: Option, pub files: GeneratedId, @@ -2302,7 +2302,7 @@ impl Entity for PhotosRef { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ReceiveInfoServiceData { pub _format: i64, pub language: String, @@ -2320,7 +2320,7 @@ impl Entity for ReceiveInfoServiceData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Recipients { pub _id: Option, pub bccRecipients: Vec, @@ -2340,7 +2340,7 @@ impl Entity for Recipients { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct RemoteImapSyncInfo { pub _format: i64, pub _id: Option, @@ -2362,7 +2362,7 @@ impl Entity for RemoteImapSyncInfo { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ReportMailPostData { pub _format: i64, #[serde(with = "serde_bytes")] @@ -2383,7 +2383,7 @@ impl Entity for ReportMailPostData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct ReportedMailFieldMarker { pub _id: Option, pub marker: String, @@ -2402,7 +2402,7 @@ impl Entity for ReportedMailFieldMarker { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SecureExternalRecipientKeyData { pub _id: Option, pub kdfVersion: i64, @@ -2433,7 +2433,7 @@ impl Entity for SecureExternalRecipientKeyData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SendDraftData { pub _format: i64, #[serde(with = "serde_bytes")] @@ -2465,7 +2465,7 @@ impl Entity for SendDraftData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SendDraftReturn { pub _format: i64, pub messageId: String, @@ -2486,7 +2486,7 @@ impl Entity for SendDraftReturn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SharedGroupData { pub _id: Option, #[serde(with = "serde_bytes")] @@ -2518,7 +2518,7 @@ impl Entity for SharedGroupData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SimpleMoveMailPostIn { pub _format: i64, pub destinationSetType: i64, @@ -2537,7 +2537,7 @@ impl Entity for SimpleMoveMailPostIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SpamResults { pub _id: Option, pub list: GeneratedId, @@ -2555,7 +2555,7 @@ impl Entity for SpamResults { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct Subfiles { pub _id: Option, pub files: GeneratedId, @@ -2573,7 +2573,7 @@ impl Entity for Subfiles { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct SymEncInternalRecipientKeyData { pub _id: Option, pub mailAddress: String, @@ -2595,7 +2595,7 @@ impl Entity for SymEncInternalRecipientKeyData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct TemplateGroupRoot { pub _format: i64, pub _id: Option, @@ -2622,7 +2622,7 @@ impl Entity for TemplateGroupRoot { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct TranslationGetIn { pub _format: i64, pub lang: String, @@ -2640,7 +2640,7 @@ impl Entity for TranslationGetIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct TranslationGetOut { pub _format: i64, pub giftCardSubject: String, @@ -2659,7 +2659,7 @@ impl Entity for TranslationGetOut { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct TutanotaProperties { pub _format: i64, pub _id: Option, @@ -2698,7 +2698,7 @@ impl Entity for TutanotaProperties { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UnreadMailStatePostIn { pub _format: i64, pub unread: bool, @@ -2717,7 +2717,7 @@ impl Entity for UnreadMailStatePostIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UpdateMailFolderData { pub _format: i64, pub folder: IdTupleGenerated, @@ -2736,7 +2736,7 @@ impl Entity for UpdateMailFolderData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UserAccountCreateData { pub _format: i64, pub date: Option, @@ -2756,7 +2756,7 @@ impl Entity for UserAccountCreateData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UserAccountUserData { pub _id: Option, #[serde(with = "serde_bytes")] @@ -2814,7 +2814,7 @@ impl Entity for UserAccountUserData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UserAreaGroupData { pub _id: Option, #[serde(with = "serde_bytes")] @@ -2845,7 +2845,7 @@ impl Entity for UserAreaGroupData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UserAreaGroupDeleteData { pub _format: i64, pub group: GeneratedId, @@ -2863,7 +2863,7 @@ impl Entity for UserAreaGroupDeleteData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UserAreaGroupPostData { pub _format: i64, pub groupData: UserAreaGroupData, @@ -2881,7 +2881,7 @@ impl Entity for UserAreaGroupPostData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UserSettingsGroupRoot { pub _format: i64, pub _id: Option, diff --git a/tuta-sdk/rust/sdk/src/entities/generated/usage.rs b/tuta-sdk/rust/sdk/src/entities/generated/usage.rs index f1266c85985..7671513b1ea 100644 --- a/tuta-sdk/rust/sdk/src/entities/generated/usage.rs +++ b/tuta-sdk/rust/sdk/src/entities/generated/usage.rs @@ -4,7 +4,7 @@ use super::super::*; use serde::{Deserialize, Serialize}; #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UsageTestAssignment { pub _id: Option, pub name: String, @@ -26,7 +26,7 @@ impl Entity for UsageTestAssignment { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UsageTestAssignmentIn { pub _format: i64, pub testDeviceId: Option, @@ -44,7 +44,7 @@ impl Entity for UsageTestAssignmentIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UsageTestAssignmentOut { pub _format: i64, pub testDeviceId: GeneratedId, @@ -63,7 +63,7 @@ impl Entity for UsageTestAssignmentOut { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UsageTestMetricConfig { pub _id: Option, pub name: String, @@ -84,7 +84,7 @@ impl Entity for UsageTestMetricConfig { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UsageTestMetricConfigValue { pub _id: Option, pub key: String, @@ -103,7 +103,7 @@ impl Entity for UsageTestMetricConfigValue { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UsageTestMetricData { pub _id: Option, pub name: String, @@ -122,7 +122,7 @@ impl Entity for UsageTestMetricData { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UsageTestParticipationIn { pub _format: i64, pub stage: i64, @@ -143,7 +143,7 @@ impl Entity for UsageTestParticipationIn { #[derive(uniffi::Record, Clone, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Debug))] +#[cfg_attr(any(test, feature = "testing"), derive(PartialEq, Debug))] pub struct UsageTestStage { pub _id: Option, pub maxPings: i64, From 97eb93ac1a0140c34ed56def53d39056dc2c9cf5 Mon Sep 17 00:00:00 2001 From: nig Date: Wed, 13 Nov 2024 15:44:23 +0100 Subject: [PATCH 27/32] use ParsedEntity type alias where appropriate --- packages/node-mimimi/src/importer.rs | 1004 +++++++------- .../src/importer/importable_mail.rs | 1187 +++++++++-------- packages/node-mimimi/src/reduce_to_chunks.rs | 180 +-- 3 files changed, 1197 insertions(+), 1174 deletions(-) diff --git a/packages/node-mimimi/src/importer.rs b/packages/node-mimimi/src/importer.rs index 094b50ae4ff..d315d3a5552 100644 --- a/packages/node-mimimi/src/importer.rs +++ b/packages/node-mimimi/src/importer.rs @@ -9,7 +9,9 @@ use std::sync::{Arc, Mutex}; use tutasdk::crypto::aes::Iv; use tutasdk::crypto::key::GenericAesKey; use tutasdk::crypto::randomizer_facade::RandomizerFacade; -use tutasdk::entities::generated::tutanota::{ImportAttachment, ImportMailData, ImportMailPostIn, NewImportAttachment}; +use tutasdk::entities::generated::tutanota::{ + ImportAttachment, ImportMailData, ImportMailPostIn, NewImportAttachment, +}; use tutasdk::entities::json_size_estimator::estimate_json_size; use tutasdk::login::Credentials; use tutasdk::net::native_rest_client::NativeRestClient; @@ -27,13 +29,13 @@ mod importable_mail; #[derive(Clone, PartialEq)] pub enum ImportParams { - Imap { - imap_import_config: ImapImportConfig, - }, - LocalFile { - file_path: String, - is_mbox: bool, - }, + Imap { + imap_import_config: ImapImportConfig, + }, + LocalFile { + file_path: String, + is_mbox: bool, + }, } /// current state of the imap_reader import for this tuta account @@ -43,541 +45,559 @@ pub enum ImportParams { #[derive(PartialEq, Default)] #[cfg_attr(test, derive(Debug))] pub enum ImportState { - #[default] - NotInitialized, - Paused, - Running, - Postponed, - Finished, + #[default] + NotInitialized, + Paused, + Running, + Postponed, + Finished, } #[cfg_attr(feature = "javascript", napi_derive::napi(object))] #[derive(PartialEq, Clone, Default)] #[cfg_attr(test, derive(Debug))] pub struct ImportStatus { - pub state: ImportState, - pub imported_mails: u32, + pub state: ImportState, + pub imported_mails: u32, } struct Importer { - status: ImportStatus, - logged_in_sdk: Arc, - target_owner_group: GeneratedId, - target_mail_folder: IdTupleGenerated, - import_source: Arc>, - randomizer_facade: RandomizerFacade, + status: ImportStatus, + logged_in_sdk: Arc, + target_owner_group: GeneratedId, + target_mail_folder: IdTupleGenerated, + import_source: Arc>, + randomizer_facade: RandomizerFacade, } pub enum ImportSource { - RemoteImap { imap_import_client: ImapImport }, - LocalFile { fs_email_client: FileImport }, + RemoteImap { imap_import_client: ImapImport }, + LocalFile { fs_email_client: FileImport }, } /// Wrapper for `Importer` to be used from napi-rs interface #[cfg_attr(feature = "javascript", napi_derive::napi)] pub struct ImporterApi { - inner: Arc>, + inner: Arc>, } #[derive(Debug, PartialEq, Clone)] pub enum IterationError { - Imap(ImapIterationError), - File(FileIterationError), + Imap(ImapIterationError), + File(FileIterationError), } struct ImportSourceIterator { - // it would be nice to not need the mutex, but when the importer continues the import, - // it mutates its own state and also calls mutating functions on the source. solving this - // probably requires a bigger restructure of the code (it's very OOP atm) - source: Arc>, + // it would be nice to not need the mutex, but when the importer continues the import, + // it mutates its own state and also calls mutating functions on the source. solving this + // probably requires a bigger restructure of the code (it's very OOP atm) + source: Arc>, } impl Iterator for ImportSourceIterator { - type Item = ImportableMail; - - fn next(&mut self) -> Option { - let mut source = self.source.lock().unwrap(); - let next_importable_mail = match &mut *source { - // the other way (converting fs_source to an async_iterator) would be nicer, but that's a nightly feature - ImportSource::RemoteImap { imap_import_client } => imap_import_client - .fetch_next_mail() - .map_err(IterationError::Imap), - ImportSource::LocalFile { fs_email_client } => fs_email_client - .get_next_importable_mail() - .map_err(IterationError::File), - }; - - match next_importable_mail { - Ok(next_importable_mail) => Some(next_importable_mail), - - // source says, all the iteration have ended, - Err(IterationError::File(FileIterationError::SourceEnd)) - | Err(IterationError::Imap(ImapIterationError::SourceEnd)) => None, - - Err(e) => { - // once we handle this case we will need another iterator that filters (and logs) the - // errors so we don't have to handle the error case during the chunking + upload - panic!("Cannot get next email from source: {e:?}") - } - } - } + type Item = ImportableMail; + + fn next(&mut self) -> Option { + let mut source = self.source.lock().unwrap(); + let next_importable_mail = match &mut *source { + // the other way (converting fs_source to an async_iterator) would be nicer, but that's a nightly feature + ImportSource::RemoteImap { imap_import_client } => imap_import_client + .fetch_next_mail() + .map_err(IterationError::Imap), + ImportSource::LocalFile { fs_email_client } => fs_email_client + .get_next_importable_mail() + .map_err(IterationError::File), + }; + + match next_importable_mail { + Ok(next_importable_mail) => Some(next_importable_mail), + + // source says, all the iteration have ended, + Err(IterationError::File(FileIterationError::SourceEnd)) + | Err(IterationError::Imap(ImapIterationError::SourceEnd)) => None, + + Err(e) => { + // once we handle this case we will need another iterator that filters (and logs) the + // errors so we don't have to handle the error case during the chunking + upload + panic!("Cannot get next email from source: {e:?}") + }, + } + } } impl Importer { - pub async fn continue_import(&mut self) -> Result { - let source_iterator = ImportSourceIterator { - source: Arc::clone(&self.import_source), - }; - let _ = self.import_all_mail(source_iterator).await; - Ok(self.status.clone()) - } - - /// once we get the ImportableMail from either of source, - /// continue to the uploading counterpart - async fn import_all_mail( - &mut self, - importable_mails: Iter, - ) -> Result, ()> - where - Iter: Iterator + Send + 'static, - { - let new_mail_aes_256_key = GenericAesKey::from_bytes( - self.randomizer_facade - .generate_random_array::<{ tutasdk::crypto::aes::AES_256_KEY_SIZE }>() - .as_slice(), - ) - .unwrap(); - let mail_group_key = self - .logged_in_sdk - .get_current_sym_group_key(&self.target_owner_group) - .await - .map_err(|_e| ())?; - let owner_enc_mail_session_key = - mail_group_key.encrypt_key(&new_mail_aes_256_key, Iv::generate(&self.randomizer_facade)); - - const MAX_REQUEST_SIZE: usize = 1024 * 1024 * 5; - let import_mail_data_and_attachments = importable_mails.map(<(ImportMailData, Vec)>::from); - let import_chunks = reduce_to_chunks( - import_mail_data_and_attachments, - MAX_REQUEST_SIZE, - Box::new(|(imd, a)| estimate_json_size(imd)), - ); - - let mut mails: Vec = Vec::new(); - let mut new_status = ImportStatus { - state: ImportState::Running, - imported_mails: 0, - }; - for imports in import_chunks { - let import_len = imports.len(); - - let mut imports_with_attachments = Vec::new(); - for (import_mail_data, importable_mail_attachments) in imports.into_iter() { - let mut import_mail_data = import_mail_data; - let mut import_attachments = Vec::new(); - for importable_mail_attachment in importable_mail_attachments { - let importable_mail_attachment = importable_mail_attachment as ImportableMailAttachment; - let new_file_aes_256_key = GenericAesKey::from_bytes( - self.randomizer_facade - .generate_random_array::<{ tutasdk::crypto::aes::AES_256_KEY_SIZE }>() - .as_slice(), - ).unwrap(); - let owner_enc_file_session_key = mail_group_key - .encrypt_key(&new_file_aes_256_key, Iv::generate(&self.randomizer_facade)); - - let reference_tokens = self.logged_in_sdk - .blob_facade() - .encrypt_and_upload( - ArchiveDataType::Attachments, - &self.target_owner_group, - &new_file_aes_256_key, - importable_mail_attachment.content, - ) - .await - .unwrap(); - - // todo: do we need to upload the ivs and how? - let enc_file_name = new_file_aes_256_key.encrypt_data(importable_mail_attachment.filename.as_ref(), Iv::generate(&self.randomizer_facade)).unwrap(); - let enc_mime_type = new_file_aes_256_key.encrypt_data(importable_mail_attachment.content_type.as_ref(), Iv::generate(&self.randomizer_facade)).unwrap(); - let enc_cid: Option> = match importable_mail_attachment.content_id { - Some(cid) => Some(new_file_aes_256_key.encrypt_data(cid.as_bytes(), Iv::generate(&self.randomizer_facade)).unwrap()), - None => None, - }; - - let import_attachment = ImportAttachment { - _id: None, - ownerEncFileSessionKey: owner_enc_file_session_key.object, - ownerFileKeyVersion: owner_enc_file_session_key.version, - existingAttachmentFile: None, - newAttachment: Some(NewImportAttachment { - _id: None, - encCid: enc_cid, - encFileHash: None, - encFileName: enc_file_name, - encMimeType: enc_mime_type, - ownerEncFileHashSessionKey: None, - referenceTokens: reference_tokens, - }), - }; - - import_attachments.push(import_attachment); - } - import_mail_data.importedAttachments = import_attachments; - imports_with_attachments.push(import_mail_data); - } - - let import_mail_post_in = ImportMailPostIn { - ownerEncSessionKey: owner_enc_mail_session_key.object.clone(), - ownerGroup: self.target_owner_group.clone(), - ownerKeyVersion: owner_enc_mail_session_key.version, - imports: imports_with_attachments, - targetMailFolder: self.target_mail_folder.clone(), - _format: 0, - _errors: None, - _finalIvs: Default::default(), - }; - - let service_params = ExtraServiceParams { - session_key: Some(new_mail_aes_256_key.clone()), - ..Default::default() - }; - - let response = self - .logged_in_sdk - .get_service_executor() - .post::(import_mail_post_in, service_params) - .await; - - match response { - // this import has been success, - Ok(mut imported_post_out) => { - mails.append(&mut imported_post_out.mails); - new_status = ImportStatus { - state: ImportState::Running, - imported_mails: self - .status - .imported_mails - .saturating_add(u32::try_from(import_len).unwrap_or(u32::MAX)), - }; + pub async fn continue_import(&mut self) -> Result { + let source_iterator = ImportSourceIterator { + source: Arc::clone(&self.import_source), + }; + let _ = self.import_all_mail(source_iterator).await; + Ok(self.status.clone()) + } + + /// once we get the ImportableMail from either of source, + /// continue to the uploading counterpart + async fn import_all_mail( + &mut self, + importable_mails: Iter, + ) -> Result, ()> + where + Iter: Iterator + Send + 'static, + { + let new_mail_aes_256_key = GenericAesKey::from_bytes( + self.randomizer_facade + .generate_random_array::<{ tutasdk::crypto::aes::AES_256_KEY_SIZE }>() + .as_slice(), + ) + .unwrap(); + let mail_group_key = self + .logged_in_sdk + .get_current_sym_group_key(&self.target_owner_group) + .await + .map_err(|_e| ())?; + let owner_enc_mail_session_key = mail_group_key + .encrypt_key(&new_mail_aes_256_key, Iv::generate(&self.randomizer_facade)); + + const MAX_REQUEST_SIZE: usize = 1024 * 1024 * 5; + let import_mail_data_and_attachments = + importable_mails.map(<(ImportMailData, Vec)>::from); + let import_chunks = reduce_to_chunks( + import_mail_data_and_attachments, + MAX_REQUEST_SIZE, + Box::new(|(imd, a)| estimate_json_size(imd)), + ); + + let mut mails: Vec = Vec::new(); + let mut new_status = ImportStatus { + state: ImportState::Running, + imported_mails: 0, + }; + for imports in import_chunks { + let import_len = imports.len(); + + let mut imports_with_attachments = Vec::new(); + for (import_mail_data, importable_mail_attachments) in imports.into_iter() { + let mut import_mail_data = import_mail_data; + let mut import_attachments = Vec::new(); + for importable_mail_attachment in importable_mail_attachments { + let importable_mail_attachment = + importable_mail_attachment as ImportableMailAttachment; + let new_file_aes_256_key = GenericAesKey::from_bytes( + self.randomizer_facade + .generate_random_array::<{ tutasdk::crypto::aes::AES_256_KEY_SIZE }>() + .as_slice(), + ) + .unwrap(); + let owner_enc_file_session_key = mail_group_key + .encrypt_key(&new_file_aes_256_key, Iv::generate(&self.randomizer_facade)); + + let reference_tokens = self + .logged_in_sdk + .blob_facade() + .encrypt_and_upload( + ArchiveDataType::Attachments, + &self.target_owner_group, + &new_file_aes_256_key, + importable_mail_attachment.content, + ) + .await + .unwrap(); + + // todo: do we need to upload the ivs and how? + let enc_file_name = new_file_aes_256_key + .encrypt_data( + importable_mail_attachment.filename.as_ref(), + Iv::generate(&self.randomizer_facade), + ) + .unwrap(); + let enc_mime_type = new_file_aes_256_key + .encrypt_data( + importable_mail_attachment.content_type.as_ref(), + Iv::generate(&self.randomizer_facade), + ) + .unwrap(); + let enc_cid: Option> = match importable_mail_attachment.content_id { + Some(cid) => Some( + new_file_aes_256_key + .encrypt_data(cid.as_bytes(), Iv::generate(&self.randomizer_facade)) + .unwrap(), + ), + None => None, + }; + + let import_attachment = ImportAttachment { + _id: None, + ownerEncFileSessionKey: owner_enc_file_session_key.object, + ownerFileKeyVersion: owner_enc_file_session_key.version, + existingAttachmentFile: None, + newAttachment: Some(NewImportAttachment { + _id: None, + encCid: enc_cid, + encFileHash: None, + encFileName: enc_file_name, + encMimeType: enc_mime_type, + ownerEncFileHashSessionKey: None, + referenceTokens: reference_tokens, + }), + }; + + import_attachments.push(import_attachment); + } + import_mail_data.importedAttachments = import_attachments; + imports_with_attachments.push(import_mail_data); + } + + let import_mail_post_in = ImportMailPostIn { + ownerEncSessionKey: owner_enc_mail_session_key.object.clone(), + ownerGroup: self.target_owner_group.clone(), + ownerKeyVersion: owner_enc_mail_session_key.version, + imports: imports_with_attachments, + targetMailFolder: self.target_mail_folder.clone(), + _format: 0, + _errors: None, + _finalIvs: Default::default(), + }; + + let service_params = ExtraServiceParams { + session_key: Some(new_mail_aes_256_key.clone()), + ..Default::default() + }; + + let response = self + .logged_in_sdk + .get_service_executor() + .post::(import_mail_post_in, service_params) + .await; + + match response { + // this import has been success, + Ok(mut imported_post_out) => { + mails.append(&mut imported_post_out.mails); + new_status = ImportStatus { + state: ImportState::Running, + imported_mails: self + .status + .imported_mails + .saturating_add(u32::try_from(import_len).unwrap_or(u32::MAX)), + }; }, Err(_) => { - // todo: save the ImportableMails to some fail list, - // since, in this iteration the source will not give these mail again, - new_status = ImportStatus { - state: ImportState::Postponed, - imported_mails: self.status.imported_mails, - }; + // todo: save the ImportableMails to some fail list, + // since, in this iteration the source will not give these mail again, + new_status = ImportStatus { + state: ImportState::Postponed, + imported_mails: self.status.imported_mails, + }; }, - } - } - new_status.state = if new_status.state == ImportState::Postponed { - ImportState::Postponed - } else { - ImportState::Finished - }; - - self.status = new_status; - Ok(mails) - } + } + } + new_status.state = if new_status.state == ImportState::Postponed { + ImportState::Postponed + } else { + ImportState::Finished + }; + + self.status = new_status; + Ok(mails) + } } impl ImporterApi { - pub fn new( - logged_in_sdk: Arc, - target_owner_group: GeneratedId, - target_mail_folder: IdTupleGenerated, - import_source: Arc>, - ) -> Self { - let import_inner = Importer { - logged_in_sdk, - target_owner_group, - target_mail_folder, - import_source, - status: ImportStatus::default(), - randomizer_facade: RandomizerFacade::from_core(rand::rngs::OsRng), - }; - Self { - inner: Arc::new(NapiTokioMutex::new(import_inner)), - } - } - - pub async fn continue_import_inner(&mut self) -> Result { - self.inner.lock().await.continue_import().await - } - - pub async fn delete_import_inner(&mut self) -> Result { - todo!() - } - - pub async fn pause_import_inner(&mut self) -> Result { - todo!() - } - - pub async fn create_file_importer_inner( - tuta_credentials: TutaCredentials, - target_owner_group: String, - target_mail_folder: (String, String), - source_paths: Vec, - ) -> napi::Result { - let logged_in_sdk_future = Self::create_sdk(tuta_credentials); - - let fs_email_client = FileImport::new(source_paths) - .map_err(|_e| NapiError::from_reason("Cannot create file import"))?; - let import_source = Arc::new(Mutex::new(ImportSource::LocalFile { fs_email_client })); - let logged_in_sdk = logged_in_sdk_future - .await - .map_err(|_e| NapiError::from_reason("Cannot create logged in sdk"))?; - - Ok(ImporterApi::new( - logged_in_sdk, - GeneratedId(target_owner_group), - IdTupleGenerated::new( - GeneratedId(target_mail_folder.0), - GeneratedId(target_mail_folder.1), - ), - import_source, - )) - } - - async fn create_sdk(tuta_credentials: TutaCredentials) -> Result, String> { - let rest_client = Arc::new( - NativeRestClient::try_new() - .map_err(|e| format!("Cannot build native rest client: {e}"))?, - ); - - let logged_in_sdk = { - let sdk = Sdk::new(tuta_credentials.api_url.clone(), rest_client); - - let sdk_credentials: Credentials = tuta_credentials - .clone() - .try_into() - .map_err(|_| "Cannot convert to valid credentials".to_string())?; - sdk.login(sdk_credentials) - .await - .map_err(|e| format!("Cannot login to sdk. Error: {:?}", e))? - }; - - Ok(logged_in_sdk) - } + pub fn new( + logged_in_sdk: Arc, + target_owner_group: GeneratedId, + target_mail_folder: IdTupleGenerated, + import_source: Arc>, + ) -> Self { + let import_inner = Importer { + logged_in_sdk, + target_owner_group, + target_mail_folder, + import_source, + status: ImportStatus::default(), + randomizer_facade: RandomizerFacade::from_core(rand::rngs::OsRng), + }; + Self { + inner: Arc::new(NapiTokioMutex::new(import_inner)), + } + } + + pub async fn continue_import_inner(&mut self) -> Result { + self.inner.lock().await.continue_import().await + } + + pub async fn delete_import_inner(&mut self) -> Result { + todo!() + } + + pub async fn pause_import_inner(&mut self) -> Result { + todo!() + } + + pub async fn create_file_importer_inner( + tuta_credentials: TutaCredentials, + target_owner_group: String, + target_mail_folder: (String, String), + source_paths: Vec, + ) -> napi::Result { + let logged_in_sdk_future = Self::create_sdk(tuta_credentials); + + let fs_email_client = FileImport::new(source_paths) + .map_err(|_e| NapiError::from_reason("Cannot create file import"))?; + let import_source = Arc::new(Mutex::new(ImportSource::LocalFile { fs_email_client })); + let logged_in_sdk = logged_in_sdk_future + .await + .map_err(|_e| NapiError::from_reason("Cannot create logged in sdk"))?; + + Ok(ImporterApi::new( + logged_in_sdk, + GeneratedId(target_owner_group), + IdTupleGenerated::new( + GeneratedId(target_mail_folder.0), + GeneratedId(target_mail_folder.1), + ), + import_source, + )) + } + + async fn create_sdk(tuta_credentials: TutaCredentials) -> Result, String> { + let rest_client = Arc::new( + NativeRestClient::try_new() + .map_err(|e| format!("Cannot build native rest client: {e}"))?, + ); + + let logged_in_sdk = { + let sdk = Sdk::new(tuta_credentials.api_url.clone(), rest_client); + + let sdk_credentials: Credentials = tuta_credentials + .clone() + .try_into() + .map_err(|_| "Cannot convert to valid credentials".to_string())?; + sdk.login(sdk_credentials) + .await + .map_err(|e| format!("Cannot login to sdk. Error: {:?}", e))? + }; + + Ok(logged_in_sdk) + } } // Wrapper for napi #[cfg(feature = "javascript")] #[napi_derive::napi] impl ImporterApi { - // once Self::continue_import return custom error, - // do the error conversion here, or in trait - fn error_conversion(_err: E) -> napi::Error { - todo!() - } - - #[napi] - pub async unsafe fn continue_import(&mut self) -> napi::Result { - self.continue_import_inner() - .await - .map_err(Self::error_conversion) - } - - #[napi] - pub async unsafe fn delete_import(&mut self) -> napi::Result { - self.delete_import_inner() - .await - .map_err(Self::error_conversion) - } - - #[napi] - pub async unsafe fn pause_import(&mut self) -> napi::Result { - self.pause_import_inner() - .await - .map_err(Self::error_conversion) - } - - #[napi] - pub async fn create_file_importer( - tuta_credentials: TutaCredentials, - target_owner_group: String, - target_mail_folder: (String, String), - source_paths: Vec, - ) -> napi::Result { - Self::create_file_importer_inner( - tuta_credentials, - target_owner_group, - target_mail_folder, - source_paths, - ) - .await - } + // once Self::continue_import return custom error, + // do the error conversion here, or in trait + fn error_conversion(_err: E) -> napi::Error { + todo!() + } + + #[napi] + pub async unsafe fn continue_import(&mut self) -> napi::Result { + self.continue_import_inner() + .await + .map_err(Self::error_conversion) + } + + #[napi] + pub async unsafe fn delete_import(&mut self) -> napi::Result { + self.delete_import_inner() + .await + .map_err(Self::error_conversion) + } + + #[napi] + pub async unsafe fn pause_import(&mut self) -> napi::Result { + self.pause_import_inner() + .await + .map_err(Self::error_conversion) + } + + #[napi] + pub async fn create_file_importer( + tuta_credentials: TutaCredentials, + target_owner_group: String, + target_mail_folder: (String, String), + source_paths: Vec, + ) -> napi::Result { + Self::create_file_importer_inner( + tuta_credentials, + target_owner_group, + target_mail_folder, + source_paths, + ) + .await + } } #[cfg(test)] mod tests { - use super::*; - use crate::importer::imap_reader::{ImapCredentials, LoginMechanism}; - use crate::tuta_imap::testing::GreenMailTestServer; - use mail_builder::MessageBuilder; - use tutasdk::entities::generated::tutanota::MailFolder; - use tutasdk::folder_system::MailSetKind; - use tutasdk::net::native_rest_client::NativeRestClient; - use tutasdk::Sdk; - - fn sample_email(subject: String) -> String { - let email = MessageBuilder::new() + use super::*; + use crate::importer::imap_reader::{ImapCredentials, LoginMechanism}; + use crate::tuta_imap::testing::GreenMailTestServer; + use mail_builder::MessageBuilder; + use tutasdk::entities::generated::tutanota::MailFolder; + use tutasdk::folder_system::MailSetKind; + use tutasdk::net::native_rest_client::NativeRestClient; + use tutasdk::Sdk; + + fn sample_email(subject: String) -> String { + let email = MessageBuilder::new() .from(("Matthias", "map@example.org")) .to(("Johannes", "jhm@example.org")) .subject(subject) .text_body("Hello tutao! this is the first step to have email import.Want to see html πŸ˜€?

red

") .write_to_string() .unwrap(); - email - } - - async fn get_test_import_folder_id( - logged_in_sdk: &Arc, - kind: MailSetKind, - ) -> MailFolder { - let mail_facade = logged_in_sdk.mail_facade(); - let mailbox = mail_facade.load_user_mailbox().await.unwrap(); - let folders = mail_facade - .load_folders_for_mailbox(&mailbox) - .await - .unwrap(); - folders - .system_folder_by_type(kind) - .expect("inbox should exist") - .clone() - } - - async fn init_imap_importer() -> (Importer, GreenMailTestServer) { - let importer_mail_address = "map-free@tutanota.de".to_string(); - let logged_in_sdk = Sdk::new( - "http://localhost:9000".to_string(), - Arc::new(NativeRestClient::try_new().unwrap()), - ) - .create_session(importer_mail_address.as_str(), "map") - .await - .unwrap(); - let greenmail = GreenMailTestServer::new(); - let imap_import_config = ImapImportConfig { - root_import_mail_folder_name: "/".to_string(), - credentials: ImapCredentials { - host: "127.0.0.1".to_string(), - port: greenmail.imaps_port.try_into().unwrap(), - login_mechanism: LoginMechanism::Plain { - username: "sug@example.org".to_string(), - password: "sug".to_string(), - }, - }, - }; - - let import_source = Arc::new(Mutex::new(ImportSource::RemoteImap { - imap_import_client: ImapImport::new(imap_import_config), - })); - let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng); - let target_mail_folder = get_test_import_folder_id(&logged_in_sdk, MailSetKind::Archive) - .await - ._id - .unwrap(); - - let target_owner_group = logged_in_sdk - .mail_facade() - .get_group_id_for_mail_address(importer_mail_address.as_str()) - .await - .unwrap(); - - let importer = Importer { - target_owner_group, - target_mail_folder, - logged_in_sdk, - import_source, - randomizer_facade, - status: ImportStatus::default(), - }; - - (importer, greenmail) - } - - pub async fn init_file_importer(source_paths: Vec) -> Importer { - let logged_in_sdk = Sdk::new( - "http://localhost:9000".to_string(), - Arc::new(NativeRestClient::try_new().unwrap()), - ) - .create_session("map-free@tutanota.de", "map") - .await - .unwrap(); - - let import_source = Arc::new(Mutex::new(ImportSource::LocalFile { - fs_email_client: FileImport::new(source_paths).unwrap(), - })); - let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng); - let target_mail_folder = get_test_import_folder_id(&logged_in_sdk, MailSetKind::Archive) - .await - ._id - .unwrap(); - - let target_owner_group = logged_in_sdk - .mail_facade() - .get_group_id_for_mail_address("map-free@tutanota.de") - .await - .unwrap(); - - Importer { - status: ImportStatus::default(), - target_owner_group, - target_mail_folder, - logged_in_sdk, - import_source, - randomizer_facade, - } - } - - #[tokio::test] - pub async fn import_multiple_from_imap_default_folder() { - let (mut importer, greenmail) = init_imap_importer().await; - - let email_first = sample_email("Hello from imap πŸ˜€! -- Бписок.doc".to_string()); - let email_second = sample_email("Second time: hello".to_string()); - greenmail.store_mail("sug@example.org", email_first.as_str()); - greenmail.store_mail("sug@example.org", email_second.as_str()); - - let import_res = importer.continue_import().await.map_err(|_| ()); - assert_eq!( - Ok(ImportStatus { - state: ImportState::Finished, - imported_mails: 2, - }), - import_res - ); - } - - #[tokio::test] - pub async fn import_single_from_imap_default_folder() { - let (mut importer, greenmail) = init_imap_importer().await; - - let email = sample_email("Single email".to_string()); - greenmail.store_mail("sug@example.org", email.as_str()); - - let import_res = importer.continue_import().await.map_err(|_| ()); - assert_eq!( - Ok(ImportStatus { - state: ImportState::Finished, - imported_mails: 1, - }), - import_res - ); - } - - #[tokio::test] - async fn can_import_single_eml_file() { - let mut importer = init_file_importer(vec!["./test/sample.eml".to_string()]).await; - - let import_res = importer.continue_import().await.map_err(|_| ()); - assert_eq!( - Ok(ImportStatus { - state: ImportState::Finished, - imported_mails: 1, - }), - import_res - ); - } + email + } + + async fn get_test_import_folder_id( + logged_in_sdk: &Arc, + kind: MailSetKind, + ) -> MailFolder { + let mail_facade = logged_in_sdk.mail_facade(); + let mailbox = mail_facade.load_user_mailbox().await.unwrap(); + let folders = mail_facade + .load_folders_for_mailbox(&mailbox) + .await + .unwrap(); + folders + .system_folder_by_type(kind) + .expect("inbox should exist") + .clone() + } + + async fn init_imap_importer() -> (Importer, GreenMailTestServer) { + let importer_mail_address = "map-free@tutanota.de".to_string(); + let logged_in_sdk = Sdk::new( + "http://localhost:9000".to_string(), + Arc::new(NativeRestClient::try_new().unwrap()), + ) + .create_session(importer_mail_address.as_str(), "map") + .await + .unwrap(); + let greenmail = GreenMailTestServer::new(); + let imap_import_config = ImapImportConfig { + root_import_mail_folder_name: "/".to_string(), + credentials: ImapCredentials { + host: "127.0.0.1".to_string(), + port: greenmail.imaps_port.try_into().unwrap(), + login_mechanism: LoginMechanism::Plain { + username: "sug@example.org".to_string(), + password: "sug".to_string(), + }, + }, + }; + + let import_source = Arc::new(Mutex::new(ImportSource::RemoteImap { + imap_import_client: ImapImport::new(imap_import_config), + })); + let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng); + let target_mail_folder = get_test_import_folder_id(&logged_in_sdk, MailSetKind::Archive) + .await + ._id + .unwrap(); + + let target_owner_group = logged_in_sdk + .mail_facade() + .get_group_id_for_mail_address(importer_mail_address.as_str()) + .await + .unwrap(); + + let importer = Importer { + target_owner_group, + target_mail_folder, + logged_in_sdk, + import_source, + randomizer_facade, + status: ImportStatus::default(), + }; + + (importer, greenmail) + } + + pub async fn init_file_importer(source_paths: Vec) -> Importer { + let logged_in_sdk = Sdk::new( + "http://localhost:9000".to_string(), + Arc::new(NativeRestClient::try_new().unwrap()), + ) + .create_session("map-free@tutanota.de", "map") + .await + .unwrap(); + + let import_source = Arc::new(Mutex::new(ImportSource::LocalFile { + fs_email_client: FileImport::new(source_paths).unwrap(), + })); + let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng); + let target_mail_folder = get_test_import_folder_id(&logged_in_sdk, MailSetKind::Archive) + .await + ._id + .unwrap(); + + let target_owner_group = logged_in_sdk + .mail_facade() + .get_group_id_for_mail_address("map-free@tutanota.de") + .await + .unwrap(); + + Importer { + status: ImportStatus::default(), + target_owner_group, + target_mail_folder, + logged_in_sdk, + import_source, + randomizer_facade, + } + } + + #[tokio::test] + pub async fn import_multiple_from_imap_default_folder() { + let (mut importer, greenmail) = init_imap_importer().await; + + let email_first = sample_email("Hello from imap πŸ˜€! -- Бписок.doc".to_string()); + let email_second = sample_email("Second time: hello".to_string()); + greenmail.store_mail("sug@example.org", email_first.as_str()); + greenmail.store_mail("sug@example.org", email_second.as_str()); + + let import_res = importer.continue_import().await.map_err(|_| ()); + assert_eq!( + Ok(ImportStatus { + state: ImportState::Finished, + imported_mails: 2, + }), + import_res + ); + } + + #[tokio::test] + pub async fn import_single_from_imap_default_folder() { + let (mut importer, greenmail) = init_imap_importer().await; + + let email = sample_email("Single email".to_string()); + greenmail.store_mail("sug@example.org", email.as_str()); + + let import_res = importer.continue_import().await.map_err(|_| ()); + assert_eq!( + Ok(ImportStatus { + state: ImportState::Finished, + imported_mails: 1, + }), + import_res + ); + } + + #[tokio::test] + async fn can_import_single_eml_file() { + let mut importer = init_file_importer(vec!["./test/sample.eml".to_string()]).await; + + let import_res = importer.continue_import().await.map_err(|_| ()); + assert_eq!( + Ok(ImportStatus { + state: ImportState::Finished, + imported_mails: 1, + }), + import_res + ); + } } diff --git a/packages/node-mimimi/src/importer/importable_mail.rs b/packages/node-mimimi/src/importer/importable_mail.rs index b8d5e2d73ea..4e42f28361e 100644 --- a/packages/node-mimimi/src/importer/importable_mail.rs +++ b/packages/node-mimimi/src/importer/importable_mail.rs @@ -2,15 +2,15 @@ use crate::tuta_imap::client::types::ImapMail; use extend_mail_parser::MakeString; use mail_builder::headers::Header; use mail_parser::{ - Address, ContentType, GetHeader, HeaderValue, Message, MessageParser, MessagePart, - MessagePartId, MimeHeaders, PartType, + Address, ContentType, GetHeader, HeaderValue, Message, MessageParser, MessagePart, + MessagePartId, MimeHeaders, PartType, }; use regex::Regex; use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use tutasdk::date::DateTime; use tutasdk::entities::generated::tutanota::{ - EncryptedMailAddress, ImportMailData, ImportMailDataMailReference, MailAddress, Recipients, + EncryptedMailAddress, ImportMailData, ImportMailDataMailReference, MailAddress, Recipients, }; pub mod extend_mail_parser; mod plain_text_to_html_converter; @@ -22,194 +22,194 @@ pub(crate) const FIXED_CUSTOM_ID: &str = "____"; #[derive(Default)] #[cfg_attr(test, derive(PartialEq, Debug))] pub(super) enum MailState { - #[default] - Received = 2, - Sent = 1, - Draft = 0, + #[default] + Received = 2, + Sent = 1, + Draft = 0, } #[repr(i64)] #[derive(Default)] #[cfg_attr(test, derive(PartialEq, Debug))] pub(super) enum ICalType { - #[default] - Nothing = 0, - ICalPublishh = 1, - ICalRequest = 2, - ICalAdd = 3, - ICalCancel = 4, - ICalRefresh = 5, - ICalCounter = 6, - ICalDeclineCounter = 7, + #[default] + Nothing = 0, + ICalPublishh = 1, + ICalRequest = 2, + ICalAdd = 3, + ICalCancel = 4, + ICalRefresh = 5, + ICalCounter = 6, + ICalDeclineCounter = 7, } #[derive(Default)] #[cfg_attr(test, derive(PartialEq, Debug))] pub(super) enum ReplyType { - #[default] - Nothing = 0, - Reply = 1, - Forward = 2, - ReplyForward = 3, + #[default] + Nothing = 0, + Reply = 1, + Forward = 2, + ReplyForward = 3, } #[cfg_attr(test, derive(PartialEq, Debug, Clone))] pub(super) struct ImportableMailAttachment { - pub filename: String, - pub content_id: Option, - pub content_type: String, - pub content: Vec, - is_inline: bool, + pub filename: String, + pub content_id: Option, + pub content_type: String, + pub content: Vec, + is_inline: bool, } #[cfg_attr(test, derive(PartialEq, Debug))] pub(super) enum BodyText { - Html(String), - Plain(String), + Html(String), + Plain(String), } #[derive(Default, PartialEq)] #[cfg_attr(test, derive(Debug))] pub(super) struct MailContact { - pub mail_address: String, - pub name: String, + pub mail_address: String, + pub name: String, } impl<'a> From> for MailContact { - fn from(value: mail_parser::Addr) -> Self { - Self { - name: value.name.unwrap_or_default().to_string(), - mail_address: value.address.unwrap_or_default().to_string(), - } - } + fn from(value: mail_parser::Addr) -> Self { + Self { + name: value.name.unwrap_or_default().to_string(), + mail_address: value.address.unwrap_or_default().to_string(), + } + } } impl From for MailAddress { - fn from(value: MailContact) -> Self { - Self { - _id: None, - address: value.mail_address, - name: value.name, - contact: None, - _finalIvs: Default::default(), - } - } + fn from(value: MailContact) -> Self { + Self { + _id: None, + address: value.mail_address, + name: value.name, + contact: None, + _finalIvs: Default::default(), + } + } } /// Input data for mail import service #[cfg_attr(test, derive(PartialEq, Debug))] pub struct ImportableMail { - pub(super) headers_string: String, - pub(super) subject: String, - pub(super) html_body_text: String, - pub(super) attachments: Vec, - - pub(super) date: Option, - - pub(super) different_envelope_sender: Option, - pub(super) from_addresses: Vec, - pub(super) to_addresses: Vec, - pub(super) cc_addresses: Vec, - pub(super) bcc_addresses: Vec, - pub(super) reply_to_addresses: Vec, - - pub(super) ical_type: ICalType, - pub(super) reply_type: ReplyType, - - pub(super) mail_state: MailState, - pub(super) is_phishing: bool, // https://turbo.fish/::%3Cphising%3E - pub(super) unread: bool, - - pub(super) message_id: Option, - pub(super) in_reply_to: Option, - pub(super) references: Vec, + pub(super) headers_string: String, + pub(super) subject: String, + pub(super) html_body_text: String, + pub(super) attachments: Vec, + + pub(super) date: Option, + + pub(super) different_envelope_sender: Option, + pub(super) from_addresses: Vec, + pub(super) to_addresses: Vec, + pub(super) cc_addresses: Vec, + pub(super) bcc_addresses: Vec, + pub(super) reply_to_addresses: Vec, + + pub(super) ical_type: ICalType, + pub(super) reply_type: ReplyType, + + pub(super) mail_state: MailState, + pub(super) is_phishing: bool, // https://turbo.fish/::%3Cphising%3E + pub(super) unread: bool, + + pub(super) message_id: Option, + pub(super) in_reply_to: Option, + pub(super) references: Vec, } impl ImportableMail { - /// Utility function to convert mail_parser::Address - /// to a list of tutasdk::MailAddress - /// in such a way that every address must have mail-address and optional name - /// - /// returns None, if any of the address have empty mail-address - /// - /// set the _id: of all mail address to random 4-byte long customId, - /// this will only be valid in dataTransferType context - fn map_to_tuta_mail_address(mail_parser_addresses: Cow
) -> Vec { - let address_list = match mail_parser_addresses.as_ref() { - Address::List(address_list) => Cow::Borrowed(address_list), - Address::Group(group_senders) => { - let group_addresses = group_senders - .iter() - .map(|group| group.addresses.as_slice()) - .collect::>() - .concat(); - - Cow::Owned(group_addresses) - } - }; - - address_list - .as_ref() - .iter() - .map(|address| MailContact { - mail_address: address.address().unwrap_or_default().to_string(), - name: address.name().unwrap_or_default().to_string(), - }) - .collect() - } - - fn handle_plain_text(email_body_as_html: &mut String, plain_text: &str) { - let plain_text_as_html = plain_text_to_html_converter::plain_text_to_html(plain_text); - Self::handle_html_text(email_body_as_html, plain_text_as_html.as_str()) - } - - fn handle_html_text(email_body_as_html: &mut String, html_text: &str) { - email_body_as_html.push_str(html_text); - } - - // from the parsed message - // return : - // .0 a single string that ca be display as email in html format - // .1 list of attachment found - fn process_all_parts( - parsed_message: &mail_parser::Message, - ) -> Result<(String, Vec), MailParseError> { - let mut email_body_as_html = String::new(); - let mut attachments = Vec::with_capacity(parsed_message.attachments.len()); - - // all the alternative of multipart/alternative that we chose not to include - let mut multipart_ignored_alternative = HashSet::new(); - - for (part_id, part) in parsed_message.parts.iter().enumerate() { - if multipart_ignored_alternative.contains(&part_id) { - continue; - } - match &part.body { - PartType::Binary(binary_content) => { - Self::handle_binary(part, &mut attachments, binary_content.to_vec(), false); - } - - PartType::InlineBinary(binary_content) => { - Self::handle_binary(part, &mut attachments, binary_content.to_vec(), true); - } - - PartType::Text(text) => { + /// Utility function to convert mail_parser::Address + /// to a list of tutasdk::MailAddress + /// in such a way that every address must have mail-address and optional name + /// + /// returns None, if any of the address have empty mail-address + /// + /// set the _id: of all mail address to random 4-byte long customId, + /// this will only be valid in dataTransferType context + fn map_to_tuta_mail_address(mail_parser_addresses: Cow
) -> Vec { + let address_list = match mail_parser_addresses.as_ref() { + Address::List(address_list) => Cow::Borrowed(address_list), + Address::Group(group_senders) => { + let group_addresses = group_senders + .iter() + .map(|group| group.addresses.as_slice()) + .collect::>() + .concat(); + + Cow::Owned(group_addresses) + }, + }; + + address_list + .as_ref() + .iter() + .map(|address| MailContact { + mail_address: address.address().unwrap_or_default().to_string(), + name: address.name().unwrap_or_default().to_string(), + }) + .collect() + } + + fn handle_plain_text(email_body_as_html: &mut String, plain_text: &str) { + let plain_text_as_html = plain_text_to_html_converter::plain_text_to_html(plain_text); + Self::handle_html_text(email_body_as_html, plain_text_as_html.as_str()) + } + + fn handle_html_text(email_body_as_html: &mut String, html_text: &str) { + email_body_as_html.push_str(html_text); + } + + // from the parsed message + // return : + // .0 a single string that ca be display as email in html format + // .1 list of attachment found + fn process_all_parts( + parsed_message: &mail_parser::Message, + ) -> Result<(String, Vec), MailParseError> { + let mut email_body_as_html = String::new(); + let mut attachments = Vec::with_capacity(parsed_message.attachments.len()); + + // all the alternative of multipart/alternative that we chose not to include + let mut multipart_ignored_alternative = HashSet::new(); + + for (part_id, part) in parsed_message.parts.iter().enumerate() { + if multipart_ignored_alternative.contains(&part_id) { + continue; + } + match &part.body { + PartType::Binary(binary_content) => { + Self::handle_binary(part, &mut attachments, binary_content.to_vec(), false); + }, + + PartType::InlineBinary(binary_content) => { + Self::handle_binary(part, &mut attachments, binary_content.to_vec(), true); + }, + + PartType::Text(text) => { if !Self::is_attachment(&email_body_as_html, part) && Self::is_plain_text(part) { - Self::handle_plain_text(&mut email_body_as_html, text.as_ref()); - } else { - Self::handle_binary( - part, - &mut attachments, - text.as_bytes().to_vec(), - false, - ); - } - } - - PartType::Html(html_text) => { + Self::handle_plain_text(&mut email_body_as_html, text.as_ref()); + } else { + Self::handle_binary( + part, + &mut attachments, + text.as_bytes().to_vec(), + false, + ); + } + }, + + PartType::Html(html_text) => { if !Self::is_attachment(&email_body_as_html, part) { - Self::handle_html_text(&mut email_body_as_html, html_text.as_ref()) + Self::handle_html_text(&mut email_body_as_html, html_text.as_ref()) } else { Self::handle_binary( part, @@ -220,23 +220,23 @@ impl ImportableMail { } }, - PartType::Message(attached_message) => { - let ignored_result = Self::handle_message(&mut attachments, attached_message); - } + PartType::Message(attached_message) => { + let ignored_result = Self::handle_message(&mut attachments, attached_message); + }, - PartType::Multipart(multi_part_ids) => { - Self::handle_multipart( - parsed_message, - &mut multipart_ignored_alternative, - part, - multi_part_ids, - ); - } - } - } + PartType::Multipart(multi_part_ids) => { + Self::handle_multipart( + parsed_message, + &mut multipart_ignored_alternative, + part, + multi_part_ids, + ); + }, + } + } - Ok((email_body_as_html, attachments)) - } + Ok((email_body_as_html, attachments)) + } fn is_plain_text(part: &MessagePart) -> bool { part.content_type() @@ -268,455 +268,458 @@ impl ImportableMail { || (!email_body_as_html.is_empty() && part.content_id().is_some()) } - fn get_filename(part: &MessagePart, fallback_name: &str) -> String { - let content_disposition_filename = part - .content_disposition() - .map(|c| c.attribute("filename").map(ToString::to_string)) - .flatten(); - let content_type_filename = part - .content_type() - .map(|c| c.attribute("name").map(ToString::to_string)) - .flatten(); - - let file_name = content_disposition_filename.unwrap_or_else(|| { - content_type_filename.unwrap_or_else(|| { - let filename_suffix = part - .content_type() - .map(Self::get_suffix_from_content_type) - .unwrap_or_default(); - fallback_name.to_string() + filename_suffix - }) - }); - Self::escape_filename(&file_name).to_string() - } - - /// Creates a filename from the given filename that is valid on Linux and Windows. Invalid - /// characters are replaced by "_" - fn escape_filename(file_name: &str) -> Cow { - let regex = Regex::new("[\\\\/:*?<>\"|]").unwrap(); - regex.replace(file_name, "_") - } - - fn get_suffix_from_content_type(content_type: &ContentType) -> &'static str { - if content_type.c_type == "message" { - if content_type.subtype() == Some("rfc822") { - ".eml" - } else { - ".txt" - } - } else if content_type.c_type == "text" { - if content_type.subtype() == Some("calendar") { - ".ics" - } else { - ".txt" - } - } else { - "" - } - } - - fn handle_multipart( - parsed_message: &mail_parser::Message, - multipart_ignored_alternative: &mut HashSet, - part: &MessagePart, - multi_part_ids: &Vec, - ) { - let is_multipart_alternative = part - .content_type() - .map(|content_type| { + fn get_filename(part: &MessagePart, fallback_name: &str) -> String { + let content_disposition_filename = part + .content_disposition() + .map(|c| c.attribute("filename").map(ToString::to_string)) + .flatten(); + let content_type_filename = part + .content_type() + .map(|c| c.attribute("name").map(ToString::to_string)) + .flatten(); + + let file_name = content_disposition_filename.unwrap_or_else(|| { + content_type_filename.unwrap_or_else(|| { + let filename_suffix = part + .content_type() + .map(Self::get_suffix_from_content_type) + .unwrap_or_default(); + fallback_name.to_string() + filename_suffix + }) + }); + Self::escape_filename(&file_name).to_string() + } + + /// Creates a filename from the given filename that is valid on Linux and Windows. Invalid + /// characters are replaced by "_" + fn escape_filename(file_name: &str) -> Cow { + let regex = Regex::new("[\\\\/:*?<>\"|]").unwrap(); + regex.replace(file_name, "_") + } + + fn get_suffix_from_content_type(content_type: &ContentType) -> &'static str { + if content_type.c_type == "message" { + if content_type.subtype() == Some("rfc822") { + ".eml" + } else { + ".txt" + } + } else if content_type.c_type == "text" { + if content_type.subtype() == Some("calendar") { + ".ics" + } else { + ".txt" + } + } else { + "" + } + } + + fn handle_multipart( + parsed_message: &mail_parser::Message, + multipart_ignored_alternative: &mut HashSet, + part: &MessagePart, + multi_part_ids: &Vec, + ) { + let is_multipart_alternative = part + .content_type() + .map(|content_type| { content_type.c_type == "multipart" && content_type.subtype() == Some("alternative") - }) - .unwrap_or_default(); - - if !is_multipart_alternative { - // we can only take care of multipart/alternative - // what to do for other multipart/* - return; - - // edu: https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html - // The primary subtype for multipart, "mixed", is intended for use when the body parts - // are independent and intended to be displayed serially. Any multipart subtypes that - // an implementation does not recognize should be treated as being of subtype "mixed". - } - - let mut best_alternative_yet = None; - for multipart_id in multi_part_ids { - // if this part was already ignored, - if multipart_ignored_alternative.contains(multipart_id) { - continue; - } - - let alternative_part = parsed_message - .part(*multipart_id) - .expect("Expected multipart part to be there?"); - - // for now, we can only decide between alternative between text/plain and text/html - let alternative_content_type = alternative_part - .content_type() - .expect("All multipart alternative should have a Content-Type header"); - - // todo: handle other content type. example: choosing one image from list of alternatives? - let is_text_plain = alternative_content_type.c_type == "text" - && alternative_content_type.subtype() == Some("plain"); - let is_text_html = alternative_content_type.c_type == "text" - && alternative_content_type.subtype() == Some("html"); - - if is_text_plain { - // always ignore plain. we can display html everytime - multipart_ignored_alternative.insert(*multipart_id); - } else if is_text_html { - // if we found a html, this is what we will select. - // if we had found and html already, we will still choose the new one. - // and insert the last one to ignored list - if let Some(last_choice) = best_alternative_yet { - multipart_ignored_alternative.insert(last_choice); - } - best_alternative_yet = Some(*multipart_id); - } else { - // "Can only choose multipart/alternative between text/plain and text/html" - // todo: this is not a good case - if let Some(last_choice) = best_alternative_yet { - multipart_ignored_alternative.insert(last_choice); - } - best_alternative_yet = Some(*multipart_id); - } - } - - // if we did not find any alternative, we will take the last one, - // don't have to do anything with chosen multipart, - // it will anyway be included in next iteration - if best_alternative_yet.is_none() { - let last_choice = multi_part_ids - .last() - .expect("Wait. how can i choose between empty sets of alternatives?"); - - // do we remove the last_choice from ignored list? - // the problem is: - // will the same alternative part can be referenced by multiple multipart block? - // if so, if we remove last_choice now, and this was also ignored by another multipart, - // we will display it anyhow. probably this is right, right? - assert!( - multipart_ignored_alternative.remove(last_choice), - "if we did not put last_choice in ignore list. why best_alternative_yet is none?" - ); - } - - // ps: we assume that the order is: - // multipart block should always come before all it's alternative - } - - fn handle_binary( - part: &MessagePart, - attachments: &mut Vec, - content: Vec, - is_inline: bool, - ) { - let content_id = part.content_id().map(ToString::to_string); - let filename = Self::get_filename(part, "unknown"); - let content_type = part - .content_type() - .map(MakeString::make_string) - .map(Cow::into_owned) - .unwrap_or_else(|| Self::default_content_type().make_string().into_owned()) - .to_string(); - - let content = content.to_vec(); - let attachment = ImportableMailAttachment { - filename, - content_type, - content_id, - is_inline, - content, - }; - - attachments.push(attachment); - } - - fn handle_message( - attachments: &mut Vec, - message: &Message, - ) -> Result<(), MailParseError> { - let filename = - Self::get_filename(&message.parts[0], &message.subject().unwrap_or("unknown")); - let content_type = message - .content_type() + }) + .unwrap_or_default(); + + if !is_multipart_alternative { + // we can only take care of multipart/alternative + // what to do for other multipart/* + return; + + // edu: https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html + // The primary subtype for multipart, "mixed", is intended for use when the body parts + // are independent and intended to be displayed serially. Any multipart subtypes that + // an implementation does not recognize should be treated as being of subtype "mixed". + } + + let mut best_alternative_yet = None; + for multipart_id in multi_part_ids { + // if this part was already ignored, + if multipart_ignored_alternative.contains(multipart_id) { + continue; + } + + let alternative_part = parsed_message + .part(*multipart_id) + .expect("Expected multipart part to be there?"); + + // for now, we can only decide between alternative between text/plain and text/html + let alternative_content_type = alternative_part + .content_type() + .expect("All multipart alternative should have a Content-Type header"); + + // todo: handle other content type. example: choosing one image from list of alternatives? + let is_text_plain = alternative_content_type.c_type == "text" + && alternative_content_type.subtype() == Some("plain"); + let is_text_html = alternative_content_type.c_type == "text" + && alternative_content_type.subtype() == Some("html"); + + if is_text_plain { + // always ignore plain. we can display html everytime + multipart_ignored_alternative.insert(*multipart_id); + } else if is_text_html { + // if we found a html, this is what we will select. + // if we had found and html already, we will still choose the new one. + // and insert the last one to ignored list + if let Some(last_choice) = best_alternative_yet { + multipart_ignored_alternative.insert(last_choice); + } + best_alternative_yet = Some(*multipart_id); + } else { + // "Can only choose multipart/alternative between text/plain and text/html" + // todo: this is not a good case + if let Some(last_choice) = best_alternative_yet { + multipart_ignored_alternative.insert(last_choice); + } + best_alternative_yet = Some(*multipart_id); + } + } + + // if we did not find any alternative, we will take the last one, + // don't have to do anything with chosen multipart, + // it will anyway be included in next iteration + if best_alternative_yet.is_none() { + let last_choice = multi_part_ids + .last() + .expect("Wait. how can i choose between empty sets of alternatives?"); + + // do we remove the last_choice from ignored list? + // the problem is: + // will the same alternative part can be referenced by multiple multipart block? + // if so, if we remove last_choice now, and this was also ignored by another multipart, + // we will display it anyhow. probably this is right, right? + assert!( + multipart_ignored_alternative.remove(last_choice), + "if we did not put last_choice in ignore list. why best_alternative_yet is none?" + ); + } + + // ps: we assume that the order is: + // multipart block should always come before all it's alternative + } + + fn handle_binary( + part: &MessagePart, + attachments: &mut Vec, + content: Vec, + is_inline: bool, + ) { + let content_id = part.content_id().map(ToString::to_string); + let filename = Self::get_filename(part, "unknown"); + let content_type = part + .content_type() + .map(MakeString::make_string) + .map(Cow::into_owned) + .unwrap_or_else(|| Self::default_content_type().make_string().into_owned()) + .to_string(); + + let content = content.to_vec(); + let attachment = ImportableMailAttachment { + filename, + content_type, + content_id, + is_inline, + content, + }; + + attachments.push(attachment); + } + + fn handle_message( + attachments: &mut Vec, + message: &Message, + ) -> Result<(), MailParseError> { + let filename = + Self::get_filename(&message.parts[0], &message.subject().unwrap_or("unknown")); + let content_type = message + .content_type() .ok_or_else(|| Self::default_content_type()) - .map(MakeString::make_string) - .unwrap_or_default() - .to_string(); - - let nested_part = &message.parts[0]; - let content = - message.raw_message[nested_part.offset_header..nested_part.offset_end].to_vec(); - let attachment = ImportableMailAttachment { - filename, - content_type, - content, - is_inline: false, - content_id: None, - }; - attachments.push(attachment); - Ok(()) - } - - fn default_content_type() -> ContentType<'static> { - let default_content_type = ContentType { - c_type: Cow::Borrowed("text"), - c_subtype: Some(Cow::Borrowed("plain")), - attributes: Some(vec![(Cow::Borrowed("charset"), Cow::Borrowed("us-ascii"))]), - }; - default_content_type - } + .map(MakeString::make_string) + .unwrap_or_default() + .to_string(); + + let nested_part = &message.parts[0]; + let content = + message.raw_message[nested_part.offset_header..nested_part.offset_end].to_vec(); + let attachment = ImportableMailAttachment { + filename, + content_type, + content, + is_inline: false, + content_id: None, + }; + attachments.push(attachment); + Ok(()) + } + + fn default_content_type() -> ContentType<'static> { + let default_content_type = ContentType { + c_type: Cow::Borrowed("text"), + c_subtype: Some(Cow::Borrowed("plain")), + attributes: Some(vec![(Cow::Borrowed("charset"), Cow::Borrowed("us-ascii"))]), + }; + default_content_type + } } impl From for (ImportMailData, Vec) { - fn from(importable_mail: ImportableMail) -> Self { - let ImportableMail { - headers_string: headers, - subject, - html_body_text, - different_envelope_sender, - from_addresses, - cc_addresses, - bcc_addresses, - to_addresses, - date, - reply_to_addresses, - ical_type, - reply_type, - mail_state, - is_phishing, - unread, - message_id, - in_reply_to, - references, - attachments, - } = importable_mail; - - let reply_tos = reply_to_addresses - .into_iter() - .map(|reply_to| EncryptedMailAddress { - _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), - _finalIvs: Default::default(), - name: reply_to.name, - address: reply_to.mail_address, - }) - .collect(); - - let bcc_addresses = bcc_addresses.into_iter().map(Into::into).collect(); - let cc_addresses = cc_addresses.into_iter().map(Into::into).collect(); - let to_addresses = to_addresses.into_iter().map(Into::into).collect(); - let from_addresses: Vec = from_addresses.into_iter().map(Into::into).collect(); - - let references = references - .into_iter() - .map(|reference| ImportMailDataMailReference { - _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), - reference, - }) - .collect(); - - (ImportMailData { - _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), - _finalIvs: HashMap::new(), - compressedHeaders: headers, - subject, - compressedBodyText: html_body_text, - differentEnvelopeSender: different_envelope_sender, - sender: from_addresses - .first() - .cloned() - .unwrap_or(MailContact::default().into()), - recipients: Recipients { - _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), - bccRecipients: bcc_addresses, - ccRecipients: cc_addresses, - toRecipients: to_addresses, - }, - replyTos: reply_tos, - unread, - confidential: false, - method: ical_type as i64, - phishingStatus: if is_phishing { 1 } else { 0 }, - replyType: reply_type as i64, - // if no date is provided, use UNIX_EPOCH (01.01.1970) as fallback - date: date.unwrap_or_default(), - state: mail_state as i64, - messageId: message_id, - inReplyTo: in_reply_to, - references, - importedAttachments: vec![], - }, attachments) - } + fn from(importable_mail: ImportableMail) -> Self { + let ImportableMail { + headers_string: headers, + subject, + html_body_text, + different_envelope_sender, + from_addresses, + cc_addresses, + bcc_addresses, + to_addresses, + date, + reply_to_addresses, + ical_type, + reply_type, + mail_state, + is_phishing, + unread, + message_id, + in_reply_to, + references, + attachments, + } = importable_mail; + + let reply_tos = reply_to_addresses + .into_iter() + .map(|reply_to| EncryptedMailAddress { + _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), + _finalIvs: Default::default(), + name: reply_to.name, + address: reply_to.mail_address, + }) + .collect(); + + let bcc_addresses = bcc_addresses.into_iter().map(Into::into).collect(); + let cc_addresses = cc_addresses.into_iter().map(Into::into).collect(); + let to_addresses = to_addresses.into_iter().map(Into::into).collect(); + let from_addresses: Vec = from_addresses.into_iter().map(Into::into).collect(); + + let references = references + .into_iter() + .map(|reference| ImportMailDataMailReference { + _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), + reference, + }) + .collect(); + + ( + ImportMailData { + _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), + _finalIvs: HashMap::new(), + compressedHeaders: headers, + subject, + compressedBodyText: html_body_text, + differentEnvelopeSender: different_envelope_sender, + sender: from_addresses + .first() + .cloned() + .unwrap_or(MailContact::default().into()), + recipients: Recipients { + _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), + bccRecipients: bcc_addresses, + ccRecipients: cc_addresses, + toRecipients: to_addresses, + }, + replyTos: reply_tos, + unread, + confidential: false, + method: ical_type as i64, + phishingStatus: if is_phishing { 1 } else { 0 }, + replyType: reply_type as i64, + // if no date is provided, use UNIX_EPOCH (01.01.1970) as fallback + date: date.unwrap_or_default(), + state: mail_state as i64, + messageId: message_id, + inReplyTo: in_reply_to, + references, + importedAttachments: vec![], + }, + attachments, + ) + } } impl TryFrom for ImportableMail { - type Error = MailParseError; - fn try_from(imap_mail: ImapMail) -> Result { - let ImapMail { rfc822_full } = imap_mail; + type Error = MailParseError; + fn try_from(imap_mail: ImapMail) -> Result { + let ImapMail { rfc822_full } = imap_mail; - // parse the full mime message - let imap_mail = MessageParser::new() - .parse(rfc822_full.as_slice()) - .ok_or(MailParseError::InvalidMimeMessage)?; + // parse the full mime message + let imap_mail = MessageParser::new() + .parse(rfc822_full.as_slice()) + .ok_or(MailParseError::InvalidMimeMessage)?; - let mut importable_mail = Self::try_from(&imap_mail).unwrap(); + let mut importable_mail = Self::try_from(&imap_mail).unwrap(); - // example: - // add more details from imap if given, - importable_mail.is_phishing = false; - importable_mail.unread = true; + // example: + // add more details from imap if given, + importable_mail.is_phishing = false; + importable_mail.unread = true; - Ok(importable_mail) - } + Ok(importable_mail) + } } #[derive(Debug, Clone, PartialEq)] pub enum MailParseError { - InconsistentParts(&'static str), - NoSentDate, - NoRecipient, - NoFrom, - InvalidDate, - InvalidHtmlBody, - InvalidTextBody, - InvalidMimeMessage, - EmptyMailAddress, - Unknown(String), + InconsistentParts(&'static str), + NoSentDate, + NoRecipient, + NoFrom, + InvalidDate, + InvalidHtmlBody, + InvalidTextBody, + InvalidMimeMessage, + EmptyMailAddress, + Unknown(String), } /// allow to convert from parsed message impl<'x> TryFrom<&mail_parser::Message<'x>> for ImportableMail { - type Error = MailParseError; - - fn try_from(parsed_message: &mail_parser::Message) -> Result { - let subject = parsed_message.subject().unwrap_or_default().to_string(); - - let (html_body_text, attachments) = ImportableMail::process_all_parts(&parsed_message)?; - - let date = parsed_message - .date() - .as_ref() - .map(|date_time| DateTime::from_millis(date_time.to_timestamp() as u64 * 1000)); - - let from_addresses = ImportableMail::map_to_tuta_mail_address( - parsed_message.from().map(Cow::Borrowed).unwrap_or_else(|| { - parsed_message - .sender() - .map(Cow::Borrowed) - .unwrap_or_else(|| Cow::Owned(mail_parser::Address::List(vec![]))) - }), - ) - .into_iter() - .map(|mut address| { - // we currently use the name as address if no address was defined on server side - if address.mail_address.is_empty() { - address.mail_address = address.name; - address.name = String::new(); - } - address - }) - .collect::>(); - - let different_envelope_sender = parsed_message - .sender() - .map(|sender| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(sender))) - // sender is allowed to be empty - .unwrap_or_default() - // there should only be one different envelope sender - .pop() - .map(|mut address| { - // we currently use the name as address if no address was defined on server side - if address.mail_address.is_empty() { - address.mail_address = address.name; - address.name = String::new(); - } - address - }) - // different envelope sender should not contain address listed in from_addresses; - .filter(|diff_sender| { - from_addresses + type Error = MailParseError; + + fn try_from(parsed_message: &mail_parser::Message) -> Result { + let subject = parsed_message.subject().unwrap_or_default().to_string(); + + let (html_body_text, attachments) = ImportableMail::process_all_parts(&parsed_message)?; + + let date = parsed_message + .date() + .as_ref() + .map(|date_time| DateTime::from_millis(date_time.to_timestamp() as u64 * 1000)); + + let from_addresses = ImportableMail::map_to_tuta_mail_address( + parsed_message.from().map(Cow::Borrowed).unwrap_or_else(|| { + parsed_message + .sender() + .map(Cow::Borrowed) + .unwrap_or_else(|| Cow::Owned(mail_parser::Address::List(vec![]))) + }), + ) + .into_iter() + .map(|mut address| { + // we currently use the name as address if no address was defined on server side + if address.mail_address.is_empty() { + address.mail_address = address.name; + address.name = String::new(); + } + address + }) + .collect::>(); + + let different_envelope_sender = parsed_message + .sender() + .map(|sender| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(sender))) + // sender is allowed to be empty + .unwrap_or_default() + // there should only be one different envelope sender + .pop() + .map(|mut address| { + // we currently use the name as address if no address was defined on server side + if address.mail_address.is_empty() { + address.mail_address = address.name; + address.name = String::new(); + } + address + }) + // different envelope sender should not contain address listed in from_addresses; + .filter(|diff_sender| { + from_addresses .iter() .any(|from| from.mail_address != diff_sender.mail_address) - }) - .map(|mail_address| mail_address.mail_address); - - let to_addresses = parsed_message - .to() - .map(|to| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(to))) - .unwrap_or_default() - .into_iter() - .filter(|address| !address.mail_address.trim().is_empty()) - .collect(); - - let cc_addresses = parsed_message - .cc() - .map(|cc| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(cc))) - .unwrap_or_default() - .into_iter() - .filter(|address| !address.mail_address.trim().is_empty()) - .collect(); - - let bcc_addresses = parsed_message - .bcc() - .map(|bcc| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(bcc))) - .unwrap_or_default() - .into_iter() - .filter(|address| !address.mail_address.trim().is_empty()) - .collect(); - - let reply_to_addresses = parsed_message - .reply_to() - .map(|reply_to| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(reply_to))) - .unwrap_or_default() - .into_iter() - .filter(|address| !address.mail_address.trim().is_empty()) - .collect(); - - let headers_string = parsed_message - .headers_raw() - .map(|(name, value)| name.to_string() + ":" + value) - .collect::>() - .join(""); - - let reply_type = extend_mail_parser::get_reply_type_from_headers(parsed_message.headers()); - let message_id = parsed_message.message_id().map(String::from); - let in_reply_to = parsed_message.in_reply_to().as_text().map(String::from); - let references = match parsed_message.references() { - HeaderValue::Text(reference) => { - vec![reference.to_string()] - } - HeaderValue::TextList(references) => { - references.iter().map(|cow| cow.to_string()).collect() - } - _ => { - vec![] - } - }; - - Ok(Self { - headers_string, - html_body_text, - subject, - different_envelope_sender, - from_addresses, - to_addresses, - cc_addresses, - bcc_addresses, - reply_to_addresses, - date, - reply_type, - message_id, - in_reply_to, - references, - attachments, - - ical_type: Default::default(), - unread: false, - mail_state: Default::default(), - is_phishing: false, - }) - } + }) + .map(|mail_address| mail_address.mail_address); + + let to_addresses = parsed_message + .to() + .map(|to| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(to))) + .unwrap_or_default() + .into_iter() + .filter(|address| !address.mail_address.trim().is_empty()) + .collect(); + + let cc_addresses = parsed_message + .cc() + .map(|cc| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(cc))) + .unwrap_or_default() + .into_iter() + .filter(|address| !address.mail_address.trim().is_empty()) + .collect(); + + let bcc_addresses = parsed_message + .bcc() + .map(|bcc| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(bcc))) + .unwrap_or_default() + .into_iter() + .filter(|address| !address.mail_address.trim().is_empty()) + .collect(); + + let reply_to_addresses = parsed_message + .reply_to() + .map(|reply_to| ImportableMail::map_to_tuta_mail_address(Cow::Borrowed(reply_to))) + .unwrap_or_default() + .into_iter() + .filter(|address| !address.mail_address.trim().is_empty()) + .collect(); + + let headers_string = parsed_message + .headers_raw() + .map(|(name, value)| name.to_string() + ":" + value) + .collect::>() + .join(""); + + let reply_type = extend_mail_parser::get_reply_type_from_headers(parsed_message.headers()); + let message_id = parsed_message.message_id().map(String::from); + let in_reply_to = parsed_message.in_reply_to().as_text().map(String::from); + let references = match parsed_message.references() { + HeaderValue::Text(reference) => { + vec![reference.to_string()] + }, + HeaderValue::TextList(references) => { + references.iter().map(|cow| cow.to_string()).collect() + }, + _ => { + vec![] + }, + }; + + Ok(Self { + headers_string, + html_body_text, + subject, + different_envelope_sender, + from_addresses, + to_addresses, + cc_addresses, + bcc_addresses, + reply_to_addresses, + date, + reply_type, + message_id, + in_reply_to, + references, + attachments, + + ical_type: Default::default(), + unread: false, + mail_state: Default::default(), + is_phishing: false, + }) + } } #[cfg(test)] diff --git a/packages/node-mimimi/src/reduce_to_chunks.rs b/packages/node-mimimi/src/reduce_to_chunks.rs index bd31efe125c..1f2403d5244 100644 --- a/packages/node-mimimi/src/reduce_to_chunks.rs +++ b/packages/node-mimimi/src/reduce_to_chunks.rs @@ -3,115 +3,115 @@ use std::ops::Deref; struct ChunkingIterator where - Inner: Iterator + Send, - Element: Send, + Inner: Iterator + Send, + Element: Send, { - inner: Peekable, - max_size: usize, - sizer: Box usize + Send>, + inner: Peekable, + max_size: usize, + sizer: Box usize + Send>, } impl Iterator for ChunkingIterator where - Inner: Iterator + Send, - Element: Send, + Inner: Iterator + Send, + Element: Send, { - type Item = Vec; - fn next(&mut self) -> Option { - let seq = &mut self.inner; - let mut element = seq.peek()?; + type Item = Vec; + fn next(&mut self) -> Option { + let seq = &mut self.inner; + let mut element = seq.peek()?; - let mut chunk: Vec = Vec::new(); - let mut current_chunk_size = 0_usize; - loop { - let element_size = self.sizer.deref()(element); - if element_size > self.max_size { - // this element is too big for one chunk. we might just ignore that and make a - // one-element chunk that fails to upload, or we stop iteration here. - // this discards any elements already in the chunk - return None; - } - let new_chunk_size = current_chunk_size.saturating_add(element_size); - if new_chunk_size > self.max_size { - // chunk is full - this element goes into the next chunk. - // because we used peek() it'll still be available for the next call to this function. - return Some(chunk); - } else { - current_chunk_size = new_chunk_size; - chunk.push( - seq.next() - .expect("got None from next even though peek() gave Some"), - ); - element = match seq.peek() { - None => break, - Some(e) => e, - }; - } - } - Some(chunk) - } + let mut chunk: Vec = Vec::new(); + let mut current_chunk_size = 0_usize; + loop { + let element_size = self.sizer.deref()(element); + if element_size > self.max_size { + // this element is too big for one chunk. we might just ignore that and make a + // one-element chunk that fails to upload, or we stop iteration here. + // this discards any elements already in the chunk + return None; + } + let new_chunk_size = current_chunk_size.saturating_add(element_size); + if new_chunk_size > self.max_size { + // chunk is full - this element goes into the next chunk. + // because we used peek() it'll still be available for the next call to this function. + return Some(chunk); + } else { + current_chunk_size = new_chunk_size; + chunk.push( + seq.next() + .expect("got None from next even though peek() gave Some"), + ); + element = match seq.peek() { + None => break, + Some(e) => e, + }; + } + } + Some(chunk) + } } /// split a given vector of elements into a vector of chunks not exceeding max_size, where the /// chunks size is calculated by summing up the elements sizes as given by the sizer function. /// /// the number of chunks is not guaranteed to be optimal. pub fn reduce_to_chunks<'element, Element: 'element + Send>( - seq: impl Iterator + Send, - max_size: usize, - sizer: Box usize>, -) -> impl Iterator> + Send { - ChunkingIterator { - inner: seq.peekable(), - max_size, - sizer, - } + seq: impl Iterator + Send, + max_size: usize, + sizer: Box usize>, +) -> impl Iterator> + Send { + ChunkingIterator { + inner: seq.peekable(), + max_size, + sizer, + } } #[cfg(test)] mod tests { - use crate::reduce_to_chunks::reduce_to_chunks; + use crate::reduce_to_chunks::reduce_to_chunks; - #[test] - fn reduce_to_chunks_simple() { - assert_eq!( - vec![vec![1, 2, 3], vec![4], vec![5], vec![6]], - reduce_to_chunks::( - vec![1, 2, 3, 4, 5, 6].into_iter(), - 6, - Box::new(|item| { *item }) - ) - .collect::>>() - ); - } + #[test] + fn reduce_to_chunks_simple() { + assert_eq!( + vec![vec![1, 2, 3], vec![4], vec![5], vec![6]], + reduce_to_chunks::( + vec![1, 2, 3, 4, 5, 6].into_iter(), + 6, + Box::new(|item| { *item }) + ) + .collect::>>() + ); + } - #[test] - fn reduce_to_chunks_no_split() { - assert_eq!( - vec![vec![1, 2, 3, 4, 5, 6], ], - reduce_to_chunks::( - vec![1, 2, 3, 4, 5, 6].into_iter(), - 21, - Box::new(|item| { *item }) - ) - .collect::>>() - ); - } + #[test] + fn reduce_to_chunks_no_split() { + assert_eq!( + vec![vec![1, 2, 3, 4, 5, 6],], + reduce_to_chunks::( + vec![1, 2, 3, 4, 5, 6].into_iter(), + 21, + Box::new(|item| { *item }) + ) + .collect::>>() + ); + } - #[test] - fn reduce_to_chunks_empty() { - assert_eq!( - Vec::>::new(), - reduce_to_chunks::(vec![].into_iter(), 0, Box::new(|item| { *item })) - .collect::>>() - ); - } + #[test] + fn reduce_to_chunks_empty() { + assert_eq!( + Vec::>::new(), + reduce_to_chunks::(vec![].into_iter(), 0, Box::new(|item| { *item })) + .collect::>>() + ); + } - #[test] - fn split_too_big() { - assert_eq!( - Vec::>::new(), - reduce_to_chunks::(vec![1, 10, 11].into_iter(), 2, Box::new(|item| { *item })) - .collect::>>() - ); - } + #[test] + fn split_too_big() { + assert_eq!( + Vec::>::new(), + reduce_to_chunks::(vec![1, 10, 11].into_iter(), 2, Box::new(|item| { *item })) + .collect::>>() + ); + } } From c54b0038b2168a15c6ed241a14531c6c9b80924a Mon Sep 17 00:00:00 2001 From: nig Date: Thu, 14 Nov 2024 16:54:05 +0100 Subject: [PATCH 28/32] fix entity_facade test for aggregate id --- .../rust/sdk/src/entities/entity_facade.rs | 71 ++- tuta-sdk/rust/sdk/src/util/test_utils.rs | 535 +++++++++--------- 2 files changed, 326 insertions(+), 280 deletions(-) diff --git a/tuta-sdk/rust/sdk/src/entities/entity_facade.rs b/tuta-sdk/rust/sdk/src/entities/entity_facade.rs index 47802fa62b6..ed3a0582acd 100644 --- a/tuta-sdk/rust/sdk/src/entities/entity_facade.rs +++ b/tuta-sdk/rust/sdk/src/entities/entity_facade.rs @@ -192,11 +192,9 @@ impl EntityFacadeImpl { let id_key = String::from("_id"); match encrypted.get(&id_key) { Some(ElementValue::Null) => { - let new_id = self.randomizer_facade.generate_random_array::<4>(); - encrypted.insert( id_key, - ElementValue::String(BASE64_URL_SAFE_NO_PAD.encode(new_id)), + make_random_aggregate_id(&self.randomizer_facade), ); } Some(_) => { @@ -626,6 +624,13 @@ impl EntityFacade for EntityFacadeImpl { } } +fn make_random_aggregate_id(random: &RandomizerFacade) -> ElementValue { + let new_id_bytes = random.generate_random_array::<4>(); + let new_id_string = BASE64_URL_SAFE_NO_PAD.encode(new_id_bytes); + let new_id = crate::CustomId(new_id_string); + ElementValue::IdCustomId(new_id) +} + #[cfg(test)] mod lz4_compressed_string_compatibility_tests { use crate::crypto::compatibility_test_utils::{ @@ -710,9 +715,7 @@ mod tests { use crate::crypto::{aes::Iv, Aes256Key}; use crate::date::DateTime; use crate::element_value::{ElementValue, ParsedEntity}; - use crate::entities::entity_facade::{ - EntityFacade, EntityFacadeImpl, MappedValue, MAX_UNCOMPRESSED_INPUT_LZ4, - }; + use crate::entities::entity_facade::{make_random_aggregate_id, EntityFacade, EntityFacadeImpl, MappedValue, MAX_UNCOMPRESSED_INPUT_LZ4}; use crate::entities::generated::sys::CustomerAccountTerminationRequest; use crate::entities::generated::tutanota::Mail; use crate::entities::Entity; @@ -1075,7 +1078,12 @@ mod tests { let owner_enc_session_key = [0, 1, 2]; let deterministic_rng = DeterministicRng(20); - let iv = Iv::generate(&RandomizerFacade::from_core(deterministic_rng.clone())); + let random = RandomizerFacade::from_core(deterministic_rng.clone()); + // we're kind of using the implementation to test itself here, but + // this is not about the actual value, but that it is set at all + let expected_aggregate_id = make_random_aggregate_id(&random); + + let iv = Iv::generate(&random); let type_model_provider = Arc::new(init_type_model_provider()); let type_ref = Mail::type_ref(); @@ -1088,7 +1096,7 @@ mod tests { RandomizerFacade::from_core(deterministic_rng), ); - let (mut expected_encrypted_mail, raw_mail) = generate_email_entity( + let (mut expected_encrypted_mail, mut raw_mail) = generate_email_entity( &sk, &iv, true, @@ -1097,7 +1105,7 @@ mod tests { String::from("Munich"), ); - // remove finalIvs for easy comparision + // removes finalIvs for easy comparison as well as setting the aggregate _id fields { expected_encrypted_mail.remove("_finalIvs").unwrap(); expected_encrypted_mail @@ -1106,22 +1114,59 @@ mod tests { .assert_dict_mut_ref() .remove("_finalIvs") .unwrap(); + expected_encrypted_mail + .get_mut("sender") + .unwrap() + .assert_dict_mut_ref() + .insert("_id".to_string(), expected_aggregate_id.clone()); expected_encrypted_mail .get_mut("firstRecipient") .unwrap() .assert_dict_mut_ref() .remove("_finalIvs") .unwrap(); + expected_encrypted_mail + .get_mut("firstRecipient") + .unwrap() + .assert_dict_mut_ref() + .insert("_id".to_string(), expected_aggregate_id.clone()); } - let encrypted_mail = entity_facade.encrypt_and_map_inner(type_model, &raw_mail, &sk); + { + // generate_email_entity generates aggregate ids for us, but the entity_facade is supposed to set + // them on the fly if they're missing. by removing them here we test that they're re-created + raw_mail + .get_mut("sender") + .unwrap() + .assert_dict_mut_ref() + .insert(String::from("_id"), ElementValue::Null); + raw_mail + .get_mut("firstRecipient") + .unwrap() + .assert_dict_mut_ref() + .insert(String::from("_id"), ElementValue::Null); + } + + let encrypted_mail = entity_facade.encrypt_and_map_inner(type_model, &raw_mail, &sk).unwrap(); - assert_eq!(Ok(expected_encrypted_mail), encrypted_mail); + assert_eq!(expected_encrypted_mail, encrypted_mail); // verify every data is preserved as is after decryption { - let original_mail = raw_mail; - let encrypted_mail = encrypted_mail.unwrap(); + let mut original_mail = raw_mail; + { + original_mail + .get_mut("sender") + .unwrap() + .assert_dict_mut_ref() + .insert(String::from("_id"), expected_aggregate_id.clone()); + original_mail + .get_mut("firstRecipient") + .unwrap() + .assert_dict_mut_ref() + .insert(String::from("_id"), expected_aggregate_id.clone()); + } + let encrypted_mail = encrypted_mail; let mut decrypted_mail = entity_facade .decrypt_and_map( diff --git a/tuta-sdk/rust/sdk/src/util/test_utils.rs b/tuta-sdk/rust/sdk/src/util/test_utils.rs index 7f14f451b34..021acf33002 100644 --- a/tuta-sdk/rust/sdk/src/util/test_utils.rs +++ b/tuta-sdk/rust/sdk/src/util/test_utils.rs @@ -7,7 +7,7 @@ use crate::crypto::randomizer_facade::test_util::make_thread_rng_facade; use crate::crypto::Aes256Key; use crate::element_value::{ElementValue, ParsedEntity}; use crate::entities::generated::sys::{ - ArchiveRef, ArchiveType, Group, GroupKeysRef, KeyPair, PubEncKeyData, TypeInfo, + ArchiveRef, ArchiveType, Group, GroupKeysRef, KeyPair, PubEncKeyData, TypeInfo, }; use crate::entities::Entity; use crate::instance_mapper::InstanceMapper; @@ -22,69 +22,70 @@ use crate::{IdTupleCustom, IdTupleGenerated}; /// Generates a URL-safe random string of length `Size`. #[must_use] pub fn generate_random_string() -> String { - use base64::engine::Engine; - let random_bytes: [u8; SIZE] = make_thread_rng_facade().generate_random_array(); - base64::engine::general_purpose::URL_SAFE.encode(random_bytes) + use base64::engine::Engine; + use base64::prelude::BASE64_URL_SAFE_NO_PAD; + let random_bytes: [u8; SIZE] = make_thread_rng_facade().generate_random_array(); + BASE64_URL_SAFE_NO_PAD.encode(random_bytes) } pub fn generate_random_group( - current_keys: Option, - former_keys: Option, + current_keys: Option, + former_keys: Option, ) -> Group { - Group { - _format: 0, - _id: Some(GeneratedId::test_random()), - _ownerGroup: None, - _permissions: GeneratedId::test_random(), - groupInfo: IdTupleGenerated::new(GeneratedId::test_random(), GeneratedId::test_random()), - administratedGroups: None, - archives: vec![ArchiveType { - _id: Some(CustomId::test_random()), - active: ArchiveRef { - _id: Some(CustomId::test_random()), - archiveId: GeneratedId::test_random(), - }, - inactive: vec![], - r#type: TypeInfo { - _id: Some(CustomId::test_random()), - application: "app".to_string(), - typeId: 1, - }, - }], - currentKeys: current_keys, - customer: None, - formerGroupKeys: former_keys, - invitations: GeneratedId::test_random(), - members: GeneratedId::test_random(), - groupKeyVersion: 1, - admin: None, - r#type: 46, - adminGroupEncGKey: None, - adminGroupKeyVersion: None, - enabled: true, - external: false, - pubAdminGroupEncGKey: Some(PubEncKeyData { - _id: Some(CustomId::test_random()), - recipientIdentifier: "adminGroupId".to_string(), - recipientIdentifierType: PublicKeyIdentifierType::GroupId as i64, - protocolVersion: CryptoProtocolVersion::Tutacrypt as i64, - pubEncSymKey: vec![1, 2, 3], - recipientKeyVersion: 0, - senderKeyVersion: Some(0), - }), - storageCounter: None, - user: None, - } + Group { + _format: 0, + _id: Some(GeneratedId::test_random()), + _ownerGroup: None, + _permissions: GeneratedId::test_random(), + groupInfo: IdTupleGenerated::new(GeneratedId::test_random(), GeneratedId::test_random()), + administratedGroups: None, + archives: vec![ArchiveType { + _id: Some(CustomId::test_random()), + active: ArchiveRef { + _id: Some(CustomId::test_random()), + archiveId: GeneratedId::test_random(), + }, + inactive: vec![], + r#type: TypeInfo { + _id: Some(CustomId::test_random()), + application: "app".to_string(), + typeId: 1, + }, + }], + currentKeys: current_keys, + customer: None, + formerGroupKeys: former_keys, + invitations: GeneratedId::test_random(), + members: GeneratedId::test_random(), + groupKeyVersion: 1, + admin: None, + r#type: 46, + adminGroupEncGKey: None, + adminGroupKeyVersion: None, + enabled: true, + external: false, + pubAdminGroupEncGKey: Some(PubEncKeyData { + _id: Some(CustomId::test_random()), + recipientIdentifier: "adminGroupId".to_string(), + recipientIdentifierType: PublicKeyIdentifierType::GroupId as i64, + protocolVersion: CryptoProtocolVersion::Tutacrypt as i64, + pubEncSymKey: vec![1, 2, 3], + recipientKeyVersion: 0, + senderKeyVersion: Some(0), + }), + storageCounter: None, + user: None, + } } pub fn random_aes256_key() -> Aes256Key { - Aes256Key::from_bytes(&random::<[u8; 32]>()).unwrap() + Aes256Key::from_bytes(&random::<[u8; 32]>()).unwrap() } /// Moves the object T into heap and leaks it. #[inline(always)] pub fn leak(what: T) -> &'static T { - Box::leak(Box::new(what)) + Box::leak(Box::new(what)) } /// Generate a test entity. @@ -109,17 +110,17 @@ pub fn leak(what: T) -> &'static T { /// ``` #[must_use] pub fn create_test_entity<'a, T: Entity + serde::Deserialize<'a>>() -> T { - let mapper = InstanceMapper::new(); - let entity = create_test_entity_dict::(); - let type_ref = T::type_ref(); - match mapper.parse_entity(entity) { - Ok(n) => n, - Err(e) => panic!( - "Failed to create test entity {app}/{type_}: parse error {e}", - app = type_ref.app, - type_ = type_ref.type_ - ), - } + let mapper = InstanceMapper::new(); + let entity = create_test_entity_dict::(); + let type_ref = T::type_ref(); + match mapper.parse_entity(entity) { + Ok(n) => n, + Err(e) => panic!( + "Failed to create test entity {app}/{type_}: parse error {e}", + app = type_ref.app, + type_ = type_ref.type_ + ), + } } /// Generate a test entity as a raw `ParsedEntity` dictionary type. @@ -132,10 +133,10 @@ pub fn create_test_entity<'a, T: Entity + serde::Deserialize<'a>>() -> T { /// **NOTE:** The resulting dictionary is unencrypted. #[must_use] pub fn create_test_entity_dict<'a, T: Entity + serde::Deserialize<'a>>() -> ParsedEntity { - let provider = init_type_model_provider(); - let type_ref = T::type_ref(); - let entity = create_test_entity_dict_with_provider(&provider, type_ref.app, type_ref.type_); - entity + let provider = init_type_model_provider(); + let type_ref = T::type_ref(); + let entity = create_test_entity_dict_with_provider(&provider, type_ref.app, type_ref.type_); + entity } /// Generate a test entity as a raw `ParsedEntity` dictionary type. @@ -150,11 +151,11 @@ pub fn create_test_entity_dict<'a, T: Entity + serde::Deserialize<'a>>() -> Pars #[must_use] #[allow(dead_code)] pub fn create_encrypted_test_entity_dict<'a, T: Entity + serde::Deserialize<'a>>() -> ParsedEntity { - let provider = init_type_model_provider(); - let type_ref = T::type_ref(); - let entity = - create_encrypted_test_entity_dict_with_provider(&provider, type_ref.app, type_ref.type_); - entity + let provider = init_type_model_provider(); + let type_ref = T::type_ref(); + let entity = + create_encrypted_test_entity_dict_with_provider(&provider, type_ref.app, type_ref.type_); + entity } /// Convert a typed entity into a raw `ParsedEntity` dictionary type. @@ -164,218 +165,218 @@ pub fn create_encrypted_test_entity_dict<'a, T: Entity + serde::Deserialize<'a>> /// Panics if the resulting entity is invalid and unable to be serialized. #[must_use] pub fn typed_entity_to_parsed_entity(entity: T) -> ParsedEntity { - let mapper = InstanceMapper::new(); - match mapper.serialize_entity(entity) { - Ok(n) => n, - Err(e) => panic!( - "Failed to serialize {}/{}: {:?}", - T::type_ref().app, - T::type_ref().type_, - e - ), - } + let mapper = InstanceMapper::new(); + match mapper.serialize_entity(entity) { + Ok(n) => n, + Err(e) => panic!( + "Failed to serialize {}/{}: {:?}", + T::type_ref().app, + T::type_ref().type_, + e + ), + } } fn create_test_entity_dict_with_provider( - provider: &TypeModelProvider, - app: &str, - type_: &str, + provider: &TypeModelProvider, + app: &str, + type_: &str, ) -> ParsedEntity { - let Some(model) = provider.get_type_model(app, type_) else { - panic!("Failed to create test entity {app}/{type_}: not in model") - }; - let mut object = ParsedEntity::new(); + let Some(model) = provider.get_type_model(app, type_) else { + panic!("Failed to create test entity {app}/{type_}: not in model") + }; + let mut object = ParsedEntity::new(); - for (&name, value) in &model.values { - let element_value = match value.cardinality { - Cardinality::ZeroOrOne => ElementValue::Null, - Cardinality::Any => ElementValue::Array(Vec::new()), - Cardinality::One => match value.value_type { - ValueType::String | ValueType::CompressedString => { - ElementValue::String(Default::default()) - }, - ValueType::Number => ElementValue::Number(Default::default()), - ValueType::Bytes => ElementValue::Bytes(Default::default()), - ValueType::Date => ElementValue::Date(Default::default()), - ValueType::Boolean => ElementValue::Bool(Default::default()), - ValueType::GeneratedId => { - if name == "_id" - && (model.element_type == ElementType::ListElement - || model.element_type == ElementType::BlobElement) - { - ElementValue::IdTupleGeneratedElementId(IdTupleGenerated::new( - GeneratedId::test_random(), - GeneratedId::test_random(), - )) - } else { - ElementValue::IdGeneratedId(GeneratedId::test_random()) - } - }, - ValueType::CustomId => { - if name == "_id" && (model.element_type == ElementType::ListElement) { - ElementValue::IdTupleCustomElementId(IdTupleCustom::new( - GeneratedId::test_random(), - CustomId::test_random(), - )) - } else if name == "_id" && model.element_type == Aggregated { - ElementValue::IdCustomId(CustomId::test_random_aggregate()) - } else { - ElementValue::IdCustomId(CustomId::test_random()) - } - }, - }, - }; + for (&name, value) in &model.values { + let element_value = match value.cardinality { + Cardinality::ZeroOrOne => ElementValue::Null, + Cardinality::Any => ElementValue::Array(Vec::new()), + Cardinality::One => match value.value_type { + ValueType::String | ValueType::CompressedString => { + ElementValue::String(Default::default()) + } + ValueType::Number => ElementValue::Number(Default::default()), + ValueType::Bytes => ElementValue::Bytes(Default::default()), + ValueType::Date => ElementValue::Date(Default::default()), + ValueType::Boolean => ElementValue::Bool(Default::default()), + ValueType::GeneratedId => { + if name == "_id" + && (model.element_type == ElementType::ListElement + || model.element_type == ElementType::BlobElement) + { + ElementValue::IdTupleGeneratedElementId(IdTupleGenerated::new( + GeneratedId::test_random(), + GeneratedId::test_random(), + )) + } else { + ElementValue::IdGeneratedId(GeneratedId::test_random()) + } + } + ValueType::CustomId => { + if name == "_id" && (model.element_type == ElementType::ListElement) { + ElementValue::IdTupleCustomElementId(IdTupleCustom::new( + GeneratedId::test_random(), + CustomId::test_random(), + )) + } else if name == "_id" && model.element_type == Aggregated { + ElementValue::IdCustomId(CustomId::test_random_aggregate()) + } else { + ElementValue::IdCustomId(CustomId::test_random()) + } + } + }, + }; - object.insert(name.to_owned(), element_value); - } + object.insert(name.to_owned(), element_value); + } - for (&name, value) in &model.associations { - let association_value = match value.cardinality { - Cardinality::ZeroOrOne => ElementValue::Null, - Cardinality::Any => ElementValue::Array(Vec::new()), - Cardinality::One => match value.association_type { - AssociationType::ElementAssociation => { - ElementValue::IdGeneratedId(GeneratedId::test_random()) - }, - AssociationType::ListAssociation => { - ElementValue::IdGeneratedId(GeneratedId::test_random()) - }, - AssociationType::ListElementAssociationGenerated => { - ElementValue::IdTupleGeneratedElementId(IdTupleGenerated::new( - GeneratedId::test_random(), - GeneratedId::test_random(), - )) - }, - AssociationType::ListElementAssociationCustom => { - ElementValue::IdTupleCustomElementId(IdTupleCustom::new( - GeneratedId::test_random(), - CustomId::test_random(), - )) - }, - AssociationType::Aggregation => { - ElementValue::Dict(create_test_entity_dict_with_provider( - provider, - value.dependency.unwrap_or(app), - value.ref_type, - )) - }, - AssociationType::BlobElementAssociation => ElementValue::IdTupleGeneratedElementId( - IdTupleGenerated::new(GeneratedId::test_random(), GeneratedId::test_random()), - ), - }, - }; - object.insert(name.to_owned(), association_value); - } + for (&name, value) in &model.associations { + let association_value = match value.cardinality { + Cardinality::ZeroOrOne => ElementValue::Null, + Cardinality::Any => ElementValue::Array(Vec::new()), + Cardinality::One => match value.association_type { + AssociationType::ElementAssociation => { + ElementValue::IdGeneratedId(GeneratedId::test_random()) + } + AssociationType::ListAssociation => { + ElementValue::IdGeneratedId(GeneratedId::test_random()) + } + AssociationType::ListElementAssociationGenerated => { + ElementValue::IdTupleGeneratedElementId(IdTupleGenerated::new( + GeneratedId::test_random(), + GeneratedId::test_random(), + )) + } + AssociationType::ListElementAssociationCustom => { + ElementValue::IdTupleCustomElementId(IdTupleCustom::new( + GeneratedId::test_random(), + CustomId::test_random(), + )) + } + AssociationType::Aggregation => { + ElementValue::Dict(create_test_entity_dict_with_provider( + provider, + value.dependency.unwrap_or(app), + value.ref_type, + )) + } + AssociationType::BlobElementAssociation => ElementValue::IdTupleGeneratedElementId( + IdTupleGenerated::new(GeneratedId::test_random(), GeneratedId::test_random()), + ), + }, + }; + object.insert(name.to_owned(), association_value); + } - if model.is_encrypted() { - object.insert( - "_finalIvs".to_owned(), - ElementValue::Dict(Default::default()), - ); - } + if model.is_encrypted() { + object.insert( + "_finalIvs".to_owned(), + ElementValue::Dict(Default::default()), + ); + } - object + object } fn create_encrypted_test_entity_dict_with_provider( - provider: &TypeModelProvider, - app: &str, - type_: &str, + provider: &TypeModelProvider, + app: &str, + type_: &str, ) -> ParsedEntity { - let Some(model) = provider.get_type_model(app, type_) else { - panic!("Failed to create test entity {app}/{type_}: not in model") - }; - let mut object = ParsedEntity::new(); + let Some(model) = provider.get_type_model(app, type_) else { + panic!("Failed to create test entity {app}/{type_}: not in model") + }; + let mut object = ParsedEntity::new(); - for (&name, value) in &model.values { - let element_value = match value.cardinality { - Cardinality::ZeroOrOne => ElementValue::Null, - Cardinality::Any => ElementValue::Array(Vec::new()), - Cardinality::One => { - if value.encrypted { - ElementValue::String(Default::default()) - } else { - match value.value_type { - ValueType::String | ValueType::CompressedString => { - ElementValue::String(Default::default()) - }, - ValueType::Number => ElementValue::Number(Default::default()), - ValueType::Bytes => ElementValue::Bytes(Default::default()), - ValueType::Date => ElementValue::Date(Default::default()), - ValueType::Boolean => ElementValue::Bool(Default::default()), - ValueType::GeneratedId => { - if name == "_id" - && (model.element_type == ElementType::ListElement - || model.element_type == ElementType::BlobElement) - { - ElementValue::IdTupleGeneratedElementId(IdTupleGenerated::new( - GeneratedId::test_random(), - GeneratedId::test_random(), - )) - } else { - ElementValue::IdGeneratedId(GeneratedId::test_random()) - } - }, - ValueType::CustomId => { - if name == "_id" - && (model.element_type == ElementType::ListElement - || model.element_type == ElementType::BlobElement) - { - ElementValue::IdTupleCustomElementId(IdTupleCustom::new( - GeneratedId::test_random(), - CustomId::test_random(), - )) - } else { - ElementValue::IdCustomId(CustomId::test_random()) - } - }, - } - } - }, - }; + for (&name, value) in &model.values { + let element_value = match value.cardinality { + Cardinality::ZeroOrOne => ElementValue::Null, + Cardinality::Any => ElementValue::Array(Vec::new()), + Cardinality::One => { + if value.encrypted { + ElementValue::String(Default::default()) + } else { + match value.value_type { + ValueType::String | ValueType::CompressedString => { + ElementValue::String(Default::default()) + } + ValueType::Number => ElementValue::Number(Default::default()), + ValueType::Bytes => ElementValue::Bytes(Default::default()), + ValueType::Date => ElementValue::Date(Default::default()), + ValueType::Boolean => ElementValue::Bool(Default::default()), + ValueType::GeneratedId => { + if name == "_id" + && (model.element_type == ElementType::ListElement + || model.element_type == ElementType::BlobElement) + { + ElementValue::IdTupleGeneratedElementId(IdTupleGenerated::new( + GeneratedId::test_random(), + GeneratedId::test_random(), + )) + } else { + ElementValue::IdGeneratedId(GeneratedId::test_random()) + } + } + ValueType::CustomId => { + if name == "_id" + && (model.element_type == ElementType::ListElement + || model.element_type == ElementType::BlobElement) + { + ElementValue::IdTupleCustomElementId(IdTupleCustom::new( + GeneratedId::test_random(), + CustomId::test_random(), + )) + } else { + ElementValue::IdCustomId(CustomId::test_random()) + } + } + } + } + } + }; - object.insert(name.to_owned(), element_value); - } + object.insert(name.to_owned(), element_value); + } - for (&name, value) in &model.associations { - let association_value = match value.cardinality { - Cardinality::ZeroOrOne => ElementValue::Null, - Cardinality::Any => ElementValue::Array(Vec::new()), - Cardinality::One => match value.association_type { - AssociationType::ElementAssociation => { - ElementValue::IdGeneratedId(GeneratedId::test_random()) - }, - AssociationType::ListAssociation => { - ElementValue::IdGeneratedId(GeneratedId::test_random()) - }, - AssociationType::ListElementAssociationGenerated => { - ElementValue::IdTupleGeneratedElementId(IdTupleGenerated::new( - GeneratedId::test_random(), - GeneratedId::test_random(), - )) - }, - AssociationType::ListElementAssociationCustom => { - ElementValue::IdTupleCustomElementId(IdTupleCustom::new( - GeneratedId::test_random(), - CustomId::test_random(), - )) - }, - AssociationType::Aggregation => { - ElementValue::Dict(create_encrypted_test_entity_dict_with_provider( - provider, - value.dependency.unwrap_or(app), - value.ref_type, - )) - }, - AssociationType::BlobElementAssociation => ElementValue::IdTupleGeneratedElementId( - IdTupleGenerated::new(GeneratedId::test_random(), GeneratedId::test_random()), - ), - }, - }; - object.insert(name.to_owned(), association_value); - } + for (&name, value) in &model.associations { + let association_value = match value.cardinality { + Cardinality::ZeroOrOne => ElementValue::Null, + Cardinality::Any => ElementValue::Array(Vec::new()), + Cardinality::One => match value.association_type { + AssociationType::ElementAssociation => { + ElementValue::IdGeneratedId(GeneratedId::test_random()) + } + AssociationType::ListAssociation => { + ElementValue::IdGeneratedId(GeneratedId::test_random()) + } + AssociationType::ListElementAssociationGenerated => { + ElementValue::IdTupleGeneratedElementId(IdTupleGenerated::new( + GeneratedId::test_random(), + GeneratedId::test_random(), + )) + } + AssociationType::ListElementAssociationCustom => { + ElementValue::IdTupleCustomElementId(IdTupleCustom::new( + GeneratedId::test_random(), + CustomId::test_random(), + )) + } + AssociationType::Aggregation => { + ElementValue::Dict(create_encrypted_test_entity_dict_with_provider( + provider, + value.dependency.unwrap_or(app), + value.ref_type, + )) + } + AssociationType::BlobElementAssociation => ElementValue::IdTupleGeneratedElementId( + IdTupleGenerated::new(GeneratedId::test_random(), GeneratedId::test_random()), + ), + }, + }; + object.insert(name.to_owned(), association_value); + } - object + object } #[macro_export] From f937c1951cc141f6342e1834ec01a9b94df5ccb8 Mon Sep 17 00:00:00 2001 From: map Date: Fri, 15 Nov 2024 09:57:24 +0100 Subject: [PATCH 29/32] wip --- .../importable_mail/msg_file_compatibility_test.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs b/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs index c7d32d0e043..a2ef61c7b75 100644 --- a/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs +++ b/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs @@ -68,8 +68,14 @@ fn mime_tools_test_messages() { } let parsed_message = parsed_message_result.unwrap(); - let mut importable_mail: ImportMailData = parsed_message.into(); - let mut expected_importable_mail: ImportMailData = expected_result.unwrap().into(); + let (mut importable_mail, importable_mail_attachments): ( + ImportMailData, + Vec, + ) = parsed_message.into(); + let (mut expected_importable_mail, expected_mail_attachments): ( + ImportMailData, + Vec, + ) = expected_result.unwrap().into(); // importable_mail.attachments.clear(); // expected_importable_mail.attachments.clear(); @@ -88,6 +94,7 @@ fn mime_tools_test_messages() { importable_mail.differentEnvelopeSender = None; assert_eq!(importable_mail, expected_importable_mail); + assert_eq!(importable_mail_attachments, expected_mail_attachments); } } @@ -102,7 +109,7 @@ impl From for MailContact { } } -impl From for ImportMailData { +impl From for (ImportMailData, Vec) { fn from(expected_message: ExpectedMessage) -> Self { ImportableMail { headers_string: expected_message.mail_headers, From 180d82d465a89b57205bed4b7989be60a6c7b5ba Mon Sep 17 00:00:00 2001 From: jhm <17314077+jomapp@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:11:50 +0100 Subject: [PATCH 30/32] WIP: add test for import without server running --- packages/node-mimimi/src/importer.rs | 191 +++--------------- packages/node-mimimi/src/lib.rs | 1 + packages/node-mimimi/src/stub_loggedin_sdk.rs | 173 ++++++++++++++++ .../node-mimimi/test/attachment_sample.eml | 22 ++ tuta-sdk/rust/sdk/src/rest_client.rs | 56 ++--- tuta-sdk/rust/sdk/src/services.rs | 4 +- 6 files changed, 259 insertions(+), 188 deletions(-) create mode 100644 packages/node-mimimi/src/stub_loggedin_sdk.rs create mode 100644 packages/node-mimimi/test/attachment_sample.eml diff --git a/packages/node-mimimi/src/importer.rs b/packages/node-mimimi/src/importer.rs index d315d3a5552..4007ac98737 100644 --- a/packages/node-mimimi/src/importer.rs +++ b/packages/node-mimimi/src/importer.rs @@ -3,6 +3,7 @@ use crate::importer::imap_reader::import_client::{ImapImport, ImapIterationError use crate::importer::imap_reader::ImapImportConfig; use crate::importer::importable_mail::{ImportableMail, ImportableMailAttachment}; use crate::reduce_to_chunks::reduce_to_chunks; +use crate::stub_loggedin_sdk::LoggedInSdk; use crate::tuta::credentials::TutaCredentials; use napi::bindgen_prelude::Error as NapiError; use std::sync::{Arc, Mutex}; @@ -19,7 +20,7 @@ use tutasdk::services::generated::tutanota::ImportMailService; use tutasdk::services::ExtraServiceParams; use tutasdk::tutanota_constants::ArchiveDataType; use tutasdk::GeneratedId; -use tutasdk::{IdTupleGenerated, LoggedInSdk, Sdk}; +use tutasdk::{IdTupleGenerated, Sdk}; pub type NapiTokioMutex = napi::tokio::sync::Mutex; @@ -63,7 +64,7 @@ pub struct ImportStatus { struct Importer { status: ImportStatus, - logged_in_sdk: Arc, + logged_in_sdk: Arc, target_owner_group: GeneratedId, target_mail_folder: IdTupleGenerated, import_source: Arc>, @@ -141,7 +142,7 @@ impl Importer { importable_mails: Iter, ) -> Result, ()> where - Iter: Iterator + Send + 'static, + Iter: Iterator + Send + Sync + 'static, { let new_mail_aes_256_key = GenericAesKey::from_bytes( self.randomizer_facade @@ -192,7 +193,6 @@ impl Importer { let reference_tokens = self .logged_in_sdk - .blob_facade() .encrypt_and_upload( ArchiveDataType::Attachments, &self.target_owner_group, @@ -202,7 +202,6 @@ impl Importer { .await .unwrap(); - // todo: do we need to upload the ivs and how? let enc_file_name = new_file_aes_256_key .encrypt_data( importable_mail_attachment.filename.as_ref(), @@ -264,8 +263,7 @@ impl Importer { let response = self .logged_in_sdk - .get_service_executor() - .post::(import_mail_post_in, service_params) + .post_service_executor::(import_mail_post_in, service_params) .await; match response { @@ -304,7 +302,7 @@ impl Importer { impl ImporterApi { pub fn new( - logged_in_sdk: Arc, + logged_in_sdk: Arc, target_owner_group: GeneratedId, target_mail_folder: IdTupleGenerated, import_source: Arc>, @@ -360,7 +358,9 @@ impl ImporterApi { )) } - async fn create_sdk(tuta_credentials: TutaCredentials) -> Result, String> { + async fn create_sdk( + tuta_credentials: TutaCredentials, + ) -> Result, String> { let rest_client = Arc::new( NativeRestClient::try_new() .map_err(|e| format!("Cannot build native rest client: {e}"))?, @@ -433,13 +433,10 @@ impl ImporterApi { #[cfg(test)] mod tests { use super::*; - use crate::importer::imap_reader::{ImapCredentials, LoginMechanism}; - use crate::tuta_imap::testing::GreenMailTestServer; + use crate::stub_loggedin_sdk::StubLoggedInSdk; use mail_builder::MessageBuilder; - use tutasdk::entities::generated::tutanota::MailFolder; - use tutasdk::folder_system::MailSetKind; - use tutasdk::net::native_rest_client::NativeRestClient; - use tutasdk::Sdk; + + const TEST_EML_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/test"); fn sample_email(subject: String) -> String { let email = MessageBuilder::new() @@ -452,152 +449,30 @@ mod tests { email } - async fn get_test_import_folder_id( - logged_in_sdk: &Arc, - kind: MailSetKind, - ) -> MailFolder { - let mail_facade = logged_in_sdk.mail_facade(); - let mailbox = mail_facade.load_user_mailbox().await.unwrap(); - let folders = mail_facade - .load_folders_for_mailbox(&mailbox) - .await - .unwrap(); - folders - .system_folder_by_type(kind) - .expect("inbox should exist") - .clone() - } - - async fn init_imap_importer() -> (Importer, GreenMailTestServer) { - let importer_mail_address = "map-free@tutanota.de".to_string(); - let logged_in_sdk = Sdk::new( - "http://localhost:9000".to_string(), - Arc::new(NativeRestClient::try_new().unwrap()), - ) - .create_session(importer_mail_address.as_str(), "map") - .await - .unwrap(); - let greenmail = GreenMailTestServer::new(); - let imap_import_config = ImapImportConfig { - root_import_mail_folder_name: "/".to_string(), - credentials: ImapCredentials { - host: "127.0.0.1".to_string(), - port: greenmail.imaps_port.try_into().unwrap(), - login_mechanism: LoginMechanism::Plain { - username: "sug@example.org".to_string(), - password: "sug".to_string(), - }, + #[tokio::test] + async fn can_import_mail_with_single_attachment() { + let mut loggedin_sdk = StubLoggedInSdk::default(); + + let mut importer = Importer { + status: Default::default(), + logged_in_sdk: Arc::new(loggedin_sdk), + target_owner_group: Default::default(), + target_mail_folder: IdTupleGenerated { + list_id: Default::default(), + element_id: Default::default(), }, + import_source: Arc::new(Mutex::new(ImportSource::LocalFile { + fs_email_client: FileImport::new(vec![ + TEST_EML_DIR.to_string() + "/attachment_sample.eml", + ]) + .unwrap(), + })), + randomizer_facade: RandomizerFacade::from_core(rand::rngs::OsRng), }; - let import_source = Arc::new(Mutex::new(ImportSource::RemoteImap { - imap_import_client: ImapImport::new(imap_import_config), - })); - let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng); - let target_mail_folder = get_test_import_folder_id(&logged_in_sdk, MailSetKind::Archive) - .await - ._id - .unwrap(); - - let target_owner_group = logged_in_sdk - .mail_facade() - .get_group_id_for_mail_address(importer_mail_address.as_str()) - .await - .unwrap(); - - let importer = Importer { - target_owner_group, - target_mail_folder, - logged_in_sdk, - import_source, - randomizer_facade, - status: ImportStatus::default(), - }; - - (importer, greenmail) - } + let import_status = importer.continue_import().await.unwrap(); - pub async fn init_file_importer(source_paths: Vec) -> Importer { - let logged_in_sdk = Sdk::new( - "http://localhost:9000".to_string(), - Arc::new(NativeRestClient::try_new().unwrap()), - ) - .create_session("map-free@tutanota.de", "map") - .await - .unwrap(); - - let import_source = Arc::new(Mutex::new(ImportSource::LocalFile { - fs_email_client: FileImport::new(source_paths).unwrap(), - })); - let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng); - let target_mail_folder = get_test_import_folder_id(&logged_in_sdk, MailSetKind::Archive) - .await - ._id - .unwrap(); - - let target_owner_group = logged_in_sdk - .mail_facade() - .get_group_id_for_mail_address("map-free@tutanota.de") - .await - .unwrap(); - - Importer { - status: ImportStatus::default(), - target_owner_group, - target_mail_folder, - logged_in_sdk, - import_source, - randomizer_facade, - } - } - - #[tokio::test] - pub async fn import_multiple_from_imap_default_folder() { - let (mut importer, greenmail) = init_imap_importer().await; - - let email_first = sample_email("Hello from imap πŸ˜€! -- Бписок.doc".to_string()); - let email_second = sample_email("Second time: hello".to_string()); - greenmail.store_mail("sug@example.org", email_first.as_str()); - greenmail.store_mail("sug@example.org", email_second.as_str()); - - let import_res = importer.continue_import().await.map_err(|_| ()); - assert_eq!( - Ok(ImportStatus { - state: ImportState::Finished, - imported_mails: 2, - }), - import_res - ); - } - - #[tokio::test] - pub async fn import_single_from_imap_default_folder() { - let (mut importer, greenmail) = init_imap_importer().await; - - let email = sample_email("Single email".to_string()); - greenmail.store_mail("sug@example.org", email.as_str()); - - let import_res = importer.continue_import().await.map_err(|_| ()); - assert_eq!( - Ok(ImportStatus { - state: ImportState::Finished, - imported_mails: 1, - }), - import_res - ); - } - - #[tokio::test] - async fn can_import_single_eml_file() { - let mut importer = init_file_importer(vec!["./test/sample.eml".to_string()]).await; - - let import_res = importer.continue_import().await.map_err(|_| ()); - assert_eq!( - Ok(ImportStatus { - state: ImportState::Finished, - imported_mails: 1, - }), - import_res - ); + assert_eq!(import_status.imported_mails, 1); + assert_eq!(import_status.state, ImportState::Finished); } } diff --git a/packages/node-mimimi/src/lib.rs b/packages/node-mimimi/src/lib.rs index e1e09b69552..f49c6f2e388 100644 --- a/packages/node-mimimi/src/lib.rs +++ b/packages/node-mimimi/src/lib.rs @@ -6,3 +6,4 @@ pub mod logging; mod reduce_to_chunks; pub mod tuta; mod tuta_imap; +mod stub_loggedin_sdk; diff --git a/packages/node-mimimi/src/stub_loggedin_sdk.rs b/packages/node-mimimi/src/stub_loggedin_sdk.rs new file mode 100644 index 00000000000..b6ffb79fc42 --- /dev/null +++ b/packages/node-mimimi/src/stub_loggedin_sdk.rs @@ -0,0 +1,173 @@ +use std::sync::Arc; +use tutasdk::crypto::key::{GenericAesKey, VersionedAesKey}; +use tutasdk::entities::generated::sys::BlobReferenceTokenWrapper; +use tutasdk::services::service_executor::ResolvingServiceExecutor; +use tutasdk::services::{ExtraServiceParams, PostService}; +use tutasdk::tutanota_constants::ArchiveDataType; +use tutasdk::LoggedInSdk as RealLoggedInSdk; +use tutasdk::{ApiCallError, GeneratedId}; + +#[async_trait::async_trait] +// todo: think about restructuring to expose real LoggedInSdk hierarchy +pub trait LoggedInSdk: Sync + Send + Sized + 'static { + async fn get_current_sym_group_key( + &self, + user_group_id: &GeneratedId, + ) -> Result; + + async fn encrypt_and_upload( + &self, + archive_data_type: ArchiveDataType, + owner_group_id: &GeneratedId, + session_key: &GenericAesKey, + blob_data: Vec, + ) -> Result, ApiCallError>; + + async fn post_service_executor( + &self, + data: Service::Input, + params: ExtraServiceParams, + ) -> Result; + + async fn get_group_id_for_mail_address( + &self, + mail_address: &str, + ) -> Result; +} + +pub struct StubLoggedInSdk { + pub get_current_sym_group_key: Box< + dyn Send + + Sync + + Fn(&StubLoggedInSdk, &GeneratedId) -> Result, + >, + + pub encrypt_and_upload: Box< + dyn Send + + Sync + + Fn( + &StubLoggedInSdk, + ArchiveDataType, + &GeneratedId, + &GenericAesKey, + Vec, + ) -> Result, ApiCallError>, + >, + + pub get_service_executor: + Box &Arc>, + + pub get_group_id_for_mail_address: + Box Result>, +} + +#[async_trait::async_trait] +impl LoggedInSdk for StubLoggedInSdk { + async fn get_current_sym_group_key( + &self, + user_group_id: &GeneratedId, + ) -> Result { + (*self.get_current_sym_group_key)(self, user_group_id) + } + + async fn post_service_executor( + &self, + data: Service::Input, + params: ExtraServiceParams, + ) -> Result { + Ok(todo!()) + } + + async fn encrypt_and_upload( + &self, + archive_data_type: ArchiveDataType, + owner_group_id: &GeneratedId, + session_key: &GenericAesKey, + blob_data: Vec, + ) -> Result, ApiCallError> { + (*self.encrypt_and_upload)( + self, + archive_data_type, + owner_group_id, + session_key, + blob_data, + ) + } + + async fn get_group_id_for_mail_address( + &self, + mail_address: &str, + ) -> Result { + (*self.get_group_id_for_mail_address)(self, mail_address) + } +} + +impl Default for StubLoggedInSdk { + fn default() -> Self { + StubLoggedInSdk { + get_current_sym_group_key: Box::new(|_, _| { + Ok(VersionedAesKey { + object: GenericAesKey::from_bytes(&[0; 32]).unwrap(), + version: 0, + }) + }), + encrypt_and_upload: Box::new(|_, _, _, _, _| { + Ok(vec![ + BlobReferenceTokenWrapper { + _id: None, + blobReferenceToken: "first blobReferenceToken".to_string(), + }, + BlobReferenceTokenWrapper { + _id: None, + blobReferenceToken: "second blobReferenceToken".to_string(), + }, + ]) + }), + get_service_executor: Box::new(|_| todo!()), + get_group_id_for_mail_address: Box::new(|_, _| { + Ok(GeneratedId("generatedId".to_string())) + }), + } + } +} + +#[async_trait::async_trait] +impl LoggedInSdk for RealLoggedInSdk { + #[inline(always)] + async fn get_current_sym_group_key( + &self, + user_group_id: &GeneratedId, + ) -> Result { + self.get_current_sym_group_key(user_group_id).await + } + + async fn post_service_executor( + &self, + data: Service::Input, + params: ExtraServiceParams, + ) { + self.get_service_executor().post::(data, params) + } + + #[inline(always)] + async fn encrypt_and_upload( + &self, + archive_data_type: ArchiveDataType, + owner_group_id: &GeneratedId, + session_key: &GenericAesKey, + blob_data: Vec, + ) -> Result, ApiCallError> { + self.blob_facade() + .encrypt_and_upload(archive_data_type, owner_group_id, &session_key, blob_data) + .await + } + + async fn get_group_id_for_mail_address( + &self, + mail_address: &str, + ) -> Result { + self.mail_facade() + .get_group_id_for_mail_address(mail_address) + .await + } +} diff --git a/packages/node-mimimi/test/attachment_sample.eml b/packages/node-mimimi/test/attachment_sample.eml new file mode 100644 index 00000000000..f7d97535846 --- /dev/null +++ b/packages/node-mimimi/test/attachment_sample.eml @@ -0,0 +1,22 @@ +From: Art Vandelay (Vandelay Industries) +To: "Colleagues": "James Smythe" ; Friends: + jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?= ; +Date: Sat, 20 Nov 2021 14:22:01 -0800 +Subject: Why not both importing AND exporting? =?utf-8?b?4pi6?= +Content-Type: multipart/mixed; boundary="festivus"; + +--festivus +Content-Type: text/html; charset="us-ascii" +Content-Transfer-Encoding: base64 + +PGh0bWw+PHA+SSB3YXMgdGhpbmtpbmcgYWJvdXQgcXVpdHRpbmcgdGhlICZsZHF1bztle +HBvcnRpbmcmcmRxdW87IHRvIGZvY3VzIGp1c3Qgb24gdGhlICZsZHF1bztpbXBvcnRpbm +cmcmRxdW87LDwvcD48cD5idXQgdGhlbiBJIHRob3VnaHQsIHdoeSBub3QgZG8gYm90aD8 +gJiN4MjYzQTs8L3A+PC9odG1sPg== +--festivus +Content-Type: image/gif; name="TestFile.gif"; +Content-Transfer-Encoding: Base64 +Content-Disposition: attachment + +R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7 +--festivus-- \ No newline at end of file diff --git a/tuta-sdk/rust/sdk/src/rest_client.rs b/tuta-sdk/rust/sdk/src/rest_client.rs index a3558b0ada4..0f99f88584a 100644 --- a/tuta-sdk/rust/sdk/src/rest_client.rs +++ b/tuta-sdk/rust/sdk/src/rest_client.rs @@ -3,34 +3,34 @@ use thiserror::Error; #[derive(uniffi::Enum, Debug, PartialEq, Hash, Eq)] pub enum HttpMethod { - GET, - POST, - PUT, - DELETE, + GET, + POST, + PUT, + DELETE, } /// HTTP(S) data inserted by the `RestClient` in its REST requests #[derive(uniffi::Record, Debug, Eq, PartialEq)] pub struct RestClientOptions { - pub headers: HashMap, - pub body: Option>, + pub headers: HashMap, + pub body: Option>, } /// An error thrown by the `RestClient` (the injected HTTP client Kotlin/Swift/JavaScript) #[derive(Error, Debug, uniffi::Error, Eq, PartialEq, Clone)] pub enum RestClientError { - #[error("Network error")] - NetworkError, - #[error("Invalid URL")] - InvalidURL, - #[error("Failed handshake")] - FailedHandshake, - #[error("Invalid request")] - InvalidRequest, - #[error("Invalid response")] - InvalidResponse, - #[error("failed tls setup")] - FailedTlsSetup, + #[error("Network error")] + NetworkError, + #[error("Invalid URL")] + InvalidURL, + #[error("Failed handshake")] + FailedHandshake, + #[error("Invalid request")] + InvalidRequest, + #[error("Invalid response")] + InvalidResponse, + #[error("failed tls setup")] + FailedTlsSetup, } /// Provides a Rust SDK level interface for performing REST requests @@ -39,19 +39,19 @@ pub enum RestClientError { #[cfg_attr(test, mockall::automock)] #[async_trait::async_trait] pub trait RestClient: Send + Sync + 'static { - /// Performs an HTTP request with binary data in its body using the injected HTTP client - async fn request_binary( - &self, - url: String, - method: HttpMethod, - options: RestClientOptions, - ) -> Result; + /// Performs an HTTP request with binary data in its body using the injected HTTP client + async fn request_binary( + &self, + url: String, + method: HttpMethod, + options: RestClientOptions, + ) -> Result; } /// HTTP(S) data contained within a response from the backend #[derive(uniffi::Record, Clone)] pub struct RestResponse { - pub status: u32, - pub headers: HashMap, - pub body: Option>, + pub status: u32, + pub headers: HashMap, + pub body: Option>, } diff --git a/tuta-sdk/rust/sdk/src/services.rs b/tuta-sdk/rust/sdk/src/services.rs index efb772528ed..1d2678065d7 100644 --- a/tuta-sdk/rust/sdk/src/services.rs +++ b/tuta-sdk/rust/sdk/src/services.rs @@ -31,8 +31,8 @@ pub trait GetService: Service { #[async_trait::async_trait] pub trait PostService: Service { - type Input; - type Output; + type Input: Send; + type Output: Send; #[allow(non_snake_case)] async fn POST( From ff5d638f98838990d130de53bb3dbe5dfff717d4 Mon Sep 17 00:00:00 2001 From: map Date: Fri, 15 Nov 2024 14:34:45 +0100 Subject: [PATCH 31/32] Revert "WIP: add test for import without server running" --- packages/node-mimimi/src/importer.rs | 191 +++++++++++++++--- packages/node-mimimi/src/lib.rs | 1 - packages/node-mimimi/src/stub_loggedin_sdk.rs | 173 ---------------- .../node-mimimi/test/attachment_sample.eml | 22 -- tuta-sdk/rust/sdk/src/rest_client.rs | 56 ++--- tuta-sdk/rust/sdk/src/services.rs | 4 +- 6 files changed, 188 insertions(+), 259 deletions(-) delete mode 100644 packages/node-mimimi/src/stub_loggedin_sdk.rs delete mode 100644 packages/node-mimimi/test/attachment_sample.eml diff --git a/packages/node-mimimi/src/importer.rs b/packages/node-mimimi/src/importer.rs index 4007ac98737..d315d3a5552 100644 --- a/packages/node-mimimi/src/importer.rs +++ b/packages/node-mimimi/src/importer.rs @@ -3,7 +3,6 @@ use crate::importer::imap_reader::import_client::{ImapImport, ImapIterationError use crate::importer::imap_reader::ImapImportConfig; use crate::importer::importable_mail::{ImportableMail, ImportableMailAttachment}; use crate::reduce_to_chunks::reduce_to_chunks; -use crate::stub_loggedin_sdk::LoggedInSdk; use crate::tuta::credentials::TutaCredentials; use napi::bindgen_prelude::Error as NapiError; use std::sync::{Arc, Mutex}; @@ -20,7 +19,7 @@ use tutasdk::services::generated::tutanota::ImportMailService; use tutasdk::services::ExtraServiceParams; use tutasdk::tutanota_constants::ArchiveDataType; use tutasdk::GeneratedId; -use tutasdk::{IdTupleGenerated, Sdk}; +use tutasdk::{IdTupleGenerated, LoggedInSdk, Sdk}; pub type NapiTokioMutex = napi::tokio::sync::Mutex; @@ -64,7 +63,7 @@ pub struct ImportStatus { struct Importer { status: ImportStatus, - logged_in_sdk: Arc, + logged_in_sdk: Arc, target_owner_group: GeneratedId, target_mail_folder: IdTupleGenerated, import_source: Arc>, @@ -142,7 +141,7 @@ impl Importer { importable_mails: Iter, ) -> Result, ()> where - Iter: Iterator + Send + Sync + 'static, + Iter: Iterator + Send + 'static, { let new_mail_aes_256_key = GenericAesKey::from_bytes( self.randomizer_facade @@ -193,6 +192,7 @@ impl Importer { let reference_tokens = self .logged_in_sdk + .blob_facade() .encrypt_and_upload( ArchiveDataType::Attachments, &self.target_owner_group, @@ -202,6 +202,7 @@ impl Importer { .await .unwrap(); + // todo: do we need to upload the ivs and how? let enc_file_name = new_file_aes_256_key .encrypt_data( importable_mail_attachment.filename.as_ref(), @@ -263,7 +264,8 @@ impl Importer { let response = self .logged_in_sdk - .post_service_executor::(import_mail_post_in, service_params) + .get_service_executor() + .post::(import_mail_post_in, service_params) .await; match response { @@ -302,7 +304,7 @@ impl Importer { impl ImporterApi { pub fn new( - logged_in_sdk: Arc, + logged_in_sdk: Arc, target_owner_group: GeneratedId, target_mail_folder: IdTupleGenerated, import_source: Arc>, @@ -358,9 +360,7 @@ impl ImporterApi { )) } - async fn create_sdk( - tuta_credentials: TutaCredentials, - ) -> Result, String> { + async fn create_sdk(tuta_credentials: TutaCredentials) -> Result, String> { let rest_client = Arc::new( NativeRestClient::try_new() .map_err(|e| format!("Cannot build native rest client: {e}"))?, @@ -433,10 +433,13 @@ impl ImporterApi { #[cfg(test)] mod tests { use super::*; - use crate::stub_loggedin_sdk::StubLoggedInSdk; + use crate::importer::imap_reader::{ImapCredentials, LoginMechanism}; + use crate::tuta_imap::testing::GreenMailTestServer; use mail_builder::MessageBuilder; - - const TEST_EML_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/test"); + use tutasdk::entities::generated::tutanota::MailFolder; + use tutasdk::folder_system::MailSetKind; + use tutasdk::net::native_rest_client::NativeRestClient; + use tutasdk::Sdk; fn sample_email(subject: String) -> String { let email = MessageBuilder::new() @@ -449,30 +452,152 @@ mod tests { email } - #[tokio::test] - async fn can_import_mail_with_single_attachment() { - let mut loggedin_sdk = StubLoggedInSdk::default(); - - let mut importer = Importer { - status: Default::default(), - logged_in_sdk: Arc::new(loggedin_sdk), - target_owner_group: Default::default(), - target_mail_folder: IdTupleGenerated { - list_id: Default::default(), - element_id: Default::default(), + async fn get_test_import_folder_id( + logged_in_sdk: &Arc, + kind: MailSetKind, + ) -> MailFolder { + let mail_facade = logged_in_sdk.mail_facade(); + let mailbox = mail_facade.load_user_mailbox().await.unwrap(); + let folders = mail_facade + .load_folders_for_mailbox(&mailbox) + .await + .unwrap(); + folders + .system_folder_by_type(kind) + .expect("inbox should exist") + .clone() + } + + async fn init_imap_importer() -> (Importer, GreenMailTestServer) { + let importer_mail_address = "map-free@tutanota.de".to_string(); + let logged_in_sdk = Sdk::new( + "http://localhost:9000".to_string(), + Arc::new(NativeRestClient::try_new().unwrap()), + ) + .create_session(importer_mail_address.as_str(), "map") + .await + .unwrap(); + let greenmail = GreenMailTestServer::new(); + let imap_import_config = ImapImportConfig { + root_import_mail_folder_name: "/".to_string(), + credentials: ImapCredentials { + host: "127.0.0.1".to_string(), + port: greenmail.imaps_port.try_into().unwrap(), + login_mechanism: LoginMechanism::Plain { + username: "sug@example.org".to_string(), + password: "sug".to_string(), + }, }, - import_source: Arc::new(Mutex::new(ImportSource::LocalFile { - fs_email_client: FileImport::new(vec![ - TEST_EML_DIR.to_string() + "/attachment_sample.eml", - ]) - .unwrap(), - })), - randomizer_facade: RandomizerFacade::from_core(rand::rngs::OsRng), }; - let import_status = importer.continue_import().await.unwrap(); + let import_source = Arc::new(Mutex::new(ImportSource::RemoteImap { + imap_import_client: ImapImport::new(imap_import_config), + })); + let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng); + let target_mail_folder = get_test_import_folder_id(&logged_in_sdk, MailSetKind::Archive) + .await + ._id + .unwrap(); - assert_eq!(import_status.imported_mails, 1); - assert_eq!(import_status.state, ImportState::Finished); + let target_owner_group = logged_in_sdk + .mail_facade() + .get_group_id_for_mail_address(importer_mail_address.as_str()) + .await + .unwrap(); + + let importer = Importer { + target_owner_group, + target_mail_folder, + logged_in_sdk, + import_source, + randomizer_facade, + status: ImportStatus::default(), + }; + + (importer, greenmail) + } + + pub async fn init_file_importer(source_paths: Vec) -> Importer { + let logged_in_sdk = Sdk::new( + "http://localhost:9000".to_string(), + Arc::new(NativeRestClient::try_new().unwrap()), + ) + .create_session("map-free@tutanota.de", "map") + .await + .unwrap(); + + let import_source = Arc::new(Mutex::new(ImportSource::LocalFile { + fs_email_client: FileImport::new(source_paths).unwrap(), + })); + let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng); + let target_mail_folder = get_test_import_folder_id(&logged_in_sdk, MailSetKind::Archive) + .await + ._id + .unwrap(); + + let target_owner_group = logged_in_sdk + .mail_facade() + .get_group_id_for_mail_address("map-free@tutanota.de") + .await + .unwrap(); + + Importer { + status: ImportStatus::default(), + target_owner_group, + target_mail_folder, + logged_in_sdk, + import_source, + randomizer_facade, + } + } + + #[tokio::test] + pub async fn import_multiple_from_imap_default_folder() { + let (mut importer, greenmail) = init_imap_importer().await; + + let email_first = sample_email("Hello from imap πŸ˜€! -- Бписок.doc".to_string()); + let email_second = sample_email("Second time: hello".to_string()); + greenmail.store_mail("sug@example.org", email_first.as_str()); + greenmail.store_mail("sug@example.org", email_second.as_str()); + + let import_res = importer.continue_import().await.map_err(|_| ()); + assert_eq!( + Ok(ImportStatus { + state: ImportState::Finished, + imported_mails: 2, + }), + import_res + ); + } + + #[tokio::test] + pub async fn import_single_from_imap_default_folder() { + let (mut importer, greenmail) = init_imap_importer().await; + + let email = sample_email("Single email".to_string()); + greenmail.store_mail("sug@example.org", email.as_str()); + + let import_res = importer.continue_import().await.map_err(|_| ()); + assert_eq!( + Ok(ImportStatus { + state: ImportState::Finished, + imported_mails: 1, + }), + import_res + ); + } + + #[tokio::test] + async fn can_import_single_eml_file() { + let mut importer = init_file_importer(vec!["./test/sample.eml".to_string()]).await; + + let import_res = importer.continue_import().await.map_err(|_| ()); + assert_eq!( + Ok(ImportStatus { + state: ImportState::Finished, + imported_mails: 1, + }), + import_res + ); } } diff --git a/packages/node-mimimi/src/lib.rs b/packages/node-mimimi/src/lib.rs index f49c6f2e388..e1e09b69552 100644 --- a/packages/node-mimimi/src/lib.rs +++ b/packages/node-mimimi/src/lib.rs @@ -6,4 +6,3 @@ pub mod logging; mod reduce_to_chunks; pub mod tuta; mod tuta_imap; -mod stub_loggedin_sdk; diff --git a/packages/node-mimimi/src/stub_loggedin_sdk.rs b/packages/node-mimimi/src/stub_loggedin_sdk.rs deleted file mode 100644 index b6ffb79fc42..00000000000 --- a/packages/node-mimimi/src/stub_loggedin_sdk.rs +++ /dev/null @@ -1,173 +0,0 @@ -use std::sync::Arc; -use tutasdk::crypto::key::{GenericAesKey, VersionedAesKey}; -use tutasdk::entities::generated::sys::BlobReferenceTokenWrapper; -use tutasdk::services::service_executor::ResolvingServiceExecutor; -use tutasdk::services::{ExtraServiceParams, PostService}; -use tutasdk::tutanota_constants::ArchiveDataType; -use tutasdk::LoggedInSdk as RealLoggedInSdk; -use tutasdk::{ApiCallError, GeneratedId}; - -#[async_trait::async_trait] -// todo: think about restructuring to expose real LoggedInSdk hierarchy -pub trait LoggedInSdk: Sync + Send + Sized + 'static { - async fn get_current_sym_group_key( - &self, - user_group_id: &GeneratedId, - ) -> Result; - - async fn encrypt_and_upload( - &self, - archive_data_type: ArchiveDataType, - owner_group_id: &GeneratedId, - session_key: &GenericAesKey, - blob_data: Vec, - ) -> Result, ApiCallError>; - - async fn post_service_executor( - &self, - data: Service::Input, - params: ExtraServiceParams, - ) -> Result; - - async fn get_group_id_for_mail_address( - &self, - mail_address: &str, - ) -> Result; -} - -pub struct StubLoggedInSdk { - pub get_current_sym_group_key: Box< - dyn Send - + Sync - + Fn(&StubLoggedInSdk, &GeneratedId) -> Result, - >, - - pub encrypt_and_upload: Box< - dyn Send - + Sync - + Fn( - &StubLoggedInSdk, - ArchiveDataType, - &GeneratedId, - &GenericAesKey, - Vec, - ) -> Result, ApiCallError>, - >, - - pub get_service_executor: - Box &Arc>, - - pub get_group_id_for_mail_address: - Box Result>, -} - -#[async_trait::async_trait] -impl LoggedInSdk for StubLoggedInSdk { - async fn get_current_sym_group_key( - &self, - user_group_id: &GeneratedId, - ) -> Result { - (*self.get_current_sym_group_key)(self, user_group_id) - } - - async fn post_service_executor( - &self, - data: Service::Input, - params: ExtraServiceParams, - ) -> Result { - Ok(todo!()) - } - - async fn encrypt_and_upload( - &self, - archive_data_type: ArchiveDataType, - owner_group_id: &GeneratedId, - session_key: &GenericAesKey, - blob_data: Vec, - ) -> Result, ApiCallError> { - (*self.encrypt_and_upload)( - self, - archive_data_type, - owner_group_id, - session_key, - blob_data, - ) - } - - async fn get_group_id_for_mail_address( - &self, - mail_address: &str, - ) -> Result { - (*self.get_group_id_for_mail_address)(self, mail_address) - } -} - -impl Default for StubLoggedInSdk { - fn default() -> Self { - StubLoggedInSdk { - get_current_sym_group_key: Box::new(|_, _| { - Ok(VersionedAesKey { - object: GenericAesKey::from_bytes(&[0; 32]).unwrap(), - version: 0, - }) - }), - encrypt_and_upload: Box::new(|_, _, _, _, _| { - Ok(vec![ - BlobReferenceTokenWrapper { - _id: None, - blobReferenceToken: "first blobReferenceToken".to_string(), - }, - BlobReferenceTokenWrapper { - _id: None, - blobReferenceToken: "second blobReferenceToken".to_string(), - }, - ]) - }), - get_service_executor: Box::new(|_| todo!()), - get_group_id_for_mail_address: Box::new(|_, _| { - Ok(GeneratedId("generatedId".to_string())) - }), - } - } -} - -#[async_trait::async_trait] -impl LoggedInSdk for RealLoggedInSdk { - #[inline(always)] - async fn get_current_sym_group_key( - &self, - user_group_id: &GeneratedId, - ) -> Result { - self.get_current_sym_group_key(user_group_id).await - } - - async fn post_service_executor( - &self, - data: Service::Input, - params: ExtraServiceParams, - ) { - self.get_service_executor().post::(data, params) - } - - #[inline(always)] - async fn encrypt_and_upload( - &self, - archive_data_type: ArchiveDataType, - owner_group_id: &GeneratedId, - session_key: &GenericAesKey, - blob_data: Vec, - ) -> Result, ApiCallError> { - self.blob_facade() - .encrypt_and_upload(archive_data_type, owner_group_id, &session_key, blob_data) - .await - } - - async fn get_group_id_for_mail_address( - &self, - mail_address: &str, - ) -> Result { - self.mail_facade() - .get_group_id_for_mail_address(mail_address) - .await - } -} diff --git a/packages/node-mimimi/test/attachment_sample.eml b/packages/node-mimimi/test/attachment_sample.eml deleted file mode 100644 index f7d97535846..00000000000 --- a/packages/node-mimimi/test/attachment_sample.eml +++ /dev/null @@ -1,22 +0,0 @@ -From: Art Vandelay (Vandelay Industries) -To: "Colleagues": "James Smythe" ; Friends: - jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?= ; -Date: Sat, 20 Nov 2021 14:22:01 -0800 -Subject: Why not both importing AND exporting? =?utf-8?b?4pi6?= -Content-Type: multipart/mixed; boundary="festivus"; - ---festivus -Content-Type: text/html; charset="us-ascii" -Content-Transfer-Encoding: base64 - -PGh0bWw+PHA+SSB3YXMgdGhpbmtpbmcgYWJvdXQgcXVpdHRpbmcgdGhlICZsZHF1bztle -HBvcnRpbmcmcmRxdW87IHRvIGZvY3VzIGp1c3Qgb24gdGhlICZsZHF1bztpbXBvcnRpbm -cmcmRxdW87LDwvcD48cD5idXQgdGhlbiBJIHRob3VnaHQsIHdoeSBub3QgZG8gYm90aD8 -gJiN4MjYzQTs8L3A+PC9odG1sPg== ---festivus -Content-Type: image/gif; name="TestFile.gif"; -Content-Transfer-Encoding: Base64 -Content-Disposition: attachment - -R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7 ---festivus-- \ No newline at end of file diff --git a/tuta-sdk/rust/sdk/src/rest_client.rs b/tuta-sdk/rust/sdk/src/rest_client.rs index 0f99f88584a..a3558b0ada4 100644 --- a/tuta-sdk/rust/sdk/src/rest_client.rs +++ b/tuta-sdk/rust/sdk/src/rest_client.rs @@ -3,34 +3,34 @@ use thiserror::Error; #[derive(uniffi::Enum, Debug, PartialEq, Hash, Eq)] pub enum HttpMethod { - GET, - POST, - PUT, - DELETE, + GET, + POST, + PUT, + DELETE, } /// HTTP(S) data inserted by the `RestClient` in its REST requests #[derive(uniffi::Record, Debug, Eq, PartialEq)] pub struct RestClientOptions { - pub headers: HashMap, - pub body: Option>, + pub headers: HashMap, + pub body: Option>, } /// An error thrown by the `RestClient` (the injected HTTP client Kotlin/Swift/JavaScript) #[derive(Error, Debug, uniffi::Error, Eq, PartialEq, Clone)] pub enum RestClientError { - #[error("Network error")] - NetworkError, - #[error("Invalid URL")] - InvalidURL, - #[error("Failed handshake")] - FailedHandshake, - #[error("Invalid request")] - InvalidRequest, - #[error("Invalid response")] - InvalidResponse, - #[error("failed tls setup")] - FailedTlsSetup, + #[error("Network error")] + NetworkError, + #[error("Invalid URL")] + InvalidURL, + #[error("Failed handshake")] + FailedHandshake, + #[error("Invalid request")] + InvalidRequest, + #[error("Invalid response")] + InvalidResponse, + #[error("failed tls setup")] + FailedTlsSetup, } /// Provides a Rust SDK level interface for performing REST requests @@ -39,19 +39,19 @@ pub enum RestClientError { #[cfg_attr(test, mockall::automock)] #[async_trait::async_trait] pub trait RestClient: Send + Sync + 'static { - /// Performs an HTTP request with binary data in its body using the injected HTTP client - async fn request_binary( - &self, - url: String, - method: HttpMethod, - options: RestClientOptions, - ) -> Result; + /// Performs an HTTP request with binary data in its body using the injected HTTP client + async fn request_binary( + &self, + url: String, + method: HttpMethod, + options: RestClientOptions, + ) -> Result; } /// HTTP(S) data contained within a response from the backend #[derive(uniffi::Record, Clone)] pub struct RestResponse { - pub status: u32, - pub headers: HashMap, - pub body: Option>, + pub status: u32, + pub headers: HashMap, + pub body: Option>, } diff --git a/tuta-sdk/rust/sdk/src/services.rs b/tuta-sdk/rust/sdk/src/services.rs index 1d2678065d7..efb772528ed 100644 --- a/tuta-sdk/rust/sdk/src/services.rs +++ b/tuta-sdk/rust/sdk/src/services.rs @@ -31,8 +31,8 @@ pub trait GetService: Service { #[async_trait::async_trait] pub trait PostService: Service { - type Input: Send; - type Output: Send; + type Input; + type Output; #[allow(non_snake_case)] async fn POST( From 248a066f0c48888b76450f465bd33dfe1cba39bc Mon Sep 17 00:00:00 2001 From: map Date: Fri, 15 Nov 2024 18:54:45 +0100 Subject: [PATCH 32/32] wip --- packages/node-mimimi/src/importer.rs | 13 +- .../src/importer/importable_mail.rs | 120 ++++++++---------- .../msg_file_compatibility_test.rs | 77 +++++++---- .../bluedot-postcard-expected.json | 2 +- .../mimetools-testmsgs/bluedot-postcard.msg | 2 +- .../bluedot-simple-expected.json | 2 +- .../mimetools-testmsgs/bluedot-simple.msg | 2 +- .../mimetools-testmsgs/jt-0498-expected.json | 2 +- tuta-sdk/rust/sdk/examples/uploadBlob.rs | 2 +- tuta-sdk/rust/sdk/src/blobs/blob_facade.rs | 4 +- 10 files changed, 118 insertions(+), 108 deletions(-) diff --git a/packages/node-mimimi/src/importer.rs b/packages/node-mimimi/src/importer.rs index d315d3a5552..6cee004c270 100644 --- a/packages/node-mimimi/src/importer.rs +++ b/packages/node-mimimi/src/importer.rs @@ -1,7 +1,7 @@ use crate::importer::file_reader::import_client::{FileImport, FileIterationError}; use crate::importer::imap_reader::import_client::{ImapImport, ImapIterationError}; use crate::importer::imap_reader::ImapImportConfig; -use crate::importer::importable_mail::{ImportableMail, ImportableMailAttachment}; +use crate::importer::importable_mail::ImportableMail; use crate::reduce_to_chunks::reduce_to_chunks; use crate::tuta::credentials::TutaCredentials; use napi::bindgen_prelude::Error as NapiError; @@ -158,8 +158,11 @@ impl Importer { .encrypt_key(&new_mail_aes_256_key, Iv::generate(&self.randomizer_facade)); const MAX_REQUEST_SIZE: usize = 1024 * 1024 * 5; - let import_mail_data_and_attachments = - importable_mails.map(<(ImportMailData, Vec)>::from); + let import_mail_data_and_attachments = importable_mails.map(|mut m| { + let mut attachments = Vec::with_capacity(m.attachments.len()); + attachments.append(&mut m.attachments); + (ImportMailData::from(m), attachments) + }); let import_chunks = reduce_to_chunks( import_mail_data_and_attachments, MAX_REQUEST_SIZE, @@ -179,8 +182,6 @@ impl Importer { let mut import_mail_data = import_mail_data; let mut import_attachments = Vec::new(); for importable_mail_attachment in importable_mail_attachments { - let importable_mail_attachment = - importable_mail_attachment as ImportableMailAttachment; let new_file_aes_256_key = GenericAesKey::from_bytes( self.randomizer_facade .generate_random_array::<{ tutasdk::crypto::aes::AES_256_KEY_SIZE }>() @@ -197,7 +198,7 @@ impl Importer { ArchiveDataType::Attachments, &self.target_owner_group, &new_file_aes_256_key, - importable_mail_attachment.content, + &importable_mail_attachment.content, ) .await .unwrap(); diff --git a/packages/node-mimimi/src/importer/importable_mail.rs b/packages/node-mimimi/src/importer/importable_mail.rs index 4e42f28361e..c22bea7e49c 100644 --- a/packages/node-mimimi/src/importer/importable_mail.rs +++ b/packages/node-mimimi/src/importer/importable_mail.rs @@ -1,8 +1,9 @@ +// use crate::importer::importable_mail::extend_mail_parser::NonRevHeaderValue; use crate::tuta_imap::client::types::ImapMail; use extend_mail_parser::MakeString; use mail_builder::headers::Header; use mail_parser::{ - Address, ContentType, GetHeader, HeaderValue, Message, MessageParser, MessagePart, + Address, ContentType, GetHeader, HeaderName, HeaderValue, Message, MessageParser, MessagePart, MessagePartId, MimeHeaders, PartType, }; use regex::Regex; @@ -12,6 +13,7 @@ use tutasdk::date::DateTime; use tutasdk::entities::generated::tutanota::{ EncryptedMailAddress, ImportMailData, ImportMailDataMailReference, MailAddress, Recipients, }; + pub mod extend_mail_parser; mod plain_text_to_html_converter; @@ -59,7 +61,6 @@ pub(super) struct ImportableMailAttachment { pub content_id: Option, pub content_type: String, pub content: Vec, - is_inline: bool, } #[cfg_attr(test, derive(PartialEq, Debug))] @@ -185,12 +186,8 @@ impl ImportableMail { continue; } match &part.body { - PartType::Binary(binary_content) => { - Self::handle_binary(part, &mut attachments, binary_content.to_vec(), false); - }, - - PartType::InlineBinary(binary_content) => { - Self::handle_binary(part, &mut attachments, binary_content.to_vec(), true); + PartType::Binary(binary_content) | PartType::InlineBinary(binary_content) => { + Self::handle_binary(part, &mut attachments, binary_content.to_vec()); }, PartType::Text(text) => { @@ -198,12 +195,7 @@ impl ImportableMail { { Self::handle_plain_text(&mut email_body_as_html, text.as_ref()); } else { - Self::handle_binary( - part, - &mut attachments, - text.as_bytes().to_vec(), - false, - ); + Self::handle_binary(part, &mut attachments, text.as_bytes().to_vec()); } }, @@ -211,17 +203,13 @@ impl ImportableMail { if !Self::is_attachment(&email_body_as_html, part) { Self::handle_html_text(&mut email_body_as_html, html_text.as_ref()) } else { - Self::handle_binary( - part, - &mut attachments, - html_text.as_bytes().to_vec(), - false, - ); + Self::handle_binary(part, &mut attachments, html_text.as_bytes().to_vec()); } }, PartType::Message(attached_message) => { - let ignored_result = Self::handle_message(&mut attachments, attached_message); + let ignored_result = + Self::handle_message(&mut attachments, part, attached_message); }, PartType::Multipart(multi_part_ids) => { @@ -293,8 +281,8 @@ impl ImportableMail { /// Creates a filename from the given filename that is valid on Linux and Windows. Invalid /// characters are replaced by "_" fn escape_filename(file_name: &str) -> Cow { - let regex = Regex::new("[\\\\/:*?<>\"|]").unwrap(); - regex.replace(file_name, "_") + let regex = Regex::new("[\\/:*?<>\"|]").unwrap(); + regex.replace_all(file_name, "_") } fn get_suffix_from_content_type(content_type: &ContentType) -> &'static str { @@ -409,7 +397,6 @@ impl ImportableMail { part: &MessagePart, attachments: &mut Vec, content: Vec, - is_inline: bool, ) { let content_id = part.content_id().map(ToString::to_string); let filename = Self::get_filename(part, "unknown"); @@ -425,7 +412,6 @@ impl ImportableMail { filename, content_type, content_id, - is_inline, content, }; @@ -434,25 +420,24 @@ impl ImportableMail { fn handle_message( attachments: &mut Vec, + parent_part: &MessagePart, message: &Message, ) -> Result<(), MailParseError> { - let filename = - Self::get_filename(&message.parts[0], &message.subject().unwrap_or("unknown")); - let content_type = message + let filename = Self::get_filename(parent_part, &message.subject().unwrap_or("unknown")); + + let nested_part = &message.parts[0]; + let content = + message.raw_message[nested_part.offset_header..nested_part.offset_end].to_vec(); + let content_type = parent_part .content_type() .ok_or_else(|| Self::default_content_type()) .map(MakeString::make_string) .unwrap_or_default() .to_string(); - - let nested_part = &message.parts[0]; - let content = - message.raw_message[nested_part.offset_header..nested_part.offset_end].to_vec(); let attachment = ImportableMailAttachment { filename, content_type, content, - is_inline: false, content_id: None, }; attachments.push(attachment); @@ -469,7 +454,7 @@ impl ImportableMail { } } -impl From for (ImportMailData, Vec) { +impl From for (ImportMailData) { fn from(importable_mail: ImportableMail) -> Self { let ImportableMail { headers_string: headers, @@ -516,50 +501,51 @@ impl From for (ImportMailData, Vec) { }) .collect(); - ( - ImportMailData { + ImportMailData { + _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), + _finalIvs: HashMap::new(), + compressedHeaders: headers, + subject, + compressedBodyText: html_body_text, + differentEnvelopeSender: different_envelope_sender, + sender: from_addresses + .first() + .cloned() + .unwrap_or(MailContact::default().into()), + recipients: Recipients { _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), - _finalIvs: HashMap::new(), - compressedHeaders: headers, - subject, - compressedBodyText: html_body_text, - differentEnvelopeSender: different_envelope_sender, - sender: from_addresses - .first() - .cloned() - .unwrap_or(MailContact::default().into()), - recipients: Recipients { - _id: Some(tutasdk::CustomId::from_custom_string(FIXED_CUSTOM_ID)), - bccRecipients: bcc_addresses, - ccRecipients: cc_addresses, - toRecipients: to_addresses, - }, - replyTos: reply_tos, - unread, - confidential: false, - method: ical_type as i64, - phishingStatus: if is_phishing { 1 } else { 0 }, - replyType: reply_type as i64, - // if no date is provided, use UNIX_EPOCH (01.01.1970) as fallback - date: date.unwrap_or_default(), - state: mail_state as i64, - messageId: message_id, - inReplyTo: in_reply_to, - references, - importedAttachments: vec![], + bccRecipients: bcc_addresses, + ccRecipients: cc_addresses, + toRecipients: to_addresses, }, - attachments, - ) + replyTos: reply_tos, + unread, + confidential: false, + method: ical_type as i64, + phishingStatus: if is_phishing { 1 } else { 0 }, + replyType: reply_type as i64, + // if no date is provided, use UNIX_EPOCH (01.01.1970) as fallback + date: date.unwrap_or_default(), + state: mail_state as i64, + messageId: message_id, + inReplyTo: in_reply_to, + references, + importedAttachments: vec![], + } } } +fn get_parser() -> MessageParser { + MessageParser::new().header_text(HeaderName::Date) +} + impl TryFrom for ImportableMail { type Error = MailParseError; fn try_from(imap_mail: ImapMail) -> Result { let ImapMail { rfc822_full } = imap_mail; // parse the full mime message - let imap_mail = MessageParser::new() + let imap_mail = get_parser() .parse(rfc822_full.as_slice()) .ok_or(MailParseError::InvalidMimeMessage)?; diff --git a/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs b/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs index a2ef61c7b75..8c66ca8bdbf 100644 --- a/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs +++ b/packages/node-mimimi/src/importer/importable_mail/msg_file_compatibility_test.rs @@ -7,7 +7,6 @@ use serde::Deserialize; use std::collections::HashSet; use std::io::Read; use tutasdk::date::DateTime; -use tutasdk::entities::generated::tutanota::ImportMailData; #[test] fn mime_tools_test_messages() { @@ -67,34 +66,38 @@ fn mime_tools_test_messages() { continue; } - let parsed_message = parsed_message_result.unwrap(); - let (mut importable_mail, importable_mail_attachments): ( - ImportMailData, - Vec, - ) = parsed_message.into(); - let (mut expected_importable_mail, expected_mail_attachments): ( - ImportMailData, - Vec, - ) = expected_result.unwrap().into(); - - // importable_mail.attachments.clear(); - // expected_importable_mail.attachments.clear(); + let mut parsed_message = parsed_message_result.unwrap(); + let mut expected_importable_mail: ImportableMail = expected_result.unwrap().into(); // we import raw headers and there is no need to compare them - importable_mail.compressedHeaders.clear(); - expected_importable_mail.compressedHeaders.clear(); + parsed_message.headers_string.clear(); + expected_importable_mail.headers_string.clear(); // we don't cover date headers in server as well. // .msg and -expected.json do not share same date seems like - importable_mail.date = DateTime::default(); - expected_importable_mail.date = DateTime::default(); + parsed_message.date = None; + expected_importable_mail.date = None; // todo: // we don't have different envelope sender in -expected.json - importable_mail.differentEnvelopeSender = None; + parsed_message.different_envelope_sender = None; + + for i in 0..std::cmp::max( + parsed_message.attachments.len(), + expected_importable_mail.attachments.len(), + ) { + let a = &mut parsed_message.attachments[i]; + let b = &mut expected_importable_mail.attachments[i]; + + assert!(a.content_type.starts_with(b.content_type.as_str())); + a.content_type.clear(); + b.content_type.clear(); + } + // since headers might have more attribute in actual message + // and in expected message we only have mime-type;charset + // we can make sure the first part ( i.e mime-type;charset ) is same - assert_eq!(importable_mail, expected_importable_mail); - assert_eq!(importable_mail_attachments, expected_mail_attachments); + assert_eq!(parsed_message, expected_importable_mail); } } @@ -109,7 +112,7 @@ impl From for MailContact { } } -impl From for (ImportMailData, Vec) { +impl From for ImportableMail { fn from(expected_message: ExpectedMessage) -> Self { ImportableMail { headers_string: expected_message.mail_headers, @@ -125,17 +128,39 @@ impl From for (ImportMailData, Vec) { .into_iter() .map(|f| ImportableMailAttachment { filename: f.name, - content_id: Some(f.content_id), - content_type: "".to_string(), + content_id: if f.content_id.is_empty() { + None + } else { + Some(f.content_id) + }, + content_type: { + let mut content_type = String::new(); + + if !f.mime_type.is_empty() { + content_type.push_str(f.mime_type.as_str()); + } + if let Some(charset) = f.charset { + content_type.push_str(";"); + content_type.push_str(&format!("charset=\"{charset}\"")); + } + + content_type + }, content: base64_decode(f.data.as_bytes()).unwrap(), - is_inline: false, }) .collect(), date: expected_message .sent_date .map(|timestamp| DateTime::from_millis(timestamp as u64)), different_envelope_sender: None, - from_addresses: vec![expected_message.sender.into()], + from_addresses: { + let sender = expected_message.sender; + if sender.name.is_empty() && sender.mail_address.is_empty() { + vec![] + } else { + vec![sender.into()] + } + }, to_addresses: expected_message .to_recipients .into_iter() @@ -165,8 +190,6 @@ impl From for (ImportMailData, Vec) { in_reply_to: expected_message.in_reply_to, references: expected_message.references, } - .try_into() - .unwrap() } } diff --git a/packages/node-mimimi/test/mimetools-testmsgs/bluedot-postcard-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/bluedot-postcard-expected.json index 33fd6b16fd3..aeff9dcb7c4 100644 --- a/packages/node-mimimi/test/mimetools-testmsgs/bluedot-postcard-expected.json +++ b/packages/node-mimimi/test/mimetools-testmsgs/bluedot-postcard-expected.json @@ -27,7 +27,7 @@ "data": "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KOGCJAkccahVRQMAADgADjFeFjM2veFD7/8AL/M+qy7h+zVXFf8AgP8An/l/wxDp2nWmk6fDY2MCw20IwiAk9Tkkk8kkkkk5JJJJJNWqKK8I+r2CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigDlNY+H2iarcPdQefpd1I5eWawKp5pJJJZGVkLEnJfbvOAN2OK5O88AeJ7PJt5dN1ONV3kqXtZCf7iod6k8cEyKCTg4AyfV6K6aOLr0dIS0OLEZdhcRrVgm++z+9HiU2j+IrWJprrwzqccK/edDDORngfJFI7nn0U46nABNVP9L/6A2uf+Ce6/wDjde8UV2RzjELdJ/L/AIJ5suHMG3o5L5r9UeEJHfzSLHFomttI5CoraZPGCT0BZ0Cr9WIA7kCtGHwx4ruZViXw7JbFv+Wt3dwLEvf5jG7t7DCnkjOBkj2ailLN8Q9rL5f5lQ4dwcd7v1f+SR5xYfDK7lkR9Z1lTCQGe2sITGc8ZQzMxJXGRlVRjwQV6V2ukeH9I0GN00rTra0MgUSvHGA8u3ODI/3nPJ5Yk5JOeTWlRXBVr1KrvUdz1aGFo4dWpRSCiiisjoCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//Z", "mimeType": "image/jpeg", "charset": null, - "contentId": "", + "contentId": "my-graphic", "calendarMethod": null } ], diff --git a/packages/node-mimimi/test/mimetools-testmsgs/bluedot-postcard.msg b/packages/node-mimimi/test/mimetools-testmsgs/bluedot-postcard.msg index 89c2516d13b..68af357e231 100644 --- a/packages/node-mimimi/test/mimetools-testmsgs/bluedot-postcard.msg +++ b/packages/node-mimimi/test/mimetools-testmsgs/bluedot-postcard.msg @@ -35,7 +35,7 @@ Content-Transfer-Encoding: binary Content-Type: image/jpeg; name="bluedot.jpg" Content-Disposition: inline; filename="bluedot.jpg" Content-Transfer-Encoding: base64 -Content-Id: my-graphic +Content-Id: /9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsL DBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/ diff --git a/packages/node-mimimi/test/mimetools-testmsgs/bluedot-simple-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/bluedot-simple-expected.json index 97534fe9dfa..65f7c452220 100644 --- a/packages/node-mimimi/test/mimetools-testmsgs/bluedot-simple-expected.json +++ b/packages/node-mimimi/test/mimetools-testmsgs/bluedot-simple-expected.json @@ -27,7 +27,7 @@ "data": "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KOGCJAkccahVRQMAADgADjFeFjM2veFD7/8AL/M+qy7h+zVXFf8AgP8An/l/wxDp2nWmk6fDY2MCw20IwiAk9Tkkk8kkkkk5JJJJJNWqKK8I+r2CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigDlNY+H2iarcPdQefpd1I5eWawKp5pJJJZGVkLEnJfbvOAN2OK5O88AeJ7PJt5dN1ONV3kqXtZCf7iod6k8cEyKCTg4AyfV6K6aOLr0dIS0OLEZdhcRrVgm++z+9HiU2j+IrWJprrwzqccK/edDDORngfJFI7nn0U46nABNVP9L/6A2uf+Ce6/wDjde8UV2RzjELdJ/L/AIJ5suHMG3o5L5r9UeEJHfzSLHFomttI5CoraZPGCT0BZ0Cr9WIA7kCtGHwx4ruZViXw7JbFv+Wt3dwLEvf5jG7t7DCnkjOBkj2ailLN8Q9rL5f5lQ4dwcd7v1f+SR5xYfDK7lkR9Z1lTCQGe2sITGc8ZQzMxJXGRlVRjwQV6V2ukeH9I0GN00rTra0MgUSvHGA8u3ODI/3nPJ5Yk5JOeTWlRXBVr1KrvUdz1aGFo4dWpRSCiiisjoCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//Z", "mimeType": "image/jpeg", "charset": null, - "contentId": "", + "contentId": "my-graphic", "calendarMethod": null } ], diff --git a/packages/node-mimimi/test/mimetools-testmsgs/bluedot-simple.msg b/packages/node-mimimi/test/mimetools-testmsgs/bluedot-simple.msg index 8d9e4a7a390..03b4a08269b 100644 --- a/packages/node-mimimi/test/mimetools-testmsgs/bluedot-simple.msg +++ b/packages/node-mimimi/test/mimetools-testmsgs/bluedot-simple.msg @@ -1,7 +1,7 @@ Content-Type: image/jpeg; name="bluedot.jpg" Content-Disposition: inline; filename="bluedot.jpg" Content-Transfer-Encoding: base64 -Content-Id: my-graphic +Content-Id: /9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsL DBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/ diff --git a/packages/node-mimimi/test/mimetools-testmsgs/jt-0498-expected.json b/packages/node-mimimi/test/mimetools-testmsgs/jt-0498-expected.json index 8d996452459..42dadb193f5 100644 --- a/packages/node-mimimi/test/mimetools-testmsgs/jt-0498-expected.json +++ b/packages/node-mimimi/test/mimetools-testmsgs/jt-0498-expected.json @@ -22,7 +22,7 @@ "inReplyTo": null, "references": [], "autoSubmitted": null, - "sentDate": 893950130000, + "sentDate": 893935730000, "subject": "Fwd: PC800: Tall Hondaline Windshield Distortion", "plainBodyText": "Hello to all, \n\nI purchased a tall windshield about two weeks ago from Waynesville Cycle\nCenter in NC. The entire length and width of the windshield has optical waves\nor ripples which distort the view through it. It was very uncomfortable to\nride with and I would be afraid to ride at night with it.\n\nI contacted the manager at WCC. He in turn contacted Honda Customer Service\n(310-532-9811). They replied to him that there were no bulletins concerning\nthis problem and that they inspected several windshields. They claim that all\nthe windshields had distortions. They have offered to give me a full refund.\n\nWhat bothers me is two things. First, the stock windshield has no optical\ndistortion. Second, it appears that Honda knows that it is selling a less\nthan perfect product and is apparently unconcerned about it (seems like a\nstrange way to do business). \n\nPerhaps my windshield is the worst one ever made, but they made no offer to\ninspect mine and compare to others that they have in stock.\n\nI am going to call Honda on Monday and raise the issues of safety and quality.\nI will ask them if they have a problem with me forwarding their position to\npublications such as Cycle World, Rider, etc.\n\nDennis Gaffney\nMarlboro, NY\ngaffneydp@aol.com\n1994 PC800\nBought used in 1997 (2000 miles)\nModifications: tall windshield?\n", "htmlBodyText": null, diff --git a/tuta-sdk/rust/sdk/examples/uploadBlob.rs b/tuta-sdk/rust/sdk/examples/uploadBlob.rs index d182f868384..f27a044623e 100644 --- a/tuta-sdk/rust/sdk/examples/uploadBlob.rs +++ b/tuta-sdk/rust/sdk/examples/uploadBlob.rs @@ -36,7 +36,7 @@ async fn main() -> Result<(), Box> { ArchiveDataType::Attachments, &owner_group_id, &new_aes_256_key, - vec![0; 1024], + &vec![0; 1024], ) .await?; for tw in result { diff --git a/tuta-sdk/rust/sdk/src/blobs/blob_facade.rs b/tuta-sdk/rust/sdk/src/blobs/blob_facade.rs index c110830435b..88441c21d16 100644 --- a/tuta-sdk/rust/sdk/src/blobs/blob_facade.rs +++ b/tuta-sdk/rust/sdk/src/blobs/blob_facade.rs @@ -58,7 +58,7 @@ impl BlobFacade { archive_data_type: ArchiveDataType, owner_group_id: &GeneratedId, session_key: &GenericAesKey, - blob_data: Vec, + blob_data: &[u8], ) -> Result, ApiCallError> { let chunks = blob_data.chunks(MAX_BLOB_SIZE_BYTES); let mut blob_reference_token_wrappers: Vec = @@ -360,7 +360,7 @@ mod tests { ArchiveDataType::Attachments, &owner_group_id, &session_key, - blob_data.clone(), + &blob_data, ) .await .unwrap();