mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-10-07 19:15:47 +08:00
Automatically create Inbox for group accounts
This commit is contained in:
parent
a0ceaeba1c
commit
80f6a5469a
8 changed files with 128 additions and 51 deletions
|
@ -73,43 +73,8 @@ impl MemoryDirectory {
|
||||||
member_of,
|
member_of,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let mut emails = Vec::new();
|
|
||||||
for (pos, (_, email)) in config
|
|
||||||
.values((prefix.as_str(), "users", lookup_id, "email"))
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
directory
|
|
||||||
.emails_to_names
|
|
||||||
.entry(email.to_string())
|
|
||||||
.or_default()
|
|
||||||
.push(if pos > 0 {
|
|
||||||
EmailType::Alias(name.clone())
|
|
||||||
} else {
|
|
||||||
EmailType::Primary(name.clone())
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some((_, domain)) = email.rsplit_once('@') {
|
directory.parse_emails(config, (prefix.as_str(), "users", lookup_id), name)?;
|
||||||
directory.domains.insert(domain.to_lowercase());
|
|
||||||
}
|
|
||||||
|
|
||||||
emails.push(if pos > 0 {
|
|
||||||
EmailType::Alias(email.to_lowercase())
|
|
||||||
} else {
|
|
||||||
EmailType::Primary(email.to_lowercase())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
for (_, email) in config.values((prefix.as_str(), "users", lookup_id, "email-list")) {
|
|
||||||
directory
|
|
||||||
.emails_to_names
|
|
||||||
.entry(email.to_lowercase())
|
|
||||||
.or_default()
|
|
||||||
.push(EmailType::List(name.clone()));
|
|
||||||
if let Some((_, domain)) = email.rsplit_once('@') {
|
|
||||||
directory.domains.insert(domain.to_lowercase());
|
|
||||||
}
|
|
||||||
emails.push(EmailType::List(email.to_lowercase()));
|
|
||||||
}
|
|
||||||
directory.names_to_email.insert(name, emails);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for lookup_id in config.sub_keys((prefix.as_str(), "groups")) {
|
for lookup_id in config.sub_keys((prefix.as_str(), "groups")) {
|
||||||
|
@ -119,7 +84,7 @@ impl MemoryDirectory {
|
||||||
directory.principals.insert(
|
directory.principals.insert(
|
||||||
name.clone(),
|
name.clone(),
|
||||||
Principal {
|
Principal {
|
||||||
name,
|
name: name.clone(),
|
||||||
secrets: vec![],
|
secrets: vec![],
|
||||||
typ: Type::Group,
|
typ: Type::Group,
|
||||||
description: config
|
description: config
|
||||||
|
@ -134,6 +99,8 @@ impl MemoryDirectory {
|
||||||
.collect(),
|
.collect(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
directory.parse_emails(config, (prefix.as_str(), "groups", lookup_id), name)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
directory
|
directory
|
||||||
|
@ -143,3 +110,49 @@ impl MemoryDirectory {
|
||||||
Ok(Arc::new(directory))
|
Ok(Arc::new(directory))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MemoryDirectory {
|
||||||
|
fn parse_emails(
|
||||||
|
&mut self,
|
||||||
|
config: &Config,
|
||||||
|
prefix: impl AsKey,
|
||||||
|
name: String,
|
||||||
|
) -> utils::config::Result<()> {
|
||||||
|
let prefix = prefix.as_key();
|
||||||
|
let mut emails = Vec::new();
|
||||||
|
|
||||||
|
for (pos, (_, email)) in config.values((prefix.as_str(), "email")).enumerate() {
|
||||||
|
self.emails_to_names
|
||||||
|
.entry(email.to_string())
|
||||||
|
.or_default()
|
||||||
|
.push(if pos > 0 {
|
||||||
|
EmailType::Alias(name.clone())
|
||||||
|
} else {
|
||||||
|
EmailType::Primary(name.clone())
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some((_, domain)) = email.rsplit_once('@') {
|
||||||
|
self.domains.insert(domain.to_lowercase());
|
||||||
|
}
|
||||||
|
|
||||||
|
emails.push(if pos > 0 {
|
||||||
|
EmailType::Alias(email.to_lowercase())
|
||||||
|
} else {
|
||||||
|
EmailType::Primary(email.to_lowercase())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (_, email) in config.values((prefix.as_str(), "email-list")) {
|
||||||
|
self.emails_to_names
|
||||||
|
.entry(email.to_lowercase())
|
||||||
|
.or_default()
|
||||||
|
.push(EmailType::List(name.clone()));
|
||||||
|
if let Some((_, domain)) = email.rsplit_once('@') {
|
||||||
|
self.domains.insert(domain.to_lowercase());
|
||||||
|
}
|
||||||
|
emails.push(EmailType::List(email.to_lowercase()));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.names_to_email.insert(name, emails);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ use crate::{DirectoryOptions, Principal};
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod lookup;
|
pub mod lookup;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default, Debug)]
|
||||||
pub struct MemoryDirectory {
|
pub struct MemoryDirectory {
|
||||||
principals: AHashMap<String, Principal>,
|
principals: AHashMap<String, Principal>,
|
||||||
emails_to_names: AHashMap<String, Vec<EmailType>>,
|
emails_to_names: AHashMap<String, Vec<EmailType>>,
|
||||||
|
@ -37,6 +37,7 @@ pub struct MemoryDirectory {
|
||||||
opt: DirectoryOptions,
|
opt: DirectoryOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
enum EmailType {
|
enum EmailType {
|
||||||
Primary(String),
|
Primary(String),
|
||||||
Alias(String),
|
Alias(String),
|
||||||
|
|
|
@ -81,17 +81,13 @@ impl SessionData {
|
||||||
mailbox_prefix: Option<String>,
|
mailbox_prefix: Option<String>,
|
||||||
access_token: &AccessToken,
|
access_token: &AccessToken,
|
||||||
) -> crate::Result<Account> {
|
) -> crate::Result<Account> {
|
||||||
let mailbox_ids = if access_token.is_primary_id(account_id) {
|
let mailbox_ids = if access_token.is_primary_id(account_id)
|
||||||
|
|| access_token.member_of.contains(&account_id)
|
||||||
|
{
|
||||||
self.jmap
|
self.jmap
|
||||||
.mailbox_get_or_create(account_id)
|
.mailbox_get_or_create(account_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| {})?
|
.map_err(|_| {})?
|
||||||
} else if access_token.member_of.contains(&account_id) {
|
|
||||||
self.jmap
|
|
||||||
.get_document_ids(account_id, Collection::Mailbox)
|
|
||||||
.await
|
|
||||||
.map_err(|_| {})?
|
|
||||||
.unwrap_or_default()
|
|
||||||
} else {
|
} else {
|
||||||
self.jmap
|
self.jmap
|
||||||
.shared_documents(access_token, account_id, Collection::Mailbox, Acl::Read)
|
.shared_documents(access_token, account_id, Collection::Mailbox, Acl::Read)
|
||||||
|
|
|
@ -57,8 +57,7 @@ pub fn spawn_writer(mut stream: Event, span: tracing::Span) -> mpsc::Sender<Even
|
||||||
data = std::str::from_utf8(bytes.as_ref()).unwrap_or_default(),
|
data = std::str::from_utf8(bytes.as_ref()).unwrap_or_default(),
|
||||||
size = bytes.len()
|
size = bytes.len()
|
||||||
);
|
);
|
||||||
/*let tmp = "dd";
|
/*let c = println!(
|
||||||
println!(
|
|
||||||
"<- {:?}",
|
"<- {:?}",
|
||||||
String::from_utf8_lossy(
|
String::from_utf8_lossy(
|
||||||
&bytes[..std::cmp::min(bytes.len(), 100)]
|
&bytes[..std::cmp::min(bytes.len(), 100)]
|
||||||
|
|
|
@ -176,10 +176,12 @@ email-list = ["info@example.org"]
|
||||||
|
|
||||||
[[directory."local".groups]]
|
[[directory."local".groups]]
|
||||||
name = "sales"
|
name = "sales"
|
||||||
|
email = "sales@example.org"
|
||||||
description = "Sales Team"
|
description = "Sales Team"
|
||||||
|
|
||||||
[[directory."local".groups]]
|
[[directory."local".groups]]
|
||||||
name = "support"
|
name = "support"
|
||||||
|
email = "support@example.org"
|
||||||
description = "Support Team"
|
description = "Support Team"
|
||||||
|
|
||||||
[oauth]
|
[oauth]
|
||||||
|
|
|
@ -23,9 +23,27 @@
|
||||||
|
|
||||||
use imap_proto::ResponseType;
|
use imap_proto::ResponseType;
|
||||||
|
|
||||||
|
use crate::jmap::delivery::SmtpConnection;
|
||||||
|
|
||||||
use super::{append::assert_append_message, AssertResult, ImapConnection, Type};
|
use super::{append::assert_append_message, AssertResult, ImapConnection, Type};
|
||||||
|
|
||||||
pub async fn test(mut imap_john: &mut ImapConnection, _imap_check: &mut ImapConnection) {
|
pub async fn test(mut imap_john: &mut ImapConnection, _imap_check: &mut ImapConnection) {
|
||||||
|
// Delivery to support account
|
||||||
|
let mut lmtp = SmtpConnection::connect_port(11201).await;
|
||||||
|
lmtp.ingest(
|
||||||
|
"bill@example.com",
|
||||||
|
&["support@example.com"],
|
||||||
|
concat!(
|
||||||
|
"From: bill@example.com\r\n",
|
||||||
|
"To: support@example.com\r\n",
|
||||||
|
"Subject: TPS Report\r\n",
|
||||||
|
"\r\n",
|
||||||
|
"I'm going to need those TPS reports ASAP. ",
|
||||||
|
"So, if you could do that, that'd be great."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
// Connect to all test accounts
|
// Connect to all test accounts
|
||||||
let mut imap_jane = ImapConnection::connect(b"_w ").await;
|
let mut imap_jane = ImapConnection::connect(b"_w ").await;
|
||||||
let mut imap_bill = ImapConnection::connect(b"_z ").await;
|
let mut imap_bill = ImapConnection::connect(b"_z ").await;
|
||||||
|
@ -43,6 +61,25 @@ pub async fn test(mut imap_john: &mut ImapConnection, _imap_check: &mut ImapConn
|
||||||
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Jane should see the Support account
|
||||||
|
imap_jane.send("LIST \"\" \"*\"").await;
|
||||||
|
imap_jane
|
||||||
|
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||||
|
.await
|
||||||
|
.assert_contains("Shared Folders/support@example.com/Inbox");
|
||||||
|
|
||||||
|
imap_jane
|
||||||
|
.send("SELECT \"Shared Folders/support@example.com/Inbox\"")
|
||||||
|
.await;
|
||||||
|
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||||
|
imap_jane.send("FETCH 1 (PREVIEW)").await;
|
||||||
|
imap_jane
|
||||||
|
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||||
|
.await
|
||||||
|
.assert_contains("TPS reports ASAP");
|
||||||
|
imap_jane.send("UNSELECT").await;
|
||||||
|
imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||||
|
|
||||||
// John should have no shared folders
|
// John should have no shared folders
|
||||||
imap_john.send("LIST \"\" \"*\"").await;
|
imap_john.send("LIST \"\" \"*\"").await;
|
||||||
imap_john
|
imap_john
|
||||||
|
|
|
@ -42,7 +42,7 @@ use directory::config::ConfigDirectory;
|
||||||
use imap::core::{ImapSessionManager, IMAP};
|
use imap::core::{ImapSessionManager, IMAP};
|
||||||
use imap_proto::ResponseType;
|
use imap_proto::ResponseType;
|
||||||
use jmap::{api::JmapSessionManager, services::IPC_CHANNEL_BUFFER, JMAP};
|
use jmap::{api::JmapSessionManager, services::IPC_CHANNEL_BUFFER, JMAP};
|
||||||
use smtp::core::SMTP;
|
use smtp::core::{SmtpSessionManager, SMTP};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Lines, ReadHalf, WriteHalf},
|
io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Lines, ReadHalf, WriteHalf},
|
||||||
net::TcpStream,
|
net::TcpStream,
|
||||||
|
@ -53,7 +53,8 @@ use utils::{config::ServerProtocol, UnwrapFailure};
|
||||||
use crate::{
|
use crate::{
|
||||||
add_test_certs,
|
add_test_certs,
|
||||||
directory::sql::{
|
directory::sql::{
|
||||||
add_to_group, create_test_directory, create_test_user, create_test_user_with_email,
|
add_to_group, create_test_directory, create_test_group_with_email, create_test_user,
|
||||||
|
create_test_user_with_email,
|
||||||
},
|
},
|
||||||
store::TempDir,
|
store::TempDir,
|
||||||
};
|
};
|
||||||
|
@ -79,6 +80,12 @@ protocol = "managesieve"
|
||||||
max-connections = 81920
|
max-connections = 81920
|
||||||
tls.implicit = true
|
tls.implicit = true
|
||||||
|
|
||||||
|
[server.listener.lmtp-debug]
|
||||||
|
bind = ['127.0.0.1:11201']
|
||||||
|
greeting = 'Test LMTP instance'
|
||||||
|
protocol = 'lmtp'
|
||||||
|
tls.implicit = false
|
||||||
|
|
||||||
[server.socket]
|
[server.socket]
|
||||||
reuse-addr = true
|
reuse-addr = true
|
||||||
|
|
||||||
|
@ -260,6 +267,9 @@ async fn init_imap_tests(delete_if_exists: bool) -> IMAPTest {
|
||||||
ManageSieveSessionManager::new(jmap.clone(), imap.clone()),
|
ManageSieveSessionManager::new(jmap.clone(), imap.clone()),
|
||||||
shutdown_rx,
|
shutdown_rx,
|
||||||
),
|
),
|
||||||
|
ServerProtocol::Smtp | ServerProtocol::Lmtp => {
|
||||||
|
server.spawn(SmtpSessionManager::new(smtp.clone()), shutdown_rx)
|
||||||
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -289,6 +299,18 @@ async fn init_imap_tests(delete_if_exists: bool) -> IMAPTest {
|
||||||
"Bill Foobar",
|
"Bill Foobar",
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
create_test_group_with_email(
|
||||||
|
jmap.directory.as_ref(),
|
||||||
|
"support@example.com",
|
||||||
|
"Support Group",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
add_to_group(
|
||||||
|
jmap.directory.as_ref(),
|
||||||
|
"jane.smith@example.com",
|
||||||
|
"support@example.com",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
if delete_if_exists {
|
if delete_if_exists {
|
||||||
jmap.store.destroy().await;
|
jmap.store.destroy().await;
|
||||||
|
|
|
@ -295,8 +295,15 @@ impl SmtpConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn connect() -> Self {
|
pub async fn connect() -> Self {
|
||||||
let (reader, writer) =
|
SmtpConnection::connect_port(11200).await
|
||||||
tokio::io::split(TcpStream::connect("127.0.0.1:11200").await.unwrap());
|
}
|
||||||
|
|
||||||
|
pub async fn connect_port(port: u16) -> Self {
|
||||||
|
let (reader, writer) = tokio::io::split(
|
||||||
|
TcpStream::connect(&format!("127.0.0.1:{port}"))
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
let mut conn = SmtpConnection {
|
let mut conn = SmtpConnection {
|
||||||
reader: BufReader::new(reader).lines(),
|
reader: BufReader::new(reader).lines(),
|
||||||
writer,
|
writer,
|
||||||
|
|
Loading…
Add table
Reference in a new issue