209 lines
6.9 KiB
Rust
209 lines
6.9 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*/
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
use super::error::{SeedError, SeedResult};
|
|
use super::jmap::Jmap;
|
|
|
|
pub const STALWART: &[&str] = &["urn:ietf:params:jmap:core", "urn:stalwart:jmap"];
|
|
|
|
pub struct Admin {
|
|
jmap: Jmap,
|
|
account_id: String,
|
|
}
|
|
|
|
impl Admin {
|
|
pub fn connect(base: &str, user: &str, password: &str) -> SeedResult<Admin> {
|
|
let jmap = Jmap::connect(base, user, password)?;
|
|
let account_id = jmap.account_id.clone();
|
|
Ok(Admin { jmap, account_id })
|
|
}
|
|
|
|
fn created_id(response: &Value, key: &str) -> SeedResult<String> {
|
|
response
|
|
.get("created")
|
|
.and_then(|c| c.get(key))
|
|
.and_then(|o| o.get("id"))
|
|
.and_then(Value::as_str)
|
|
.map(str::to_owned)
|
|
.ok_or_else(|| SeedError::Shape(format!("create {key} missing id: {response}")))
|
|
}
|
|
|
|
pub fn domain_id(&self, name: &str) -> SeedResult<Option<String>> {
|
|
let calls = json!([
|
|
["x:Domain/query", { "accountId": self.account_id, "filter": { "name": name } }, "q"],
|
|
["x:Domain/get", {
|
|
"accountId": self.account_id,
|
|
"#ids": { "resultOf": "q", "name": "x:Domain/query", "path": "/ids" },
|
|
"properties": ["id", "name"]
|
|
}, "g"]
|
|
]);
|
|
let parsed = self.jmap.request(STALWART, calls)?;
|
|
let responses = parsed
|
|
.get("methodResponses")
|
|
.and_then(Value::as_array)
|
|
.ok_or_else(|| SeedError::Shape("missing methodResponses".to_owned()))?;
|
|
let get = responses
|
|
.get(1)
|
|
.and_then(|r| r.get(1))
|
|
.ok_or_else(|| SeedError::Shape("missing x:Domain/get".to_owned()))?;
|
|
let list = get.get("list").and_then(Value::as_array);
|
|
if let Some(list) = list {
|
|
for entry in list {
|
|
if entry.get("name").and_then(Value::as_str) == Some(name)
|
|
&& let Some(id) = entry.get("id").and_then(Value::as_str)
|
|
{
|
|
return Ok(Some(id.to_owned()));
|
|
}
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|
|
|
|
pub fn ensure_domain(&self, name: &str) -> SeedResult<String> {
|
|
if let Some(id) = self.domain_id(name)? {
|
|
return Ok(id);
|
|
}
|
|
let response = self.jmap.set_create(
|
|
STALWART,
|
|
"x:Domain/set",
|
|
&self.account_id,
|
|
json!({ "v": { "name": name } }),
|
|
&[],
|
|
)?;
|
|
Self::created_id(&response, "v")
|
|
}
|
|
|
|
fn accounts_in_domain(&self, domain_id: &str) -> SeedResult<Vec<String>> {
|
|
let response = self.jmap.call(
|
|
STALWART,
|
|
"x:Account/get",
|
|
&self.account_id,
|
|
json!({ "properties": ["id", "domainId"] }),
|
|
)?;
|
|
let mut ids = Vec::new();
|
|
if let Some(list) = response.get("list").and_then(Value::as_array) {
|
|
for entry in list {
|
|
if entry.get("domainId").and_then(Value::as_str) == Some(domain_id)
|
|
&& let Some(id) = entry.get("id").and_then(Value::as_str)
|
|
{
|
|
ids.push(id.to_owned());
|
|
}
|
|
}
|
|
}
|
|
Ok(ids)
|
|
}
|
|
|
|
fn destroy(&self, method: &str, ids: &[String]) -> SeedResult<()> {
|
|
if ids.is_empty() {
|
|
return Ok(());
|
|
}
|
|
self.jmap.call(
|
|
STALWART,
|
|
method,
|
|
&self.account_id,
|
|
json!({ "destroy": ids }),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn invalidate_caches(&self) -> SeedResult<()> {
|
|
self.jmap.call(
|
|
STALWART,
|
|
"x:Action/set",
|
|
&self.account_id,
|
|
json!({ "create": { "c": { "@type": "InvalidateCaches" } } }),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn teardown_domain(&self, name: &str) -> SeedResult<()> {
|
|
let Some(domain_id) = self.domain_id(name)? else {
|
|
return Ok(());
|
|
};
|
|
let accounts = self.accounts_in_domain(&domain_id)?;
|
|
self.destroy("x:Account/set", &accounts)?;
|
|
for _ in 0..6 {
|
|
let response = self.jmap.call(
|
|
STALWART,
|
|
"x:Domain/set",
|
|
&self.account_id,
|
|
json!({ "destroy": [domain_id] }),
|
|
)?;
|
|
if response
|
|
.get("destroyed")
|
|
.and_then(Value::as_array)
|
|
.map(|d| d.iter().any(|v| v.as_str() == Some(domain_id.as_str())))
|
|
.unwrap_or(false)
|
|
{
|
|
return Ok(());
|
|
}
|
|
let linked = response
|
|
.get("notDestroyed")
|
|
.and_then(|n| n.get(&domain_id))
|
|
.and_then(|e| e.get("linkedObjects"))
|
|
.and_then(Value::as_array)
|
|
.cloned()
|
|
.unwrap_or_default();
|
|
if linked.is_empty() {
|
|
return Err(SeedError::Method {
|
|
method: "x:Domain/set".to_owned(),
|
|
detail: format!("domain {name} not destroyed: {response}"),
|
|
});
|
|
}
|
|
let mut dkim = Vec::new();
|
|
let mut accounts = Vec::new();
|
|
for obj in &linked {
|
|
let kind = obj
|
|
.get("object")
|
|
.and_then(Value::as_str)
|
|
.unwrap_or_default();
|
|
let Some(id) = obj.get("id").and_then(Value::as_str) else {
|
|
continue;
|
|
};
|
|
match kind {
|
|
"DkimSignature" => dkim.push(id.to_owned()),
|
|
"Account" => accounts.push(id.to_owned()),
|
|
_ => {}
|
|
}
|
|
}
|
|
self.destroy("x:Account/set", &accounts)?;
|
|
self.destroy("x:DkimSignature/set", &dkim)?;
|
|
}
|
|
Err(SeedError::Method {
|
|
method: "x:Domain/set".to_owned(),
|
|
detail: format!("domain {name} still linked after retries"),
|
|
})
|
|
}
|
|
|
|
pub fn create_account(
|
|
&self,
|
|
localpart: &str,
|
|
domain_id: &str,
|
|
password: &str,
|
|
admin_role: bool,
|
|
) -> SeedResult<String> {
|
|
let role = if admin_role { "Admin" } else { "User" };
|
|
let create = json!({
|
|
"a": {
|
|
"@type": "User",
|
|
"name": localpart,
|
|
"domainId": domain_id,
|
|
"credentials": { "0": { "@type": "Password", "secret": password } },
|
|
"encryptionAtRest": { "@type": "Disabled" },
|
|
"permissions": { "@type": "Inherit" },
|
|
"roles": { "@type": role },
|
|
"locale": "en_US"
|
|
}
|
|
});
|
|
let response =
|
|
self.jmap
|
|
.set_create(STALWART, "x:Account/set", &self.account_id, create, &[])?;
|
|
Self::created_id(&response, "a")
|
|
}
|
|
}
|