Some refactoring
This commit is contained in:
parent
1ac56a9198
commit
6be90936a1
9 changed files with 173 additions and 82 deletions
10
src/bayou.rs
10
src/bayou.rs
|
@ -57,18 +57,16 @@ pub struct Bayou<S: BayouState> {
|
||||||
|
|
||||||
impl<S: BayouState> Bayou<S> {
|
impl<S: BayouState> Bayou<S> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
k2v_region: &Region,
|
|
||||||
s3_region: &Region,
|
|
||||||
creds: &Credentials,
|
creds: &Credentials,
|
||||||
path: String,
|
path: String,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let k2v_client = creds.k2v_client(k2v_region)?;
|
let k2v_client = creds.k2v_client()?;
|
||||||
let s3_client = creds.s3_client(s3_region)?;
|
let s3_client = creds.s3_client()?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
bucket: creds.bucket.clone(),
|
bucket: creds.bucket().to_string(),
|
||||||
path,
|
path,
|
||||||
key: creds.master_key.clone(),
|
key: creds.keys.master.clone(),
|
||||||
k2v: k2v_client,
|
k2v: k2v_client,
|
||||||
s3: s3_client,
|
s3: s3_client,
|
||||||
checkpoint: (Timestamp::zero(), S::default()),
|
checkpoint: (Timestamp::zero(), S::default()),
|
||||||
|
|
|
@ -25,10 +25,13 @@ pub struct LoginStaticConfig {
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct LoginStaticUser {
|
pub struct LoginStaticUser {
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
|
||||||
pub aws_access_key_id: String,
|
pub aws_access_key_id: String,
|
||||||
pub aws_secret_access_key: String,
|
pub aws_secret_access_key: String,
|
||||||
pub bucket: Option<String>,
|
pub bucket: Option<String>,
|
||||||
|
|
||||||
pub master_key: Option<String>,
|
pub master_key: Option<String>,
|
||||||
|
pub secret_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
|
|
@ -5,17 +5,22 @@ use anyhow::{anyhow, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use zstd::stream::{decode_all as zstd_decode, encode_all as zstd_encode};
|
use zstd::stream::{decode_all as zstd_decode, encode_all as zstd_encode};
|
||||||
|
|
||||||
use sodiumoxide::crypto::secretbox::xsalsa20poly1305::{self, gen_nonce, Nonce, NONCEBYTES};
|
use sodiumoxide::crypto::secretbox::xsalsa20poly1305 as secretbox;
|
||||||
|
use sodiumoxide::crypto::box_ as publicbox;
|
||||||
|
|
||||||
pub use sodiumoxide::crypto::secretbox::xsalsa20poly1305::{gen_key, Key, KEYBYTES};
|
pub use sodiumoxide::crypto::secretbox::xsalsa20poly1305::{gen_key, Key, KEYBYTES};
|
||||||
|
pub use sodiumoxide::crypto::box_::{gen_keypair, PublicKey, SecretKey, PUBLICKEYBYTES, SECRETKEYBYTES};
|
||||||
|
|
||||||
pub fn open(cryptoblob: &[u8], key: &Key) -> Result<Vec<u8>> {
|
pub fn open(cryptoblob: &[u8], key: &Key) -> Result<Vec<u8>> {
|
||||||
|
use secretbox::{NONCEBYTES, Nonce};
|
||||||
|
|
||||||
if cryptoblob.len() < NONCEBYTES {
|
if cryptoblob.len() < NONCEBYTES {
|
||||||
return Err(anyhow!("Cyphertext too short"));
|
return Err(anyhow!("Cyphertext too short"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt -> get Zstd data
|
// Decrypt -> get Zstd data
|
||||||
let nonce = Nonce::from_slice(&cryptoblob[..NONCEBYTES]).unwrap();
|
let nonce = Nonce::from_slice(&cryptoblob[..NONCEBYTES]).unwrap();
|
||||||
let zstdblob = xsalsa20poly1305::open(&cryptoblob[NONCEBYTES..], &nonce, key)
|
let zstdblob = secretbox::open(&cryptoblob[NONCEBYTES..], &nonce, key)
|
||||||
.map_err(|_| anyhow!("Could not decrypt blob"))?;
|
.map_err(|_| anyhow!("Could not decrypt blob"))?;
|
||||||
|
|
||||||
// Decompress zstd data
|
// Decompress zstd data
|
||||||
|
@ -26,13 +31,15 @@ pub fn open(cryptoblob: &[u8], key: &Key) -> Result<Vec<u8>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn seal(plainblob: &[u8], key: &Key) -> Result<Vec<u8>> {
|
pub fn seal(plainblob: &[u8], key: &Key) -> Result<Vec<u8>> {
|
||||||
|
use secretbox::{NONCEBYTES, gen_nonce};
|
||||||
|
|
||||||
// Compress data using zstd
|
// Compress data using zstd
|
||||||
let mut reader = &plainblob[..];
|
let mut reader = &plainblob[..];
|
||||||
let zstdblob = zstd_encode(&mut reader, 0)?;
|
let zstdblob = zstd_encode(&mut reader, 0)?;
|
||||||
|
|
||||||
// Encrypt
|
// Encrypt
|
||||||
let nonce = gen_nonce();
|
let nonce = gen_nonce();
|
||||||
let cryptoblob = xsalsa20poly1305::seal(&zstdblob, &nonce, key);
|
let cryptoblob = secretbox::seal(&zstdblob, &nonce, key);
|
||||||
|
|
||||||
let mut res = Vec::with_capacity(NONCEBYTES + cryptoblob.len());
|
let mut res = Vec::with_capacity(NONCEBYTES + cryptoblob.len());
|
||||||
res.extend(nonce.as_ref());
|
res.extend(nonce.as_ref());
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use rusoto_signature::Region;
|
||||||
|
|
||||||
use crate::config::*;
|
use crate::config::*;
|
||||||
use crate::login::*;
|
use crate::login::*;
|
||||||
|
@ -9,7 +10,7 @@ pub struct LdapLoginProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LdapLoginProvider {
|
impl LdapLoginProvider {
|
||||||
pub fn new(_config: LoginLdapConfig) -> Result<Self> {
|
pub fn new(_config: LoginLdapConfig, _k2v_region: Region, _s3_region: Region) -> Result<Self> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ use rusoto_credential::{AwsCredentials, StaticProvider};
|
||||||
use rusoto_s3::S3Client;
|
use rusoto_s3::S3Client;
|
||||||
use rusoto_signature::Region;
|
use rusoto_signature::Region;
|
||||||
|
|
||||||
use crate::cryptoblob::Key as SymmetricKey;
|
use crate::cryptoblob::*;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait LoginProvider {
|
pub trait LoginProvider {
|
||||||
|
@ -18,14 +18,51 @@ pub trait LoginProvider {
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Credentials {
|
pub struct Credentials {
|
||||||
|
pub storage: StorageCredentials,
|
||||||
|
pub keys: CryptoKeys,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct StorageCredentials {
|
||||||
|
pub s3_region: Region,
|
||||||
|
pub k2v_region: Region,
|
||||||
|
|
||||||
pub aws_access_key_id: String,
|
pub aws_access_key_id: String,
|
||||||
pub aws_secret_access_key: String,
|
pub aws_secret_access_key: String,
|
||||||
pub bucket: String,
|
pub bucket: String,
|
||||||
pub master_key: SymmetricKey,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct CryptoKeys {
|
||||||
|
// Master key for symmetric encryption of mailbox data
|
||||||
|
pub master: Key,
|
||||||
|
// Public/private keypair for encryption of incomming emails
|
||||||
|
pub secret: SecretKey,
|
||||||
|
pub public: PublicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----
|
||||||
|
|
||||||
impl Credentials {
|
impl Credentials {
|
||||||
pub fn k2v_client(&self, k2v_region: &Region) -> Result<K2vClient> {
|
pub fn k2v_client(&self) -> Result<K2vClient> {
|
||||||
|
self.storage.k2v_client()
|
||||||
|
}
|
||||||
|
pub fn s3_client(&self) -> Result<S3Client> {
|
||||||
|
self.storage.s3_client()
|
||||||
|
}
|
||||||
|
pub fn bucket(&self) -> &str {
|
||||||
|
self.storage.bucket.as_str()
|
||||||
|
}
|
||||||
|
pub fn dump_config(&self) {
|
||||||
|
println!("aws_access_key_id = \"{}\"", self.storage.aws_access_key_id);
|
||||||
|
println!("aws_secret_access_key = \"{}\"", self.storage.aws_secret_access_key);
|
||||||
|
println!("master_key = \"{}\"", base64::encode(&self.keys.master));
|
||||||
|
println!("secret_key = \"{}\"", base64::encode(&self.keys.secret));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StorageCredentials {
|
||||||
|
pub fn k2v_client(&self) -> Result<K2vClient> {
|
||||||
let aws_creds = AwsCredentials::new(
|
let aws_creds = AwsCredentials::new(
|
||||||
self.aws_access_key_id.clone(),
|
self.aws_access_key_id.clone(),
|
||||||
self.aws_secret_access_key.clone(),
|
self.aws_secret_access_key.clone(),
|
||||||
|
@ -34,14 +71,14 @@ impl Credentials {
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(K2vClient::new(
|
Ok(K2vClient::new(
|
||||||
k2v_region.clone(),
|
self.k2v_region.clone(),
|
||||||
self.bucket.clone(),
|
self.bucket.clone(),
|
||||||
aws_creds,
|
aws_creds,
|
||||||
None,
|
None,
|
||||||
)?)
|
)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn s3_client(&self, s3_region: &Region) -> Result<S3Client> {
|
pub fn s3_client(&self) -> Result<S3Client> {
|
||||||
let aws_creds_provider = StaticProvider::new_minimal(
|
let aws_creds_provider = StaticProvider::new_minimal(
|
||||||
self.aws_access_key_id.clone(),
|
self.aws_access_key_id.clone(),
|
||||||
self.aws_secret_access_key.clone(),
|
self.aws_secret_access_key.clone(),
|
||||||
|
@ -50,7 +87,34 @@ impl Credentials {
|
||||||
Ok(S3Client::new_with(
|
Ok(S3Client::new_with(
|
||||||
HttpClient::new()?,
|
HttpClient::new()?,
|
||||||
aws_creds_provider,
|
aws_creds_provider,
|
||||||
s3_region.clone(),
|
self.s3_region.clone(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CryptoKeys {
|
||||||
|
pub fn init(storage: &StorageCredentials) -> Result<Self> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_without_password(storage: &StorageCredentials, master_key: &Key, secret_key: &SecretKey) -> Result<Self> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open(storage: &StorageCredentials, password: &str) -> Result<Self> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_without_password(storage: &StorageCredentials, master_key: &Key, secret_key: &SecretKey) -> Result<Self> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_password(&self, storage: &StorageCredentials, password: &str) -> Result<()> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_password(&self, storage: &StorageCredentials, password: &str, allow_remove_all: bool) -> Result<()> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,21 +5,23 @@ use async_trait::async_trait;
|
||||||
use rusoto_signature::Region;
|
use rusoto_signature::Region;
|
||||||
|
|
||||||
use crate::config::*;
|
use crate::config::*;
|
||||||
use crate::cryptoblob::Key;
|
use crate::cryptoblob::{Key, SecretKey};
|
||||||
use crate::login::*;
|
use crate::login::*;
|
||||||
|
|
||||||
pub struct StaticLoginProvider {
|
pub struct StaticLoginProvider {
|
||||||
default_bucket: Option<String>,
|
default_bucket: Option<String>,
|
||||||
users: HashMap<String, LoginStaticUser>,
|
users: HashMap<String, LoginStaticUser>,
|
||||||
k2v_region: Region,
|
k2v_region: Region,
|
||||||
|
s3_region: Region,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StaticLoginProvider {
|
impl StaticLoginProvider {
|
||||||
pub fn new(config: LoginStaticConfig, k2v_region: Region) -> Result<Self> {
|
pub fn new(config: LoginStaticConfig, k2v_region: Region, s3_region: Region) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
default_bucket: config.default_bucket,
|
default_bucket: config.default_bucket,
|
||||||
users: config.users,
|
users: config.users,
|
||||||
k2v_region,
|
k2v_region,
|
||||||
|
s3_region,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,18 +44,31 @@ impl LoginProvider for StaticLoginProvider {
|
||||||
"No bucket configured and no default bucket specieid"
|
"No bucket configured and no default bucket specieid"
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
// TODO if master key is not specified, retrieve it from K2V key storage
|
let storage = StorageCredentials {
|
||||||
let master_key_str = u.master_key.as_ref().ok_or(anyhow!(
|
k2v_region: self.k2v_region.clone(),
|
||||||
"Master key must be specified in config file for now, this will change"
|
s3_region: self.s3_region.clone(),
|
||||||
))?;
|
|
||||||
let master_key = Key::from_slice(&base64::decode(master_key_str)?)
|
|
||||||
.ok_or(anyhow!("Invalid master key"))?;
|
|
||||||
|
|
||||||
Ok(Credentials {
|
|
||||||
aws_access_key_id: u.aws_access_key_id.clone(),
|
aws_access_key_id: u.aws_access_key_id.clone(),
|
||||||
aws_secret_access_key: u.aws_secret_access_key.clone(),
|
aws_secret_access_key: u.aws_secret_access_key.clone(),
|
||||||
bucket,
|
bucket,
|
||||||
master_key,
|
};
|
||||||
|
|
||||||
|
let keys = match (&u.master_key, &u.secret_key) {
|
||||||
|
(Some(m), Some(s)) => {
|
||||||
|
let master_key = Key::from_slice(&base64::decode(m)?)
|
||||||
|
.ok_or(anyhow!("Invalid master key"))?;
|
||||||
|
let secret_key = SecretKey::from_slice(&base64::decode(m)?)
|
||||||
|
.ok_or(anyhow!("Invalid secret key"))?;
|
||||||
|
CryptoKeys::open_without_password(&storage, &master_key, &secret_key)?
|
||||||
|
}
|
||||||
|
(None, None) => {
|
||||||
|
CryptoKeys::open(&storage, password)?
|
||||||
|
}
|
||||||
|
_ => bail!("Either both master and secret key or none of them must be specified for user"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Credentials {
|
||||||
|
storage,
|
||||||
|
keys,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,19 +22,17 @@ pub struct Mailbox {
|
||||||
|
|
||||||
impl Mailbox {
|
impl Mailbox {
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
k2v_region: &Region,
|
|
||||||
s3_region: &Region,
|
|
||||||
creds: &Credentials,
|
creds: &Credentials,
|
||||||
name: String,
|
name: String,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let uid_index = Bayou::<UidIndex>::new(k2v_region, s3_region, creds, name.clone())?;
|
let uid_index = Bayou::<UidIndex>::new(creds, name.clone())?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
bucket: creds.bucket.clone(),
|
bucket: creds.bucket().to_string(),
|
||||||
name,
|
name,
|
||||||
key: creds.master_key.clone(),
|
key: creds.keys.master.clone(),
|
||||||
k2v: creds.k2v_client(&k2v_region)?,
|
k2v: creds.k2v_client()?,
|
||||||
s3: creds.s3_client(&s3_region)?,
|
s3: creds.s3_client()?,
|
||||||
uid_index,
|
uid_index,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
52
src/main.rs
52
src/main.rs
|
@ -5,6 +5,7 @@ mod login;
|
||||||
mod mailbox;
|
mod mailbox;
|
||||||
mod time;
|
mod time;
|
||||||
mod uidindex;
|
mod uidindex;
|
||||||
|
mod server;
|
||||||
|
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -14,6 +15,7 @@ use rusoto_signature::Region;
|
||||||
use config::*;
|
use config::*;
|
||||||
use login::{ldap_provider::*, static_provider::*, *};
|
use login::{ldap_provider::*, static_provider::*, *};
|
||||||
use mailbox::Mailbox;
|
use mailbox::Mailbox;
|
||||||
|
use server::Server;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
@ -26,53 +28,7 @@ async fn main() {
|
||||||
async fn main2() -> Result<()> {
|
async fn main2() -> Result<()> {
|
||||||
let config = read_config("mailrage.toml".into())?;
|
let config = read_config("mailrage.toml".into())?;
|
||||||
|
|
||||||
let main = Main::new(config)?;
|
let server = Server::new(config)?;
|
||||||
main.run().await
|
server.run().await
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Main {
|
|
||||||
pub s3_region: Region,
|
|
||||||
pub k2v_region: Region,
|
|
||||||
pub login_provider: Box<dyn LoginProvider>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Main {
|
|
||||||
fn new(config: Config) -> Result<Arc<Self>> {
|
|
||||||
let s3_region = Region::Custom {
|
|
||||||
name: config.s3_region,
|
|
||||||
endpoint: config.s3_endpoint,
|
|
||||||
};
|
|
||||||
let k2v_region = Region::Custom {
|
|
||||||
name: config.k2v_region,
|
|
||||||
endpoint: config.k2v_endpoint,
|
|
||||||
};
|
|
||||||
let login_provider: Box<dyn LoginProvider> = match (config.login_static, config.login_ldap)
|
|
||||||
{
|
|
||||||
(Some(st), None) => Box::new(StaticLoginProvider::new(st, k2v_region.clone())?),
|
|
||||||
(None, Some(ld)) => Box::new(LdapLoginProvider::new(ld)?),
|
|
||||||
(Some(_), Some(_)) => bail!("A single login provider must be set up in config file"),
|
|
||||||
(None, None) => bail!("No login provider is set up in config file"),
|
|
||||||
};
|
|
||||||
Ok(Arc::new(Self {
|
|
||||||
s3_region,
|
|
||||||
k2v_region,
|
|
||||||
login_provider,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run(self: &Arc<Self>) -> Result<()> {
|
|
||||||
let creds = self.login_provider.login("lx", "plop").await?;
|
|
||||||
|
|
||||||
let mut mailbox = Mailbox::new(
|
|
||||||
&self.k2v_region,
|
|
||||||
&self.s3_region,
|
|
||||||
&creds,
|
|
||||||
"TestMailbox".to_string(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
mailbox.test().await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
49
src/server.rs
Normal file
49
src/server.rs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use rusoto_signature::Region;
|
||||||
|
|
||||||
|
use crate::config::*;
|
||||||
|
use crate::login::{ldap_provider::*, static_provider::*, *};
|
||||||
|
use crate::mailbox::Mailbox;
|
||||||
|
|
||||||
|
pub struct Server {
|
||||||
|
pub login_provider: Box<dyn LoginProvider>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
pub fn new(config: Config) -> Result<Arc<Self>> {
|
||||||
|
let s3_region = Region::Custom {
|
||||||
|
name: config.s3_region,
|
||||||
|
endpoint: config.s3_endpoint,
|
||||||
|
};
|
||||||
|
let k2v_region = Region::Custom {
|
||||||
|
name: config.k2v_region,
|
||||||
|
endpoint: config.k2v_endpoint,
|
||||||
|
};
|
||||||
|
let login_provider: Box<dyn LoginProvider> = match (config.login_static, config.login_ldap)
|
||||||
|
{
|
||||||
|
(Some(st), None) => Box::new(StaticLoginProvider::new(st, k2v_region, s3_region)?),
|
||||||
|
(None, Some(ld)) => Box::new(LdapLoginProvider::new(ld, k2v_region, s3_region)?),
|
||||||
|
(Some(_), Some(_)) => bail!("A single login provider must be set up in config file"),
|
||||||
|
(None, None) => bail!("No login provider is set up in config file"),
|
||||||
|
};
|
||||||
|
Ok(Arc::new(Self {
|
||||||
|
login_provider,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(self: &Arc<Self>) -> Result<()> {
|
||||||
|
let creds = self.login_provider.login("lx", "plop").await?;
|
||||||
|
|
||||||
|
let mut mailbox = Mailbox::new(
|
||||||
|
&creds,
|
||||||
|
"TestMailbox".to_string(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
mailbox.test().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue