mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-09-20 15:26:17 +08:00
CLI import/export tested.
This commit is contained in:
parent
1f4204c6bf
commit
fac3273f10
|
@ -31,6 +31,7 @@ use console::style;
|
||||||
use jmap_client::client::{Client, Credentials};
|
use jmap_client::client::{Client, Credentials};
|
||||||
use modules::{
|
use modules::{
|
||||||
cli::{Cli, Commands},
|
cli::{Cli, Commands},
|
||||||
|
database::cmd_database,
|
||||||
export::cmd_export,
|
export::cmd_export,
|
||||||
get,
|
get,
|
||||||
import::cmd_import,
|
import::cmd_import,
|
||||||
|
@ -64,19 +65,14 @@ async fn main() -> std::io::Result<()> {
|
||||||
};
|
};
|
||||||
|
|
||||||
if is_jmap {
|
if is_jmap {
|
||||||
let client = Client::new()
|
|
||||||
.credentials(credentials)
|
|
||||||
.accept_invalid_certs(is_localhost(&args.url))
|
|
||||||
.connect(&args.url)
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|err| {
|
|
||||||
eprintln!("Failed to connect to JMAP server {}: {}.", args.url, err);
|
|
||||||
std::process::exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
Commands::Import(command) => cmd_import(client, command).await,
|
Commands::Import(command) => {
|
||||||
Commands::Export(command) => cmd_export(client, command).await,
|
cmd_import(build_client(&args.url, credentials).await, command).await
|
||||||
|
}
|
||||||
|
Commands::Export(command) => {
|
||||||
|
cmd_export(build_client(&args.url, credentials).await, command).await
|
||||||
|
}
|
||||||
|
Commands::Database(command) => cmd_database(&args.url, credentials, command).await,
|
||||||
Commands::Queue(_) | Commands::Report(_) => unreachable!(),
|
Commands::Queue(_) | Commands::Report(_) => unreachable!(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -90,6 +86,18 @@ async fn main() -> std::io::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn build_client(url: &str, credentials: Credentials) -> Client {
|
||||||
|
Client::new()
|
||||||
|
.credentials(credentials)
|
||||||
|
.accept_invalid_certs(is_localhost(url))
|
||||||
|
.connect(url)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|err| {
|
||||||
|
eprintln!("Failed to connect to JMAP server {}: {}.", url, err);
|
||||||
|
std::process::exit(1);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_credentials(credentials: &str) -> Credentials {
|
fn parse_credentials(credentials: &str) -> Credentials {
|
||||||
if let Some((account, secret)) = credentials.split_once(':') {
|
if let Some((account, secret)) = credentials.split_once(':') {
|
||||||
Credentials::basic(account, secret)
|
Credentials::basic(account, secret)
|
||||||
|
|
|
@ -49,6 +49,10 @@ pub enum Commands {
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
Export(ExportCommands),
|
Export(ExportCommands),
|
||||||
|
|
||||||
|
/// Manage JMAP database
|
||||||
|
#[clap(subcommand)]
|
||||||
|
Database(DatabaseCommands),
|
||||||
|
|
||||||
/// Manage SMTP message queue
|
/// Manage SMTP message queue
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
Queue(QueueCommands),
|
Queue(QueueCommands),
|
||||||
|
@ -106,6 +110,26 @@ pub enum ExportCommands {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum DatabaseCommands {
|
||||||
|
/// Delete a JMAP account
|
||||||
|
Delete {
|
||||||
|
/// Account name to delete
|
||||||
|
account: String,
|
||||||
|
},
|
||||||
|
/// Rename a JMAP account
|
||||||
|
Rename {
|
||||||
|
/// Account name to rename
|
||||||
|
account: String,
|
||||||
|
|
||||||
|
/// New account name
|
||||||
|
new_account: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Purge expired blobs
|
||||||
|
Purge {},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
pub enum MailboxFormat {
|
pub enum MailboxFormat {
|
||||||
/// Mbox format
|
/// Mbox format
|
||||||
|
|
39
crates/cli/src/modules/database.rs
Normal file
39
crates/cli/src/modules/database.rs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
use jmap_client::client::Credentials;
|
||||||
|
use reqwest::header::AUTHORIZATION;
|
||||||
|
|
||||||
|
use super::{cli::DatabaseCommands, is_localhost, UnwrapResult};
|
||||||
|
|
||||||
|
pub async fn cmd_database(url: &str, credentials: Credentials, command: DatabaseCommands) {
|
||||||
|
let url = match command {
|
||||||
|
DatabaseCommands::Delete { account } => format!("{}/admin/account/delete/{}", url, account),
|
||||||
|
DatabaseCommands::Rename {
|
||||||
|
account,
|
||||||
|
new_account,
|
||||||
|
} => format!("{}/admin/account/rename/{}/{}", url, account, new_account),
|
||||||
|
DatabaseCommands::Purge {} => format!("{}/admin/blob/purge", url),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = reqwest::Client::builder()
|
||||||
|
.danger_accept_invalid_certs(is_localhost(&url))
|
||||||
|
.build()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.get(url)
|
||||||
|
.header(
|
||||||
|
AUTHORIZATION,
|
||||||
|
match credentials {
|
||||||
|
Credentials::Basic(s) => format!("Basic {s}"),
|
||||||
|
Credentials::Bearer(s) => format!("Bearer {s}"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap_result("send GET request");
|
||||||
|
if response.status().is_success() {
|
||||||
|
eprintln!("Success.");
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"Request Failed: {}",
|
||||||
|
response.text().await.unwrap_result("fetch text")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -460,7 +460,7 @@ async fn import_mailboxes(client: &Client, path: &Path) -> HashMap<String, Strin
|
||||||
if !matches!(mailbox.role(), Role::None) {
|
if !matches!(mailbox.role(), Role::None) {
|
||||||
if let Some(existing_mailbox) = existing_mailboxes
|
if let Some(existing_mailbox) = existing_mailboxes
|
||||||
.iter()
|
.iter()
|
||||||
.find(|m| m.role() != mailbox.role())
|
.find(|m| m.role() == mailbox.role())
|
||||||
{
|
{
|
||||||
id_mappings.insert(
|
id_mappings.insert(
|
||||||
id.to_string(),
|
id.to_string(),
|
||||||
|
@ -484,6 +484,7 @@ async fn import_mailboxes(client: &Client, path: &Path) -> HashMap<String, Strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut total_imported = 0;
|
let mut total_imported = 0;
|
||||||
|
let mut total_existing = 0;
|
||||||
if !id_missing.is_empty() {
|
if !id_missing.is_empty() {
|
||||||
let mut request = client.build();
|
let mut request = client.build();
|
||||||
let set_request = request.set_mailbox();
|
let set_request = request.set_mailbox();
|
||||||
|
@ -492,6 +493,7 @@ async fn import_mailboxes(client: &Client, path: &Path) -> HashMap<String, Strin
|
||||||
// Skip if mailbox already exists
|
// Skip if mailbox already exists
|
||||||
let id = mailbox.id().unwrap_result("obtain mailbox id").to_string();
|
let id = mailbox.id().unwrap_result("obtain mailbox id").to_string();
|
||||||
if id_mappings.contains_key(&id) {
|
if id_mappings.contains_key(&id) {
|
||||||
|
total_existing += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let create_request = set_request
|
let create_request = set_request
|
||||||
|
@ -533,9 +535,16 @@ async fn import_mailboxes(client: &Client, path: &Path) -> HashMap<String, Strin
|
||||||
);
|
);
|
||||||
total_imported += 1;
|
total_imported += 1;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
total_existing = mailboxes.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
eprintln!("Successfully imported {} mailboxes.", total_imported);
|
eprintln!(
|
||||||
|
"Successfully processed {} mailboxes ({} imported, {} already exist).",
|
||||||
|
total_existing + total_imported,
|
||||||
|
total_imported,
|
||||||
|
total_existing
|
||||||
|
);
|
||||||
|
|
||||||
id_mappings
|
id_mappings
|
||||||
}
|
}
|
||||||
|
@ -564,19 +573,19 @@ async fn import_emails(
|
||||||
.await;
|
.await;
|
||||||
let existing_ids = existing_emails
|
let existing_ids = existing_emails
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|email| email.message_id())
|
.map(|email| (email.message_id(), email.received_at()))
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
let mut futures = FuturesUnordered::new();
|
let mut futures = FuturesUnordered::new();
|
||||||
let total_imported = Arc::new(AtomicUsize::from(0));
|
let total_imported = Arc::new(AtomicUsize::from(0));
|
||||||
|
let mut total_existing = 0;
|
||||||
let mut path = PathBuf::from(path);
|
let mut path = PathBuf::from(path);
|
||||||
path.push("blobs");
|
path.push("blobs");
|
||||||
|
|
||||||
for email in emails {
|
for email in emails {
|
||||||
// Skip messages that already exist in the server
|
// Skip messages that already exist in the server
|
||||||
if let Some(message_ids) = email.message_id() {
|
if existing_ids.contains(&(email.message_id(), email.received_at())) {
|
||||||
if existing_ids.contains(message_ids) {
|
total_existing += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn import tasks
|
// Spawn import tasks
|
||||||
|
@ -668,8 +677,10 @@ async fn import_emails(
|
||||||
|
|
||||||
// Done
|
// Done
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Successfully imported {} messages.",
|
"Successfully processed {} emails ({} imported, {} already exist).",
|
||||||
total_imported.load(Ordering::Relaxed)
|
total_imported.load(Ordering::Relaxed) + total_existing,
|
||||||
|
total_imported.load(Ordering::Relaxed),
|
||||||
|
total_existing
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -694,11 +705,13 @@ async fn import_sieve_scripts(client: &Client, path: &Path, num_concurrent: usiz
|
||||||
// Spawn tasks
|
// Spawn tasks
|
||||||
let mut futures = FuturesUnordered::new();
|
let mut futures = FuturesUnordered::new();
|
||||||
let total_imported = Arc::new(AtomicUsize::from(0));
|
let total_imported = Arc::new(AtomicUsize::from(0));
|
||||||
|
let mut total_existing = 0;
|
||||||
|
|
||||||
'outer: for script in scripts {
|
'outer: for script in scripts {
|
||||||
// Skip scripts that already exist
|
// Skip scripts that already exist
|
||||||
for existing_script in &existing_scripts {
|
for existing_script in &existing_scripts {
|
||||||
if existing_script.name() == script.name() {
|
if existing_script.name() == script.name() {
|
||||||
|
total_existing += 1;
|
||||||
continue 'outer;
|
continue 'outer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -770,8 +783,10 @@ async fn import_sieve_scripts(client: &Client, path: &Path, num_concurrent: usiz
|
||||||
|
|
||||||
// Done
|
// Done
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Successfully imported {} sieve script.",
|
"Successfully processed {} sieve scripts ({} imported, {} already exist).",
|
||||||
total_imported.load(Ordering::Relaxed)
|
total_imported.load(Ordering::Relaxed) + total_existing,
|
||||||
|
total_imported.load(Ordering::Relaxed),
|
||||||
|
total_existing
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -785,12 +800,14 @@ async fn import_identities(client: &Client, path: &Path) {
|
||||||
let mut request = client.build();
|
let mut request = client.build();
|
||||||
let set_request = request.set_identity();
|
let set_request = request.set_identity();
|
||||||
let mut create_ids = Vec::new();
|
let mut create_ids = Vec::new();
|
||||||
|
let mut total_existing = 0;
|
||||||
|
|
||||||
'outer: for identity in &identities {
|
'outer: for identity in &identities {
|
||||||
for existing_identity in &existing_identities {
|
for existing_identity in &existing_identities {
|
||||||
if identity.name() == existing_identity.name()
|
if identity.name() == existing_identity.name()
|
||||||
&& identity.email() == existing_identity.email()
|
&& identity.email() == existing_identity.email()
|
||||||
{
|
{
|
||||||
|
total_existing += 1;
|
||||||
continue 'outer;
|
continue 'outer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -798,22 +815,21 @@ async fn import_identities(client: &Client, path: &Path) {
|
||||||
if let (Some(id), Some(name), Some(email)) =
|
if let (Some(id), Some(name), Some(email)) =
|
||||||
(identity.id(), identity.name(), identity.email())
|
(identity.id(), identity.name(), identity.email())
|
||||||
{
|
{
|
||||||
if name == "vacation" {
|
if name != "vacation" {
|
||||||
continue;
|
create_ids.push(id);
|
||||||
}
|
let create_request = set_request.create_with_id(id).name(name).email(email);
|
||||||
create_ids.push(id);
|
if let Some(reply_to) = identity.reply_to() {
|
||||||
let create_request = set_request.create_with_id(id).name(name).email(email);
|
create_request.reply_to(reply_to.iter().cloned().into());
|
||||||
if let Some(reply_to) = identity.reply_to() {
|
}
|
||||||
create_request.reply_to(reply_to.iter().cloned().into());
|
if let Some(bcc) = identity.bcc() {
|
||||||
}
|
create_request.bcc(bcc.iter().cloned().into());
|
||||||
if let Some(bcc) = identity.bcc() {
|
}
|
||||||
create_request.bcc(bcc.iter().cloned().into());
|
if let Some(html_signature) = identity.html_signature() {
|
||||||
}
|
create_request.html_signature(html_signature);
|
||||||
if let Some(html_signature) = identity.html_signature() {
|
}
|
||||||
create_request.html_signature(html_signature);
|
if let Some(text_signature) = identity.text_signature() {
|
||||||
}
|
create_request.text_signature(text_signature);
|
||||||
if let Some(text_signature) = identity.text_signature() {
|
}
|
||||||
create_request.text_signature(text_signature);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Skipping identity with no id, name, and/or email.");
|
eprintln!("Skipping identity with no id, name, and/or email.");
|
||||||
|
@ -821,23 +837,31 @@ async fn import_identities(client: &Client, path: &Path) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match request.send_set_identity().await {
|
let mut total_imported = 0;
|
||||||
Ok(mut response) => {
|
if !create_ids.is_empty() {
|
||||||
let mut total_imported = 0;
|
match request.send_set_identity().await {
|
||||||
for id in create_ids {
|
Ok(mut response) => {
|
||||||
if let Err(err) = response.created(&id) {
|
for id in create_ids {
|
||||||
eprintln!("Failed to import identity {id}: {err}");
|
if let Err(err) = response.created(&id) {
|
||||||
} else {
|
eprintln!("Failed to import identity {id}: {err}");
|
||||||
total_imported += 1;
|
} else {
|
||||||
|
total_imported += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(err) => {
|
||||||
eprintln!("Successfully imported {} identities.", total_imported);
|
eprintln!("Failed to import identities: {err}");
|
||||||
}
|
return;
|
||||||
Err(err) => {
|
}
|
||||||
eprintln!("Failed to import identities: {err}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"Successfully processed {} identities ({} imported, {} already exist).",
|
||||||
|
total_imported + total_existing,
|
||||||
|
total_imported,
|
||||||
|
total_existing
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn import_vacation_responses(client: &Client, path: &Path) {
|
async fn import_vacation_responses(client: &Client, path: &Path) {
|
||||||
|
@ -849,6 +873,7 @@ async fn import_vacation_responses(client: &Client, path: &Path) {
|
||||||
}
|
}
|
||||||
let existing_vacation_responses = fetch_vacation_responses(client).await;
|
let existing_vacation_responses = fetch_vacation_responses(client).await;
|
||||||
if !existing_vacation_responses.is_empty() {
|
if !existing_vacation_responses.is_empty() {
|
||||||
|
eprintln!("Successfully processed 1 vacation response (0 imported, 1 already exist).",);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -881,7 +906,9 @@ async fn import_vacation_responses(client: &Client, path: &Path) {
|
||||||
if let Err(err) = response.created(&create_id) {
|
if let Err(err) = response.created(&create_id) {
|
||||||
eprintln!("Failed to import vacation response: {err}");
|
eprintln!("Failed to import vacation response: {err}");
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Successfully imported 1 vacation response.");
|
eprintln!(
|
||||||
|
"Successfully processed 1 vacation response (1 imported, 0 already exist).",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
@ -906,15 +933,17 @@ fn build_mailbox_tree(
|
||||||
'outer: loop {
|
'outer: loop {
|
||||||
while let Some(mailbox) = mailboxes_iter.next() {
|
while let Some(mailbox) = mailboxes_iter.next() {
|
||||||
if parent_id == mailbox.parent_id() {
|
if parent_id == mailbox.parent_id() {
|
||||||
|
let name = mailbox.name().unwrap_result("obtain mailbox name");
|
||||||
if parents.contains(&mailbox.id()) {
|
if parents.contains(&mailbox.id()) {
|
||||||
stack.push((path.clone(), parent_id, mailboxes_iter));
|
stack.push((path.clone(), parent_id, mailboxes_iter));
|
||||||
parent_id = mailbox.id();
|
parent_id = mailbox.id();
|
||||||
path.push(mailbox.name().unwrap_result("obtain mailbox name"));
|
path.push(name);
|
||||||
|
results.insert(path.clone(), mailbox);
|
||||||
mailboxes_iter = mailboxes.iter();
|
mailboxes_iter = mailboxes.iter();
|
||||||
continue 'outer;
|
continue 'outer;
|
||||||
} else {
|
} else {
|
||||||
let mut path = path.clone();
|
let mut path = path.clone();
|
||||||
path.push(mailbox.name().unwrap_result("obtain mailbox name"));
|
path.push(name);
|
||||||
results.insert(path, mailbox);
|
results.insert(path, mailbox);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -927,6 +956,7 @@ fn build_mailbox_tree(
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
debug_assert_eq!(results.len(), mailboxes.len());
|
||||||
|
|
||||||
results
|
results
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ use jmap_client::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
|
pub mod database;
|
||||||
pub mod export;
|
pub mod export;
|
||||||
pub mod import;
|
pub mod import;
|
||||||
pub mod queue;
|
pub mod queue;
|
||||||
|
|
|
@ -26,20 +26,34 @@ use jmap_proto::{
|
||||||
types::{collection::Collection, property::Property, value::Value},
|
types::{collection::Collection, property::Property, value::Value},
|
||||||
};
|
};
|
||||||
use store::{
|
use store::{
|
||||||
write::{assert::HashedValue, BatchBuilder},
|
write::{assert::HashedValue, BatchBuilder, Operation, ValueClass},
|
||||||
BitmapKey, ValueKey,
|
BitmapKey, Serialize, ValueKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{mailbox::set::SCHEMA, JMAP};
|
use crate::{auth::authenticate::AccountKey, mailbox::set::SCHEMA, JMAP};
|
||||||
|
|
||||||
impl JMAP {
|
impl JMAP {
|
||||||
pub async fn delete_account(&self, account_id: u32) -> store::Result<()> {
|
pub async fn delete_account(&self, account_name: &str, account_id: u32) -> store::Result<()> {
|
||||||
// Delete blobs
|
// Delete blobs
|
||||||
self.store.delete_account_blobs(account_id).await?;
|
self.store.delete_account_blobs(account_id).await?;
|
||||||
|
|
||||||
// Delete mailboxes
|
// Delete mailboxes
|
||||||
let mut batch = BatchBuilder::new();
|
let mut batch = BatchBuilder::new();
|
||||||
batch
|
batch
|
||||||
|
.with_account_id(u32::MAX)
|
||||||
|
.with_collection(Collection::Principal)
|
||||||
|
.op(Operation::Value {
|
||||||
|
class: ValueClass::Custom {
|
||||||
|
bytes: AccountKey::name_to_id(account_name),
|
||||||
|
},
|
||||||
|
set: None,
|
||||||
|
})
|
||||||
|
.op(Operation::Value {
|
||||||
|
class: ValueClass::Custom {
|
||||||
|
bytes: AccountKey::id_to_name(account_id),
|
||||||
|
},
|
||||||
|
set: None,
|
||||||
|
})
|
||||||
.with_account_id(account_id)
|
.with_account_id(account_id)
|
||||||
.with_collection(Collection::Mailbox);
|
.with_collection(Collection::Mailbox);
|
||||||
for mailbox_id in self
|
for mailbox_id in self
|
||||||
|
@ -73,4 +87,40 @@ impl JMAP {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn rename_account(
|
||||||
|
&self,
|
||||||
|
new_account_name: &str,
|
||||||
|
account_name: &str,
|
||||||
|
account_id: u32,
|
||||||
|
) -> store::Result<()> {
|
||||||
|
// Delete blobs
|
||||||
|
self.store.delete_account_blobs(account_id).await?;
|
||||||
|
|
||||||
|
// Delete mailboxes
|
||||||
|
let mut batch = BatchBuilder::new();
|
||||||
|
batch
|
||||||
|
.with_account_id(u32::MAX)
|
||||||
|
.with_collection(Collection::Principal)
|
||||||
|
.op(Operation::Value {
|
||||||
|
class: ValueClass::Custom {
|
||||||
|
bytes: AccountKey::name_to_id(account_name),
|
||||||
|
},
|
||||||
|
set: None,
|
||||||
|
})
|
||||||
|
.op(Operation::Value {
|
||||||
|
class: ValueClass::Custom {
|
||||||
|
bytes: AccountKey::name_to_id(new_account_name),
|
||||||
|
},
|
||||||
|
set: account_id.serialize().into(),
|
||||||
|
})
|
||||||
|
.op(Operation::Value {
|
||||||
|
class: ValueClass::Custom {
|
||||||
|
bytes: AccountKey::id_to_name(account_id),
|
||||||
|
},
|
||||||
|
set: new_account_name.serialize().into(),
|
||||||
|
});
|
||||||
|
self.store.write(batch.build()).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ use tokio::{
|
||||||
use utils::listener::{ServerInstance, SessionData, SessionManager};
|
use utils::listener::{ServerInstance, SessionData, SessionManager};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::oauth::OAuthMetadata,
|
auth::{oauth::OAuthMetadata, AccessToken},
|
||||||
blob::{DownloadResponse, UploadResponse},
|
blob::{DownloadResponse, UploadResponse},
|
||||||
services::state,
|
services::state,
|
||||||
websocket::upgrade::upgrade_websocket_connection,
|
websocket::upgrade::upgrade_websocket_connection,
|
||||||
|
@ -77,7 +77,7 @@ pub async fn parse_jmap_request(
|
||||||
|
|
||||||
match (path.next().unwrap_or(""), req.method()) {
|
match (path.next().unwrap_or(""), req.method()) {
|
||||||
("", &Method::POST) => {
|
("", &Method::POST) => {
|
||||||
return match fetch_body(&mut req, jmap.config.request_max_size)
|
return match fetch_body(&mut req, jmap.config.request_max_size, &access_token)
|
||||||
.await
|
.await
|
||||||
.ok_or_else(|| RequestError::limit(RequestLimitError::SizeRequest))
|
.ok_or_else(|| RequestError::limit(RequestLimitError::SizeRequest))
|
||||||
.and_then(|bytes| {
|
.and_then(|bytes| {
|
||||||
|
@ -127,7 +127,13 @@ pub async fn parse_jmap_request(
|
||||||
("upload", &Method::POST) => {
|
("upload", &Method::POST) => {
|
||||||
if let Some(account_id) = path.next().and_then(|p| Id::from_bytes(p.as_bytes()))
|
if let Some(account_id) = path.next().and_then(|p| Id::from_bytes(p.as_bytes()))
|
||||||
{
|
{
|
||||||
return match fetch_body(&mut req, jmap.config.upload_max_size).await {
|
return match fetch_body(
|
||||||
|
&mut req,
|
||||||
|
jmap.config.upload_max_size,
|
||||||
|
&access_token,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Some(bytes) => {
|
Some(bytes) => {
|
||||||
match jmap
|
match jmap
|
||||||
.blob_upload(
|
.blob_upload(
|
||||||
|
@ -245,23 +251,77 @@ pub async fn parse_jmap_request(
|
||||||
req.method(),
|
req.method(),
|
||||||
) {
|
) {
|
||||||
("account", "delete", &Method::GET) => {
|
("account", "delete", &Method::GET) => {
|
||||||
return if let Some(account_id) = path.next().and_then(|s| s.parse::<u32>().ok())
|
return if let Some(account_name) = path.next() {
|
||||||
{
|
if let Ok(Some(account_id)) = jmap.try_get_account_id(account_name).await {
|
||||||
match jmap.delete_account(account_id).await {
|
match jmap.delete_account(account_name, account_id).await {
|
||||||
Ok(_) => JsonResponse::new(Value::String("success".into()))
|
Ok(_) => JsonResponse::new(Value::String("success".into()))
|
||||||
|
.into_http_response(),
|
||||||
|
Err(err) => RequestError::blank(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||||
|
"Account deletion failed",
|
||||||
|
err.to_string(),
|
||||||
|
)
|
||||||
.into_http_response(),
|
.into_http_response(),
|
||||||
Err(err) => RequestError::blank(
|
}
|
||||||
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
} else {
|
||||||
"Account deletion failed",
|
RequestError::blank(
|
||||||
err.to_string(),
|
StatusCode::NOT_FOUND.as_u16(),
|
||||||
|
"Not found",
|
||||||
|
"Account not found.",
|
||||||
)
|
)
|
||||||
.into_http_response(),
|
.into_http_response()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
RequestError::blank(
|
RequestError::blank(
|
||||||
StatusCode::BAD_REQUEST.as_u16(),
|
StatusCode::BAD_REQUEST.as_u16(),
|
||||||
"Invalid parameters",
|
"Invalid parameters",
|
||||||
"Expected account id",
|
"Expected account name",
|
||||||
|
)
|
||||||
|
.into_http_response()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
("account", "rename", &Method::GET) => {
|
||||||
|
return if let (Some(account_name), Some(new_account_name)) =
|
||||||
|
(path.next(), path.next())
|
||||||
|
{
|
||||||
|
match (
|
||||||
|
jmap.try_get_account_id(account_name).await,
|
||||||
|
jmap.try_get_account_id(new_account_name).await,
|
||||||
|
) {
|
||||||
|
(Ok(Some(account_id)), Ok(None)) => {
|
||||||
|
match jmap
|
||||||
|
.rename_account(new_account_name, account_name, account_id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => JsonResponse::new(Value::String("success".into()))
|
||||||
|
.into_http_response(),
|
||||||
|
Err(err) => RequestError::blank(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||||
|
"Account rename failed",
|
||||||
|
err.to_string(),
|
||||||
|
)
|
||||||
|
.into_http_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Ok(None), _) => RequestError::blank(
|
||||||
|
StatusCode::NOT_FOUND.as_u16(),
|
||||||
|
"Not found",
|
||||||
|
"Account not found.",
|
||||||
|
)
|
||||||
|
.into_http_response(),
|
||||||
|
(_, Ok(Some(_))) => RequestError::blank(
|
||||||
|
StatusCode::BAD_REQUEST.as_u16(),
|
||||||
|
"Invalid parameters",
|
||||||
|
"New account name already exists.",
|
||||||
|
)
|
||||||
|
.into_http_response(),
|
||||||
|
_ => RequestError::internal_server_error().into_http_response(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RequestError::blank(
|
||||||
|
StatusCode::BAD_REQUEST.as_u16(),
|
||||||
|
"Invalid parameters",
|
||||||
|
"Expected old and new account names",
|
||||||
)
|
)
|
||||||
.into_http_response()
|
.into_http_response()
|
||||||
};
|
};
|
||||||
|
@ -374,11 +434,16 @@ async fn handle_request<T: AsyncRead + AsyncWrite + Unpin + Send + 'static>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_body(req: &mut HttpRequest, max_size: usize) -> Option<Vec<u8>> {
|
pub async fn fetch_body(
|
||||||
|
req: &mut HttpRequest,
|
||||||
|
max_size: usize,
|
||||||
|
access_token: &AccessToken,
|
||||||
|
) -> Option<Vec<u8>> {
|
||||||
let mut bytes = Vec::with_capacity(1024);
|
let mut bytes = Vec::with_capacity(1024);
|
||||||
while let Some(Ok(frame)) = req.frame().await {
|
while let Some(Ok(frame)) = req.frame().await {
|
||||||
if let Some(data) = frame.data_ref() {
|
if let Some(data) = frame.data_ref() {
|
||||||
if bytes.len() + data.len() <= max_size {
|
if bytes.len() + data.len() <= max_size || max_size == 0 || access_token.is_super_user()
|
||||||
|
{
|
||||||
bytes.extend_from_slice(data);
|
bytes.extend_from_slice(data);
|
||||||
} else {
|
} else {
|
||||||
return None;
|
return None;
|
||||||
|
|
|
@ -153,32 +153,29 @@ impl JMAP {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn try_get_account_id(&self, name: &str) -> Result<Option<u32>, MethodError> {
|
||||||
|
self.store
|
||||||
|
.get_value::<u32>(CustomValueKey {
|
||||||
|
value: AccountKey::name_to_id(name),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
tracing::error!(event = "error",
|
||||||
|
context = "store",
|
||||||
|
account_name = name,
|
||||||
|
error = ?err,
|
||||||
|
"Failed to retrieve account id");
|
||||||
|
MethodError::ServerPartialFail
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_account_id(&self, name: &str) -> Result<u32, MethodError> {
|
pub async fn get_account_id(&self, name: &str) -> Result<u32, MethodError> {
|
||||||
let mut try_count = 0;
|
let mut try_count = 0;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Try to obtain ID
|
// Try to obtain ID
|
||||||
match self
|
if let Some(account_id) = self.try_get_account_id(name).await? {
|
||||||
.store
|
return Ok(account_id);
|
||||||
.get_value::<u32>(CustomValueKey {
|
|
||||||
value: KeySerializer::new(name.len() + std::mem::size_of::<u32>() + 1)
|
|
||||||
.write(u32::MAX)
|
|
||||||
.write(0u8)
|
|
||||||
.write(name)
|
|
||||||
.finalize(),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(Some(id)) => return Ok(id),
|
|
||||||
Ok(None) => {}
|
|
||||||
Err(err) => {
|
|
||||||
tracing::error!(event = "error",
|
|
||||||
context = "store",
|
|
||||||
account_name = name,
|
|
||||||
error = ?err,
|
|
||||||
"Failed to retrieve account id");
|
|
||||||
return Err(MethodError::ServerPartialFail);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign new ID
|
// Assign new ID
|
||||||
|
@ -187,11 +184,7 @@ impl JMAP {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Serialize key
|
// Serialize key
|
||||||
let key = KeySerializer::new(name.len() + std::mem::size_of::<u32>() + 1)
|
let key = AccountKey::name_to_id(name);
|
||||||
.write(u32::MAX)
|
|
||||||
.write(0u8)
|
|
||||||
.write(name)
|
|
||||||
.finalize();
|
|
||||||
|
|
||||||
// Write account ID
|
// Write account ID
|
||||||
let mut batch = BatchBuilder::new();
|
let mut batch = BatchBuilder::new();
|
||||||
|
@ -206,11 +199,7 @@ impl JMAP {
|
||||||
})
|
})
|
||||||
.op(Operation::Value {
|
.op(Operation::Value {
|
||||||
class: ValueClass::Custom {
|
class: ValueClass::Custom {
|
||||||
bytes: KeySerializer::new(std::mem::size_of::<u32>() * 2 + 1)
|
bytes: AccountKey::id_to_name(account_id),
|
||||||
.write(u32::MAX)
|
|
||||||
.write(1u8)
|
|
||||||
.write(account_id)
|
|
||||||
.finalize(),
|
|
||||||
},
|
},
|
||||||
set: name.serialize().into(),
|
set: name.serialize().into(),
|
||||||
});
|
});
|
||||||
|
@ -249,11 +238,7 @@ impl JMAP {
|
||||||
pub async fn get_account_name(&self, account_id: u32) -> Result<Option<String>, MethodError> {
|
pub async fn get_account_name(&self, account_id: u32) -> Result<Option<String>, MethodError> {
|
||||||
self.store
|
self.store
|
||||||
.get_value::<String>(CustomValueKey {
|
.get_value::<String>(CustomValueKey {
|
||||||
value: KeySerializer::new(std::mem::size_of::<u32>() * 2 + 1)
|
value: AccountKey::id_to_name(account_id),
|
||||||
.write(u32::MAX)
|
|
||||||
.write(1u8)
|
|
||||||
.write(account_id)
|
|
||||||
.finalize(),
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
|
@ -333,3 +318,22 @@ impl JMAP {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AccountKey();
|
||||||
|
|
||||||
|
impl AccountKey {
|
||||||
|
pub fn name_to_id(name: &str) -> Vec<u8> {
|
||||||
|
KeySerializer::new(name.len() + std::mem::size_of::<u32>() + 1)
|
||||||
|
.write(u32::MAX)
|
||||||
|
.write(0u8)
|
||||||
|
.write(name)
|
||||||
|
.finalize()
|
||||||
|
}
|
||||||
|
pub fn id_to_name(id: u32) -> Vec<u8> {
|
||||||
|
KeySerializer::new(std::mem::size_of::<u32>() * 2 + 1)
|
||||||
|
.write(u32::MAX)
|
||||||
|
.write(1u8)
|
||||||
|
.write(id)
|
||||||
|
.finalize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -23,13 +23,11 @@
|
||||||
|
|
||||||
use std::{collections::HashMap, sync::atomic::AtomicU32};
|
use std::{collections::HashMap, sync::atomic::AtomicU32};
|
||||||
|
|
||||||
|
use http_body_util::BodyExt;
|
||||||
use hyper::{header::CONTENT_TYPE, StatusCode};
|
use hyper::{header::CONTENT_TYPE, StatusCode};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::api::{
|
use crate::api::{http::ToHttpResponse, HtmlResponse, HttpRequest, HttpResponse};
|
||||||
http::{fetch_body, ToHttpResponse},
|
|
||||||
HtmlResponse, HttpRequest, HttpResponse,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub mod device_auth;
|
pub mod device_auth;
|
||||||
pub mod token;
|
pub mod token;
|
||||||
|
@ -221,7 +219,7 @@ pub async fn parse_form_data(
|
||||||
.get(CONTENT_TYPE)
|
.get(CONTENT_TYPE)
|
||||||
.and_then(|h| h.to_str().ok())
|
.and_then(|h| h.to_str().ok())
|
||||||
.and_then(|val| val.parse::<mime::Mime>().ok()),
|
.and_then(|val| val.parse::<mime::Mime>().ok()),
|
||||||
fetch_body(req, 2048).await,
|
fetch_body(req).await,
|
||||||
) {
|
) {
|
||||||
(Some(content_type), Some(body)) => {
|
(Some(content_type), Some(body)) => {
|
||||||
let mut fields = HashMap::new();
|
let mut fields = HashMap::new();
|
||||||
|
@ -245,3 +243,17 @@ pub async fn parse_form_data(
|
||||||
.into_http_response()),
|
.into_http_response()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_body(req: &mut HttpRequest) -> Option<Vec<u8>> {
|
||||||
|
let mut bytes = Vec::with_capacity(1024);
|
||||||
|
while let Some(Ok(frame)) = req.frame().await {
|
||||||
|
if let Some(data) = frame.data_ref() {
|
||||||
|
if bytes.len() + data.len() <= 2048 {
|
||||||
|
bytes.extend_from_slice(data);
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bytes.into()
|
||||||
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ use store::{
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::{acl::EffectiveAcl, AccessToken},
|
auth::{acl::EffectiveAcl, AccessToken},
|
||||||
JMAP, SUPERUSER_ID,
|
JMAP,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{INBOX_ID, TRASH_ID};
|
use super::{INBOX_ID, TRASH_ID};
|
||||||
|
@ -755,7 +755,7 @@ impl JMAP {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "test_mode")]
|
#[cfg(feature = "test_mode")]
|
||||||
if mailbox_ids.is_empty() && account_id == SUPERUSER_ID {
|
if mailbox_ids.is_empty() && account_id == crate::SUPERUSER_ID {
|
||||||
return Ok(mailbox_ids);
|
return Ok(mailbox_ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,15 +53,14 @@ impl JMAP {
|
||||||
.map_err(|_| MethodError::ServerPartialFail)?
|
.map_err(|_| MethodError::ServerPartialFail)?
|
||||||
{
|
{
|
||||||
let account_id = self.get_account_id(&principal.name).await?;
|
let account_id = self.get_account_id(&principal.name).await?;
|
||||||
if is_set {
|
if is_set || result_set.results.contains(account_id) {
|
||||||
result_set.results =
|
result_set.results =
|
||||||
RoaringBitmap::from_sorted_iter([account_id]).unwrap();
|
RoaringBitmap::from_sorted_iter([account_id]).unwrap();
|
||||||
} else if result_set.results.contains(account_id) {
|
|
||||||
result_set.results.remove(account_id);
|
|
||||||
} else {
|
} else {
|
||||||
result_set.results = RoaringBitmap::new();
|
result_set.results = RoaringBitmap::new();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
result_set.results = RoaringBitmap::new();
|
||||||
}
|
}
|
||||||
is_set = false;
|
is_set = false;
|
||||||
}
|
}
|
||||||
|
@ -77,6 +76,7 @@ impl JMAP {
|
||||||
}
|
}
|
||||||
if is_set {
|
if is_set {
|
||||||
result_set.results = ids;
|
result_set.results = ids;
|
||||||
|
is_set = false;
|
||||||
} else {
|
} else {
|
||||||
result_set.results &= ids;
|
result_set.results &= ids;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,10 +21,12 @@
|
||||||
* for more details.
|
* for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{
|
||||||
|
sync::Arc,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
use chrono::{Datelike, TimeZone, Timelike};
|
use chrono::{Datelike, TimeZone, Timelike};
|
||||||
use store::write::now;
|
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use utils::{config::Config, failed, map::ttl_dashmap::TtlMap, UnwrapFailure};
|
use utils::{config::Config, failed, map::ttl_dashmap::TtlMap, UnwrapFailure};
|
||||||
|
|
||||||
|
@ -75,7 +77,7 @@ pub fn spawn_housekeeper(core: Arc<JMAP>, settings: &Config, mut rx: mpsc::Recei
|
||||||
purge_cache.time_to_next(),
|
purge_cache.time_to_next(),
|
||||||
];
|
];
|
||||||
let mut tasks_to_run = [false, false, false];
|
let mut tasks_to_run = [false, false, false];
|
||||||
let start_time = now();
|
let start_time = Instant::now();
|
||||||
|
|
||||||
match tokio::time::timeout(time_to_next.iter().min().copied().unwrap(), rx.recv()).await
|
match tokio::time::timeout(time_to_next.iter().min().copied().unwrap(), rx.recv()).await
|
||||||
{
|
{
|
||||||
|
@ -96,9 +98,9 @@ pub fn spawn_housekeeper(core: Arc<JMAP>, settings: &Config, mut rx: mpsc::Recei
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check which tasks are due for execution
|
// Check which tasks are due for execution
|
||||||
let now = now();
|
let now = Instant::now();
|
||||||
for (pos, time_to_next) in time_to_next.into_iter().enumerate() {
|
for (pos, time_to_next) in time_to_next.into_iter().enumerate() {
|
||||||
if start_time + time_to_next.as_secs() <= now {
|
if start_time + time_to_next <= now {
|
||||||
tasks_to_run[pos] = true;
|
tasks_to_run[pos] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,7 +108,7 @@ impl ReadTransaction<'_> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(sorted_results)
|
Ok(sorted_results)
|
||||||
} else {
|
} else if comparators.len() > 1 {
|
||||||
//TODO improve this algorithm, avoid re-sorting in memory.
|
//TODO improve this algorithm, avoid re-sorting in memory.
|
||||||
let mut sorted_ids = AHashMap::with_capacity(paginate.limit);
|
let mut sorted_ids = AHashMap::with_capacity(paginate.limit);
|
||||||
|
|
||||||
|
@ -208,6 +208,33 @@ impl ReadTransaction<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(paginate.build())
|
||||||
|
} else {
|
||||||
|
let mut seen_prefixes = AHashSet::new();
|
||||||
|
for document_id in result_set.results {
|
||||||
|
// Obtain document prefixId
|
||||||
|
let prefix_id = if let Some(prefix_key) = &paginate.prefix_key {
|
||||||
|
if let Some(prefix_id) = self
|
||||||
|
.get_value(prefix_key.with_document_id(document_id))
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
if paginate.prefix_unique && !seen_prefixes.insert(prefix_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
prefix_id
|
||||||
|
} else {
|
||||||
|
// Document no longer exists?
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add document to results
|
||||||
|
if !paginate.add(prefix_id, document_id) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(paginate.build())
|
Ok(paginate.build())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ hostname = "test.example.org"
|
||||||
|
|
||||||
[server.listener.jmap]
|
[server.listener.jmap]
|
||||||
bind = ["127.0.0.1:9990"]
|
bind = ["127.0.0.1:9990"]
|
||||||
url = "https://127.0.0.1:8899"
|
url = "https://127.0.0.1:9990"
|
||||||
protocol = "jmap"
|
protocol = "jmap"
|
||||||
max-connections = 8192
|
max-connections = 8192
|
||||||
|
|
||||||
|
@ -97,8 +97,8 @@ type = "local"
|
||||||
path = "/tmp/stalwart-test"
|
path = "/tmp/stalwart-test"
|
||||||
|
|
||||||
[certificate.default]
|
[certificate.default]
|
||||||
cert = "file://./tests/resources/tls_cert.pem"
|
cert = "file://../../tests/resources/tls_cert.pem"
|
||||||
private-key = "file://./tests/resources/tls_privatekey.pem"
|
private-key = "file://../../tests/resources/tls_privatekey.pem"
|
||||||
|
|
||||||
[jmap]
|
[jmap]
|
||||||
directory = "local"
|
directory = "local"
|
||||||
|
@ -146,7 +146,8 @@ domains = ["example.org"]
|
||||||
[[directory."local".users]]
|
[[directory."local".users]]
|
||||||
name = "admin"
|
name = "admin"
|
||||||
description = "Superadmin"
|
description = "Superadmin"
|
||||||
secret = "donotuse"
|
secret = "secret"
|
||||||
|
member-of = ["superusers"]
|
||||||
|
|
||||||
[[directory."local".users]]
|
[[directory."local".users]]
|
||||||
name = "john"
|
name = "john"
|
||||||
|
@ -180,6 +181,10 @@ description = "Sales Team"
|
||||||
name = "support"
|
name = "support"
|
||||||
description = "Support Team"
|
description = "Support Team"
|
||||||
|
|
||||||
|
[[directory."local".groups]]
|
||||||
|
name = "superusers"
|
||||||
|
description = "Superusers"
|
||||||
|
|
||||||
[oauth]
|
[oauth]
|
||||||
key = "parerga_und_paralipomena"
|
key = "parerga_und_paralipomena"
|
||||||
max-auth-attempts = 1
|
max-auth-attempts = 1
|
||||||
|
|
Loading…
Reference in a new issue