Initial commit
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user