diff --git a/crates/imap/src/core/client.rs b/crates/imap/src/core/client.rs index 9fbeb4ef..e7fec781 100644 --- a/crates/imap/src/core/client.rs +++ b/crates/imap/src/core/client.rs @@ -36,9 +36,9 @@ use super::{SelectedMailbox, Session, SessionData, State, IMAP}; impl Session { pub async fn ingest(&mut self, bytes: &[u8]) -> crate::Result { - /*for line in String::from_utf8_lossy(bytes).split("\r\n") { + for line in String::from_utf8_lossy(bytes).split("\r\n") { let c = println!("{}", line); - }*/ + } tracing::trace!(parent: &self.span, event = "read", diff --git a/crates/imap/src/core/writer.rs b/crates/imap/src/core/writer.rs index 68ac1404..e61df02d 100644 --- a/crates/imap/src/core/writer.rs +++ b/crates/imap/src/core/writer.rs @@ -58,7 +58,7 @@ pub fn spawn_writer(mut stream: Event, span: tracing::Span) -> mpsc::Sender { @@ -101,7 +101,7 @@ pub fn spawn_writer(mut stream: Event, span: tracing::Span) -> mpsc::Sender { @@ -131,9 +131,9 @@ pub fn spawn_writer(mut stream: Event, span: tracing::Span) -> mpsc::Sender Session { pub async fn write_bytes(&self, bytes: impl Into>) -> crate::OpResult { let bytes = bytes.into(); - /*for line in String::from_utf8_lossy(bytes.as_ref()).split("\r\n") { + for line in String::from_utf8_lossy(bytes.as_ref()).split("\r\n") { let c = println!("{}", line); - }*/ + } if let Err(err) = self.writer.send(Event::Bytes(bytes)).await { debug!("Failed to send bytes: {}", err); @@ -147,9 +147,9 @@ impl Session { impl SessionData { pub async fn write_bytes(&self, bytes: impl Into>) -> bool { let bytes = bytes.into(); - /*for line in String::from_utf8_lossy(bytes.as_ref()).split("\r\n") { + for line in String::from_utf8_lossy(bytes.as_ref()).split("\r\n") { let c = println!("{}", line); - }*/ + } if let Err(err) = self.writer.send(Event::Bytes(bytes)).await { debug!("Failed to send bytes: {}", err); diff --git a/crates/jmap/src/identity/get.rs b/crates/jmap/src/identity/get.rs index 35f6ac9e..1e02c72a 100644 --- a/crates/jmap/src/identity/get.rs +++ b/crates/jmap/src/identity/get.rs @@ -21,15 +21,22 @@ * for more details. */ +use directory::QueryBy; use jmap_proto::{ error::method::MethodError, method::get::{GetRequest, GetResponse, RequestArguments}, object::Object, types::{collection::Collection, property::Property, value::Value}, }; +use store::{ + roaring::RoaringBitmap, + write::{BatchBuilder, F_VALUE}, +}; use crate::JMAP; +use super::set::sanitize_email; + impl JMAP { pub async fn identity_get( &self, @@ -47,10 +54,7 @@ impl JMAP { Property::MayDelete, ]); let account_id = request.account_id.document_id(); - let identity_ids = self - .get_document_ids(account_id, Collection::Identity) - .await? - .unwrap_or_default(); + let identity_ids = self.identity_get_or_create(account_id).await?; let ids = if let Some(ids) = ids { ids } else { @@ -110,4 +114,82 @@ impl JMAP { Ok(response) } + + pub async fn identity_get_or_create( + &self, + account_id: u32, + ) -> Result { + let mut identity_ids = self + .get_document_ids(account_id, Collection::Identity) + .await? + .unwrap_or_default(); + if !identity_ids.is_empty() { + return Ok(identity_ids); + } + + // Obtain principal + let principal = self + .directory + .query(QueryBy::Id(account_id), false) + .await + .map_err(|err| { + tracing::error!( + event = "error", + context = "identity_get_or_create", + error = ?err, + "Failed to query directory."); + MethodError::ServerPartialFail + })? + .unwrap_or_default(); + if principal.emails.is_empty() { + return Ok(identity_ids); + } + + let mut batch = BatchBuilder::new(); + batch + .with_account_id(account_id) + .with_collection(Collection::Identity); + + // Create identities + let name = principal + .description + .unwrap_or(principal.name) + .trim() + .to_string(); + let has_many = principal.emails.len() > 1; + for email in principal.emails { + let email = sanitize_email(&email).unwrap_or_default(); + if email.is_empty() { + continue; + } + let identity_id = self + .assign_document_id(account_id, Collection::Identity) + .await?; + let name = if name.is_empty() { + email.clone() + } else if has_many { + format!("{} <{}>", name, email) + } else { + name.clone() + }; + batch.create_document(identity_id).value( + Property::Value, + Object::with_capacity(4) + .with_property(Property::Name, name) + .with_property(Property::Email, email), + F_VALUE, + ); + identity_ids.insert(identity_id); + } + self.store.write(batch.build()).await.map_err(|err| { + tracing::error!( + event = "error", + context = "identity_get_or_create", + error = ?err, + "Failed to create identities."); + MethodError::ServerPartialFail + })?; + + Ok(identity_ids) + } } diff --git a/tests/src/directory/internal.rs b/tests/src/directory/internal.rs index 73869ce1..f3627cc3 100644 --- a/tests/src/directory/internal.rs +++ b/tests/src/directory/internal.rs @@ -394,7 +394,7 @@ async fn internal_directory() { PrincipalUpdate::set(PrincipalField::Quota, PrincipalValue::Integer(1024)), PrincipalUpdate::set( PrincipalField::Type, - PrincipalValue::Type(Type::Superuser) + PrincipalValue::String("superuser".to_string()) ), PrincipalUpdate::remove_item( PrincipalField::Emails, diff --git a/tests/src/jmap/delivery.rs b/tests/src/jmap/delivery.rs index b8fcee7e..8453d1f1 100644 --- a/tests/src/jmap/delivery.rs +++ b/tests/src/jmap/delivery.rs @@ -24,7 +24,8 @@ use std::time::Duration; use directory::backend::internal::manage::ManageDirectory; -use jmap_proto::types::{collection::Collection, id::Id}; +use jmap::mailbox::{INBOX_ID, TRASH_ID}; +use jmap_proto::types::{collection::Collection, id::Id, property::Property}; use tokio::{ io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Lines, ReadHalf, WriteHalf}, @@ -105,6 +106,7 @@ pub async fn test(params: &mut JMAPTest) { "From: bill@example.com\r\n", "To: jdoe@example.com\r\n", "Subject: TPS Report\r\n", + "X-Spam-Status: No\r\n", "\r\n", "I'm going to need those TPS reports ASAP. ", "So, if you could do that, that'd be great." @@ -112,20 +114,35 @@ pub async fn test(params: &mut JMAPTest) { ) .await; + let john_id = Id::from_bytes(account_id_1.as_bytes()) + .unwrap() + .document_id(); assert_eq!( server - .get_document_ids( - Id::from_bytes(account_id_1.as_bytes()) - .unwrap() - .document_id(), - Collection::Email - ) + .get_document_ids(john_id, Collection::Email) .await .unwrap() .unwrap() .len(), 1 ); + assert_eq!( + server + .get_tag(john_id, Collection::Email, Property::MailboxIds, INBOX_ID) + .await + .unwrap() + .unwrap() + .len(), + 1 + ); + assert_eq!( + server + .get_tag(john_id, Collection::Email, Property::MailboxIds, TRASH_ID) + .await + .unwrap() + .map_or(0, |bm| bm.len()), + 0 + ); // Delivering to individuals' aliases lmtp.ingest( @@ -135,6 +152,7 @@ pub async fn test(params: &mut JMAPTest) { "From: bill@example.com\r\n", "To: john.doe@example.com\r\n", "Subject: Fwd: TPS Report\r\n", + "X-Spam-Status: Yes, score=13.9\r\n", "\r\n", "--- Forwarded Message ---\r\n\r\n ", "I'm going to need those TPS reports ASAP. ", @@ -145,18 +163,31 @@ pub async fn test(params: &mut JMAPTest) { assert_eq!( server - .get_document_ids( - Id::from_bytes(account_id_1.as_bytes()) - .unwrap() - .document_id(), - Collection::Email - ) + .get_document_ids(john_id, Collection::Email) .await .unwrap() .unwrap() .len(), 2 ); + assert_eq!( + server + .get_tag(john_id, Collection::Email, Property::MailboxIds, INBOX_ID) + .await + .unwrap() + .unwrap() + .len(), + 1 + ); + assert_eq!( + server + .get_tag(john_id, Collection::Email, Property::MailboxIds, TRASH_ID) + .await + .unwrap() + .unwrap() + .len(), + 1 + ); // EXPN and VRFY lmtp.expn("members@example.com", 2) diff --git a/tests/src/jmap/email_submission.rs b/tests/src/jmap/email_submission.rs index f2ce9f49..380d0265 100644 --- a/tests/src/jmap/email_submission.rs +++ b/tests/src/jmap/email_submission.rs @@ -97,6 +97,10 @@ pub async fn test(params: &mut JMAPTest) { .directory .create_test_user_with_email("jdoe@example.com", "12345", "John Doe") .await; + params + .directory + .link_test_address("jdoe@example.com", "john.doe@example.com", "alias") + .await; let account_id = Id::from( server .store @@ -106,6 +110,17 @@ pub async fn test(params: &mut JMAPTest) { ) .to_string(); + // Test automatic identity creation + for (identity_id, email) in [(0u64, "jdoe@example.com"), (1u64, "john.doe@example.com")] { + let identity = client + .identity_get(&Id::from(identity_id).to_string(), None) + .await + .unwrap() + .unwrap(); + assert_eq!(identity.email().unwrap(), email); + assert_eq!(identity.name().unwrap(), format!("John Doe <{email}>")); + } + // Create an identity without using a valid address should fail match client .set_default_account_id(&account_id) @@ -119,7 +134,7 @@ pub async fn test(params: &mut JMAPTest) { // Create an identity let identity_id = client - .identity_create("John Doe", "jdoe@example.com") + .identity_create("John Doe (manually created)", "jdoe@example.com") .await .unwrap() .take_id(); @@ -473,7 +488,13 @@ pub async fn test(params: &mut JMAPTest) { smtp_settings.lock().do_stop = true; // Destroy the created mailbox, identity and all submissions - client.identity_destroy(&identity_id).await.unwrap(); + for identity_id in [ + identity_id, + Id::from(0u64).to_string(), + Id::from(1u64).to_string(), + ] { + client.identity_destroy(&identity_id).await.unwrap(); + } for id in client .email_submission_query(None::, None::>) .await diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index b02d8fbc..8872ead3 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -177,6 +177,9 @@ data = "{STORE}" fts = "{STORE}" blob = "{STORE}" +[jmap.spam] +header = "X-Spam-Status: Yes" + [jmap.protocol.get] max-objects = 100000 @@ -295,18 +298,18 @@ pub async fn jmap_tests() { email_query_changes::test(&mut params).await; email_copy::test(&mut params).await; thread_get::test(&mut params).await;*/ - thread_merge::test(&mut params).await; - /*mailbox::test(&mut params).await; - delivery::test(&mut params).await; - auth_acl::test(&mut params).await; + //thread_merge::test(&mut params).await; + //mailbox::test(&mut params).await; + //delivery::test(&mut params).await; + /*auth_acl::test(&mut params).await; auth_limits::test(&mut params).await; auth_oauth::test(&mut params).await; event_source::test(&mut params).await; push_subscription::test(&mut params).await; sieve_script::test(&mut params).await; - vacation_response::test(&mut params).await; + vacation_response::test(&mut params).await;*/ email_submission::test(&mut params).await; - websocket::test(&mut params).await; + /*websocket::test(&mut params).await; quota::test(&mut params).await; crypto::test(&mut params).await; blob::test(&mut params).await;*/