mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-09-20 07:16:18 +08:00
Directory implementation - part 1
This commit is contained in:
parent
ab96cf0b6e
commit
5f76a1672d
89
Cargo.lock
generated
89
Cargo.lock
generated
|
@ -302,6 +302,19 @@ version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bb8"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1627eccf3aa91405435ba240be23513eeca466b5dc33866422672264de061582"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-util",
|
||||||
|
"parking_lot",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bincode"
|
name = "bincode"
|
||||||
version = "1.3.3"
|
version = "1.3.3"
|
||||||
|
@ -783,6 +796,23 @@ dependencies = [
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "directory"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"ahash 0.8.3",
|
||||||
|
"async-trait",
|
||||||
|
"bb8",
|
||||||
|
"ldap3",
|
||||||
|
"mail-send",
|
||||||
|
"rustls 0.21.1",
|
||||||
|
"smtp-proto",
|
||||||
|
"sqlx",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls 0.24.0",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
|
@ -1664,6 +1694,7 @@ dependencies = [
|
||||||
"base64 0.21.1",
|
"base64 0.21.1",
|
||||||
"bincode",
|
"bincode",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"directory",
|
||||||
"ece",
|
"ece",
|
||||||
"form-data",
|
"form-data",
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
|
@ -1760,6 +1791,43 @@ version = "1.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lber"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5d85f5e00e12cb50c70c3b1c1f0daff6546eb4c608b44d0a990e38a539e0446"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"nom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ldap3"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c5cfbd3c59ca16d6671b002b8b3dd013cd825d9c77a1664a3135194d3270511e"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
|
"futures",
|
||||||
|
"futures-util",
|
||||||
|
"lazy_static",
|
||||||
|
"lber",
|
||||||
|
"log",
|
||||||
|
"nom",
|
||||||
|
"percent-encoding",
|
||||||
|
"ring",
|
||||||
|
"rustls 0.20.8",
|
||||||
|
"rustls-native-certs",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls 0.23.4",
|
||||||
|
"tokio-stream",
|
||||||
|
"tokio-util",
|
||||||
|
"url",
|
||||||
|
"x509-parser 0.14.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.144"
|
version = "0.2.144"
|
||||||
|
@ -1929,6 +1997,7 @@ dependencies = [
|
||||||
name = "mail-server"
|
name = "mail-server"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"directory",
|
||||||
"jmap",
|
"jmap",
|
||||||
"jmap_proto",
|
"jmap_proto",
|
||||||
"smtp",
|
"smtp",
|
||||||
|
@ -3336,7 +3405,7 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
"utils",
|
"utils",
|
||||||
"webpki-roots 0.23.0",
|
"webpki-roots 0.23.0",
|
||||||
"x509-parser",
|
"x509-parser 0.15.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -4711,6 +4780,24 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "x509-parser"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0ecbeb7b67ce215e40e3cc7f2ff902f94a223acf44995934763467e7b1febc8"
|
||||||
|
dependencies = [
|
||||||
|
"asn1-rs",
|
||||||
|
"base64 0.13.1",
|
||||||
|
"data-encoding",
|
||||||
|
"der-parser",
|
||||||
|
"lazy_static",
|
||||||
|
"nom",
|
||||||
|
"oid-registry",
|
||||||
|
"rusticata-macros",
|
||||||
|
"thiserror",
|
||||||
|
"time 0.3.21",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "x509-parser"
|
name = "x509-parser"
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
|
|
|
@ -20,6 +20,7 @@ store = { path = "crates/store" }
|
||||||
jmap = { path = "crates/jmap" }
|
jmap = { path = "crates/jmap" }
|
||||||
jmap_proto = { path = "crates/jmap-proto" }
|
jmap_proto = { path = "crates/jmap-proto" }
|
||||||
smtp = { path = "crates/smtp", features = ["local_delivery"] }
|
smtp = { path = "crates/smtp", features = ["local_delivery"] }
|
||||||
|
directory = { path = "crates/directory" }
|
||||||
utils = { path = "crates/utils" }
|
utils = { path = "crates/utils" }
|
||||||
tokio = { version = "1.23", features = ["full"] }
|
tokio = { version = "1.23", features = ["full"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
@ -35,6 +36,7 @@ members = [
|
||||||
"crates/jmap-proto",
|
"crates/jmap-proto",
|
||||||
"crates/smtp",
|
"crates/smtp",
|
||||||
"crates/store",
|
"crates/store",
|
||||||
|
"crates/directory",
|
||||||
"crates/utils",
|
"crates/utils",
|
||||||
"crates/maybe-async",
|
"crates/maybe-async",
|
||||||
"tests",
|
"tests",
|
||||||
|
|
21
crates/directory/Cargo.toml
Normal file
21
crates/directory/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "directory"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
smtp-proto = { git = "https://github.com/stalwartlabs/smtp-proto" }
|
||||||
|
mail-send = { git = "https://github.com/stalwartlabs/mail-send", default-features = false, features = ["cram-md5", "skip-ehlo"] }
|
||||||
|
tokio = { version = "1.23", features = ["net"] }
|
||||||
|
tokio-rustls = { version = "0.24.0"}
|
||||||
|
rustls = "0.21.0"
|
||||||
|
sqlx = { version = "0.7.0-alpha.3", features = [ "runtime-tokio-rustls", "postgres", "mysql", "sqlite" ] }
|
||||||
|
ldap3 = { version = "0.11.1", default-features = false, features = ["tls-rustls"] }
|
||||||
|
bb8 = "0.8.0"
|
||||||
|
async-trait = "0.1.68"
|
||||||
|
ahash = { version = "0.8" }
|
||||||
|
tracing = "0.1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1.23", features = ["full"] }
|
168
crates/directory/src/imap/client.rs
Normal file
168
crates/directory/src/imap/client.rs
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
use mail_send::Credentials;
|
||||||
|
use smtp_proto::{
|
||||||
|
request::{parser::Rfc5321Parser, AUTH},
|
||||||
|
response::generate::BitToString,
|
||||||
|
IntoString, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2,
|
||||||
|
};
|
||||||
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||||
|
|
||||||
|
use super::{ImapClient, ImapError};
|
||||||
|
|
||||||
|
impl<T: AsyncRead + AsyncWrite + Unpin> ImapClient<T> {
|
||||||
|
pub async fn authenticate(
|
||||||
|
&mut self,
|
||||||
|
mechanism: u64,
|
||||||
|
credentials: &Credentials<String>,
|
||||||
|
) -> Result<(), ImapError> {
|
||||||
|
if (mechanism & (AUTH_PLAIN | AUTH_XOAUTH2 | AUTH_OAUTHBEARER)) != 0 {
|
||||||
|
self.write(
|
||||||
|
format!(
|
||||||
|
"C3 AUTHENTICATE {} {}\r\n",
|
||||||
|
mechanism.to_mechanism(),
|
||||||
|
credentials
|
||||||
|
.encode(mechanism, "")
|
||||||
|
.map_err(|err| ImapError::InvalidChallenge(err.to_string()))?
|
||||||
|
)
|
||||||
|
.as_bytes(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
self.write(format!("C3 AUTHENTICATE {}\r\n", mechanism.to_mechanism()).as_bytes())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
let mut line = self.read_line().await?;
|
||||||
|
|
||||||
|
for _ in 0..3 {
|
||||||
|
if matches!(line.first(), Some(b'+')) {
|
||||||
|
self.write(
|
||||||
|
format!(
|
||||||
|
"{}\r\n",
|
||||||
|
credentials
|
||||||
|
.encode(
|
||||||
|
mechanism,
|
||||||
|
std::str::from_utf8(line.get(2..).unwrap_or_default())
|
||||||
|
.unwrap_or_default()
|
||||||
|
)
|
||||||
|
.map_err(|err| ImapError::InvalidChallenge(err.to_string()))?
|
||||||
|
)
|
||||||
|
.as_bytes(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
line = self.read_line().await?;
|
||||||
|
} else if matches!(line.get(..5), Some(b"C3 OK")) {
|
||||||
|
return Ok(());
|
||||||
|
} else if matches!(line.get(..5), Some(b"C3 NO"))
|
||||||
|
|| matches!(line.get(..6), Some(b"C3 BAD"))
|
||||||
|
{
|
||||||
|
return Err(ImapError::AuthenticationFailed);
|
||||||
|
} else {
|
||||||
|
return Err(ImapError::InvalidResponse(line.into_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(ImapError::InvalidResponse(line.into_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn authentication_mechanisms(&mut self) -> Result<u64, ImapError> {
|
||||||
|
tokio::time::timeout(self.timeout, async {
|
||||||
|
self.write(b"C0 CAPABILITY\r\n").await?;
|
||||||
|
|
||||||
|
let line = self.read_line().await?;
|
||||||
|
if !matches!(line.get(..12), Some(b"* CAPABILITY")) {
|
||||||
|
return Err(ImapError::InvalidResponse(line.into_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut line_iter = line.iter();
|
||||||
|
let mut parser = Rfc5321Parser::new(&mut line_iter);
|
||||||
|
let mut mechanisms = 0;
|
||||||
|
|
||||||
|
'outer: while let Ok(ch) = parser.read_char() {
|
||||||
|
if ch == b' ' {
|
||||||
|
loop {
|
||||||
|
if parser.hashed_value().unwrap_or(0) == AUTH && parser.stop_char == b'=' {
|
||||||
|
if let Ok(Some(mechanism)) = parser.mechanism() {
|
||||||
|
mechanisms |= mechanism;
|
||||||
|
}
|
||||||
|
match parser.stop_char {
|
||||||
|
b' ' => (),
|
||||||
|
b'\n' => break 'outer,
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ch == b'\n' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(mechanisms)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| ImapError::Timeout)?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn noop(&mut self) -> Result<(), ImapError> {
|
||||||
|
tokio::time::timeout(self.timeout, async {
|
||||||
|
self.write(b"C8 NOOP\r\n").await?;
|
||||||
|
self.read_line().await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| ImapError::Timeout)?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn logout(&mut self) -> Result<(), ImapError> {
|
||||||
|
tokio::time::timeout(self.timeout, async {
|
||||||
|
self.write(b"C9 LOGOUT\r\n").await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| ImapError::Timeout)?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn expect_greeting(&mut self) -> Result<(), ImapError> {
|
||||||
|
tokio::time::timeout(self.timeout, async {
|
||||||
|
let line = self.read_line().await?;
|
||||||
|
if matches!(line.get(..4), Some(b"* OK")) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ImapError::InvalidResponse(line.into_string()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| ImapError::Timeout)?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_line(&mut self) -> Result<Vec<u8>, ImapError> {
|
||||||
|
let mut buf = vec![0u8; 1024];
|
||||||
|
let mut buf_extended = Vec::with_capacity(0);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let br = self.stream.read(&mut buf).await?;
|
||||||
|
|
||||||
|
if br > 0 {
|
||||||
|
if matches!(buf.get(br - 1), Some(b'\n')) {
|
||||||
|
//println!("{:?}", std::str::from_utf8(&buf[..br]).unwrap());
|
||||||
|
return Ok(if buf_extended.is_empty() {
|
||||||
|
buf.truncate(br);
|
||||||
|
buf
|
||||||
|
} else {
|
||||||
|
buf_extended.extend_from_slice(&buf[..br]);
|
||||||
|
buf_extended
|
||||||
|
});
|
||||||
|
} else if buf_extended.is_empty() {
|
||||||
|
buf_extended = buf[..br].to_vec();
|
||||||
|
} else {
|
||||||
|
buf_extended.extend_from_slice(&buf[..br]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(ImapError::Disconnected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write(&mut self, bytes: &[u8]) -> Result<(), std::io::Error> {
|
||||||
|
self.stream.write_all(bytes).await?;
|
||||||
|
self.stream.flush().await
|
||||||
|
}
|
||||||
|
}
|
86
crates/directory/src/imap/lookup.rs
Normal file
86
crates/directory/src/imap/lookup.rs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
use mail_send::Credentials;
|
||||||
|
use smtp_proto::{AUTH_CRAM_MD5, AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2};
|
||||||
|
|
||||||
|
use crate::{Directory, DirectoryError, Principal};
|
||||||
|
|
||||||
|
use super::{ImapDirectory, ImapError};
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Directory for ImapDirectory {
|
||||||
|
async fn authenticate(
|
||||||
|
&self,
|
||||||
|
credentials: &Credentials<String>,
|
||||||
|
) -> crate::Result<Option<Principal>> {
|
||||||
|
let mut client = self.pool.get().await?;
|
||||||
|
let mechanism = match credentials {
|
||||||
|
Credentials::Plain { .. }
|
||||||
|
if (client.mechanisms & (AUTH_PLAIN | AUTH_LOGIN | AUTH_CRAM_MD5)) != 0 =>
|
||||||
|
{
|
||||||
|
if client.mechanisms & AUTH_CRAM_MD5 != 0 {
|
||||||
|
AUTH_CRAM_MD5
|
||||||
|
} else if client.mechanisms & AUTH_PLAIN != 0 {
|
||||||
|
AUTH_PLAIN
|
||||||
|
} else {
|
||||||
|
AUTH_LOGIN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Credentials::OAuthBearer { .. } if client.mechanisms & AUTH_OAUTHBEARER != 0 => {
|
||||||
|
AUTH_OAUTHBEARER
|
||||||
|
}
|
||||||
|
Credentials::XOauth2 { .. } if client.mechanisms & AUTH_XOAUTH2 != 0 => AUTH_XOAUTH2,
|
||||||
|
_ => {
|
||||||
|
tracing::warn!(
|
||||||
|
context = "remote",
|
||||||
|
event = "error",
|
||||||
|
protocol = "imap",
|
||||||
|
"IMAP server does not offer any supported auth mechanisms.",
|
||||||
|
);
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match client.authenticate(mechanism, credentials).await {
|
||||||
|
Ok(_) => Ok(Some(Principal::default())),
|
||||||
|
Err(err) => match &err {
|
||||||
|
ImapError::AuthenticationFailed => Ok(None),
|
||||||
|
_ => Err(err.into()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn principal_by_name(&self, _name: &str) -> crate::Result<Option<Principal>> {
|
||||||
|
Err(DirectoryError::unsupported("imap", "principal_by_name"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn principal_by_id(&self, _id: u32) -> crate::Result<Option<Principal>> {
|
||||||
|
Err(DirectoryError::unsupported("imap", "principal_by_id"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn member_of(&self, _principal: &Principal) -> crate::Result<Vec<u32>> {
|
||||||
|
Err(DirectoryError::unsupported("imap", "member_of"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn emails_by_id(&self, _id: u32) -> crate::Result<Vec<String>> {
|
||||||
|
Err(DirectoryError::unsupported("imap", "emails_by_id"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ids_by_email(&self, _address: &str) -> crate::Result<Vec<u32>> {
|
||||||
|
Err(DirectoryError::unsupported("imap", "ids_by_email"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rcpt(&self, _address: &str) -> crate::Result<bool> {
|
||||||
|
Err(DirectoryError::unsupported("imap", "rcpt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn vrfy(&self, _address: &str) -> crate::Result<Vec<String>> {
|
||||||
|
Err(DirectoryError::unsupported("imap", "vrfy"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn expn(&self, _address: &str) -> crate::Result<Vec<String>> {
|
||||||
|
Err(DirectoryError::unsupported("imap", "expn"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(&self, _query: &str, _params: &[&str]) -> crate::Result<bool> {
|
||||||
|
Err(DirectoryError::unsupported("imap", "query"))
|
||||||
|
}
|
||||||
|
}
|
62
crates/directory/src/imap/mod.rs
Normal file
62
crates/directory/src/imap/mod.rs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
pub mod client;
|
||||||
|
pub mod lookup;
|
||||||
|
pub mod pool;
|
||||||
|
pub mod tls;
|
||||||
|
|
||||||
|
use std::{fmt::Display, sync::atomic::AtomicU64, time::Duration};
|
||||||
|
|
||||||
|
use bb8::Pool;
|
||||||
|
use tokio::io::{AsyncRead, AsyncWrite};
|
||||||
|
use tokio_rustls::TlsConnector;
|
||||||
|
|
||||||
|
pub struct ImapDirectory {
|
||||||
|
pool: Pool<ImapConnectionManager>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ImapConnectionManager {
|
||||||
|
addr: String,
|
||||||
|
timeout: Duration,
|
||||||
|
tls_connector: TlsConnector,
|
||||||
|
tls_hostname: String,
|
||||||
|
tls_implicit: bool,
|
||||||
|
mechanisms: AtomicU64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ImapClient<T: AsyncRead + AsyncWrite> {
|
||||||
|
stream: T,
|
||||||
|
mechanisms: u64,
|
||||||
|
timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ImapError {
|
||||||
|
Io(std::io::Error),
|
||||||
|
Timeout,
|
||||||
|
InvalidResponse(String),
|
||||||
|
InvalidChallenge(String),
|
||||||
|
AuthenticationFailed,
|
||||||
|
TLSInvalidName,
|
||||||
|
Disconnected,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for ImapError {
|
||||||
|
fn from(error: std::io::Error) -> Self {
|
||||||
|
ImapError::Io(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ImapError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ImapError::Io(io) => write!(f, "I/O error: {io}"),
|
||||||
|
ImapError::Timeout => f.write_str("Connection time-out"),
|
||||||
|
ImapError::InvalidResponse(response) => write!(f, "Unexpected response: {response:?}"),
|
||||||
|
ImapError::InvalidChallenge(response) => {
|
||||||
|
write!(f, "Invalid auth challenge: {response}")
|
||||||
|
}
|
||||||
|
ImapError::TLSInvalidName => f.write_str("Invalid TLS name"),
|
||||||
|
ImapError::Disconnected => f.write_str("Connection disconnected by peer"),
|
||||||
|
ImapError::AuthenticationFailed => f.write_str("Authentication failed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
crates/directory/src/imap/pool.rs
Normal file
44
crates/directory/src/imap/pool.rs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
|
use bb8::ManageConnection;
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tokio_rustls::client::TlsStream;
|
||||||
|
|
||||||
|
use super::{ImapClient, ImapConnectionManager, ImapError};
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl ManageConnection for ImapConnectionManager {
|
||||||
|
type Connection = ImapClient<TlsStream<TcpStream>>;
|
||||||
|
type Error = ImapError;
|
||||||
|
|
||||||
|
/// Attempts to create a new connection.
|
||||||
|
async fn connect(&self) -> Result<Self::Connection, Self::Error> {
|
||||||
|
let mut conn = ImapClient::connect(
|
||||||
|
&self.addr,
|
||||||
|
self.timeout,
|
||||||
|
&self.tls_connector,
|
||||||
|
&self.tls_hostname,
|
||||||
|
self.tls_implicit,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Obtain the list of supported authentication mechanisms.
|
||||||
|
conn.mechanisms = self.mechanisms.load(Ordering::Relaxed);
|
||||||
|
if conn.mechanisms == 0 {
|
||||||
|
conn.mechanisms = conn.authentication_mechanisms().await?;
|
||||||
|
self.mechanisms.store(conn.mechanisms, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines if the connection is still connected to the database.
|
||||||
|
async fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Self::Error> {
|
||||||
|
conn.noop().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synchronously determine if the connection is no longer usable, if possible.
|
||||||
|
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
84
crates/directory/src/imap/tls.rs
Normal file
84
crates/directory/src/imap/tls.rs
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use rustls::ServerName;
|
||||||
|
use smtp_proto::IntoString;
|
||||||
|
use tokio::net::{TcpStream, ToSocketAddrs};
|
||||||
|
use tokio_rustls::{client::TlsStream, TlsConnector};
|
||||||
|
|
||||||
|
use super::{ImapClient, ImapError};
|
||||||
|
|
||||||
|
impl ImapClient<TcpStream> {
|
||||||
|
async fn start_tls(
|
||||||
|
mut self,
|
||||||
|
tls_connector: &TlsConnector,
|
||||||
|
tls_hostname: &str,
|
||||||
|
) -> Result<ImapClient<TlsStream<TcpStream>>, ImapError> {
|
||||||
|
let line = tokio::time::timeout(self.timeout, async {
|
||||||
|
self.write(b"C7 STARTTLS\r\n").await?;
|
||||||
|
|
||||||
|
self.read_line().await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| ImapError::Timeout)??;
|
||||||
|
|
||||||
|
if matches!(line.get(..5), Some(b"C7 OK")) {
|
||||||
|
self.into_tls(tls_connector, tls_hostname).await
|
||||||
|
} else {
|
||||||
|
Err(ImapError::InvalidResponse(line.into_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn into_tls(
|
||||||
|
self,
|
||||||
|
tls_connector: &TlsConnector,
|
||||||
|
tls_hostname: &str,
|
||||||
|
) -> Result<ImapClient<TlsStream<TcpStream>>, ImapError> {
|
||||||
|
tokio::time::timeout(self.timeout, async {
|
||||||
|
Ok(ImapClient {
|
||||||
|
stream: tls_connector
|
||||||
|
.connect(
|
||||||
|
ServerName::try_from(tls_hostname)
|
||||||
|
.map_err(|_| ImapError::TLSInvalidName)?,
|
||||||
|
self.stream,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
timeout: self.timeout,
|
||||||
|
mechanisms: self.mechanisms,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| ImapError::Timeout)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImapClient<TlsStream<TcpStream>> {
|
||||||
|
pub async fn connect(
|
||||||
|
addr: impl ToSocketAddrs,
|
||||||
|
timeout: Duration,
|
||||||
|
tls_connector: &TlsConnector,
|
||||||
|
tls_hostname: &str,
|
||||||
|
tls_implicit: bool,
|
||||||
|
) -> Result<Self, ImapError> {
|
||||||
|
let mut client: ImapClient<TcpStream> = tokio::time::timeout(timeout, async {
|
||||||
|
match TcpStream::connect(addr).await {
|
||||||
|
Ok(stream) => Ok(ImapClient {
|
||||||
|
stream,
|
||||||
|
timeout,
|
||||||
|
mechanisms: 0,
|
||||||
|
}),
|
||||||
|
Err(err) => Err(ImapError::Io(err)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| ImapError::Timeout)??;
|
||||||
|
|
||||||
|
if tls_implicit {
|
||||||
|
let mut client = client.into_tls(tls_connector, tls_hostname).await?;
|
||||||
|
client.expect_greeting().await?;
|
||||||
|
Ok(client)
|
||||||
|
} else {
|
||||||
|
client.expect_greeting().await?;
|
||||||
|
client.start_tls(tls_connector, tls_hostname).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
0
crates/directory/src/ldap/config.rs
Normal file
0
crates/directory/src/ldap/config.rs
Normal file
309
crates/directory/src/ldap/lookup.rs
Normal file
309
crates/directory/src/ldap/lookup.rs
Normal file
|
@ -0,0 +1,309 @@
|
||||||
|
use ldap3::{Scope, SearchEntry};
|
||||||
|
use mail_send::Credentials;
|
||||||
|
|
||||||
|
use crate::{Directory, Principal, Type};
|
||||||
|
|
||||||
|
use super::{LdapDirectory, LdapMappings};
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Directory for LdapDirectory {
|
||||||
|
async fn authenticate(
|
||||||
|
&self,
|
||||||
|
credentials: &Credentials<String>,
|
||||||
|
) -> crate::Result<Option<Principal>> {
|
||||||
|
let (username, secret) = match credentials {
|
||||||
|
Credentials::Plain { username, secret } => (username, secret),
|
||||||
|
Credentials::OAuthBearer { token } => (token, token),
|
||||||
|
Credentials::XOauth2 { username, secret } => (username, secret),
|
||||||
|
};
|
||||||
|
match self
|
||||||
|
.find_principal(&self.mappings.filter_login.build(username))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Some(principal)) => {
|
||||||
|
if principal.secret.as_ref().map_or(false, |s| s == secret) {
|
||||||
|
Ok(Some(principal))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result => result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn principal_by_name(&self, name: &str) -> crate::Result<Option<Principal>> {
|
||||||
|
self.find_principal(&self.mappings.filter_name.build(name))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn principal_by_id(&self, id: u32) -> crate::Result<Option<Principal>> {
|
||||||
|
self.find_principal(&self.mappings.filter_id.build(&id.to_string()))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn member_of(&self, principal: &Principal) -> crate::Result<Vec<u32>> {
|
||||||
|
if principal.member_of.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
let mut conn = self.pool.get().await?;
|
||||||
|
let mut ids = Vec::with_capacity(principal.member_of.len());
|
||||||
|
for group in &principal.member_of {
|
||||||
|
let (rs, _res) = if group.contains('=') {
|
||||||
|
conn.search(group, Scope::Base, "", &self.mappings.attr_id)
|
||||||
|
.await?
|
||||||
|
.success()?
|
||||||
|
} else {
|
||||||
|
conn.search(
|
||||||
|
&self.mappings.base_dn,
|
||||||
|
Scope::Subtree,
|
||||||
|
&self.mappings.filter_name.build(group),
|
||||||
|
&self.mappings.attr_id,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.success()?
|
||||||
|
};
|
||||||
|
for entry in rs {
|
||||||
|
for (attr, value) in SearchEntry::construct(entry).attrs {
|
||||||
|
if self.mappings.attr_id.contains(&attr) {
|
||||||
|
if let Some(id) = value.first() {
|
||||||
|
if let Ok(id) = id.parse() {
|
||||||
|
ids.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn emails_by_id(&self, id: u32) -> crate::Result<Vec<String>> {
|
||||||
|
let (rs, _res) = self
|
||||||
|
.pool
|
||||||
|
.get()
|
||||||
|
.await?
|
||||||
|
.search(
|
||||||
|
&self.mappings.base_dn,
|
||||||
|
Scope::Subtree,
|
||||||
|
&self.mappings.filter_id.build(&id.to_string()),
|
||||||
|
&self.mappings.attrs_email,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.success()?;
|
||||||
|
|
||||||
|
let mut emails = Vec::new();
|
||||||
|
for entry in rs {
|
||||||
|
let entry = SearchEntry::construct(entry);
|
||||||
|
for attr in &self.mappings.attrs_email {
|
||||||
|
if let Some(values) = entry.attrs.get(attr) {
|
||||||
|
for email in values {
|
||||||
|
if !email.is_empty() {
|
||||||
|
emails.push(email.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(emails)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ids_by_email(&self, email: &str) -> crate::Result<Vec<u32>> {
|
||||||
|
let (rs, _res) = self
|
||||||
|
.pool
|
||||||
|
.get()
|
||||||
|
.await?
|
||||||
|
.search(
|
||||||
|
&self.mappings.base_dn,
|
||||||
|
Scope::Subtree,
|
||||||
|
&self.mappings.filter_email.build(email),
|
||||||
|
&self.mappings.attr_id,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.success()?;
|
||||||
|
|
||||||
|
let mut ids = Vec::new();
|
||||||
|
for entry in rs {
|
||||||
|
let entry = SearchEntry::construct(entry);
|
||||||
|
for attr in &self.mappings.attr_id {
|
||||||
|
if let Some(values) = entry.attrs.get(attr) {
|
||||||
|
for id in values {
|
||||||
|
if let Ok(id) = id.parse() {
|
||||||
|
ids.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rcpt(&self, address: &str) -> crate::Result<bool> {
|
||||||
|
let (rs, _res) = self
|
||||||
|
.pool
|
||||||
|
.get()
|
||||||
|
.await?
|
||||||
|
.search(
|
||||||
|
&self.mappings.base_dn,
|
||||||
|
Scope::Subtree,
|
||||||
|
&self.mappings.filter_email.build(address),
|
||||||
|
&self.mappings.attr_email_address,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.success()?;
|
||||||
|
Ok(!rs.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
|
||||||
|
let mut stream = self
|
||||||
|
.pool
|
||||||
|
.get()
|
||||||
|
.await?
|
||||||
|
.streaming_search(
|
||||||
|
&self.mappings.base_dn,
|
||||||
|
Scope::Subtree,
|
||||||
|
&self.mappings.filter_verify.build(address),
|
||||||
|
&self.mappings.attr_email_address,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut emails = Vec::new();
|
||||||
|
while let Some(entry) = stream.next().await? {
|
||||||
|
let entry = SearchEntry::construct(entry);
|
||||||
|
for attr in &self.mappings.attr_email_address {
|
||||||
|
if let Some(values) = entry.attrs.get(attr) {
|
||||||
|
for email in values {
|
||||||
|
if !email.is_empty() {
|
||||||
|
emails.push(email.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(emails)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
|
||||||
|
let mut stream = self
|
||||||
|
.pool
|
||||||
|
.get()
|
||||||
|
.await?
|
||||||
|
.streaming_search(
|
||||||
|
&self.mappings.base_dn,
|
||||||
|
Scope::Subtree,
|
||||||
|
&self.mappings.filter_expand.build(address),
|
||||||
|
&self.mappings.attr_email_address,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut emails = Vec::new();
|
||||||
|
while let Some(entry) = stream.next().await? {
|
||||||
|
let entry = SearchEntry::construct(entry);
|
||||||
|
for attr in &self.mappings.attr_email_address {
|
||||||
|
if let Some(values) = entry.attrs.get(attr) {
|
||||||
|
for email in values {
|
||||||
|
if !email.is_empty() {
|
||||||
|
emails.push(email.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(emails)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(&self, query: &str, params: &[&str]) -> crate::Result<bool> {
|
||||||
|
let mut conn = self.pool.get().await?;
|
||||||
|
|
||||||
|
Ok(if !params.is_empty() {
|
||||||
|
let mut expanded_query = String::with_capacity(query.len() + params.len() * 2);
|
||||||
|
for (pos, item) in query.split('$').enumerate() {
|
||||||
|
if pos > 0 {
|
||||||
|
if let Some(param) = params.get(pos - 1) {
|
||||||
|
expanded_query.push_str(param);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expanded_query.push_str(item);
|
||||||
|
}
|
||||||
|
conn.streaming_search(
|
||||||
|
&self.mappings.base_dn,
|
||||||
|
Scope::Subtree,
|
||||||
|
&expanded_query,
|
||||||
|
Vec::<String>::new(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
conn.streaming_search(
|
||||||
|
&self.mappings.base_dn,
|
||||||
|
Scope::Subtree,
|
||||||
|
query,
|
||||||
|
Vec::<String>::new(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}?
|
||||||
|
.next()
|
||||||
|
.await?
|
||||||
|
.is_some())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LdapDirectory {
|
||||||
|
async fn find_principal(&self, filter: &str) -> crate::Result<Option<Principal>> {
|
||||||
|
let (rs, _res) = self
|
||||||
|
.pool
|
||||||
|
.get()
|
||||||
|
.await?
|
||||||
|
.search(
|
||||||
|
&self.mappings.base_dn,
|
||||||
|
Scope::Subtree,
|
||||||
|
filter,
|
||||||
|
&self.mappings.attrs_principal,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.success()?;
|
||||||
|
Ok(rs.into_iter().next().map(|entry| {
|
||||||
|
self.mappings
|
||||||
|
.entry_to_principal(SearchEntry::construct(entry))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LdapMappings {
|
||||||
|
pub fn entry_to_principal(&self, entry: SearchEntry) -> Principal {
|
||||||
|
let mut principal = Principal {
|
||||||
|
id: u32::MAX,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
for (attr, value) in entry.attrs {
|
||||||
|
if self.attr_id.contains(&attr) {
|
||||||
|
if let Ok(id) = value.into_iter().next().unwrap_or_default().parse() {
|
||||||
|
principal.id = id;
|
||||||
|
}
|
||||||
|
} else if self.attr_name.contains(&attr) {
|
||||||
|
principal.name = value.into_iter().next().unwrap_or_default();
|
||||||
|
} else if self.attr_secret.contains(&attr) {
|
||||||
|
principal.secret = value.into_iter().next();
|
||||||
|
} else if self.attr_description.contains(&attr) {
|
||||||
|
principal.description = value.into_iter().next();
|
||||||
|
} else if self.attr_groups.contains(&attr) {
|
||||||
|
principal.member_of.extend(value);
|
||||||
|
} else if self.attr_quota.contains(&attr) {
|
||||||
|
if let Ok(quota) = value.into_iter().next().unwrap_or_default().parse() {
|
||||||
|
principal.quota = quota;
|
||||||
|
}
|
||||||
|
} else if attr.eq_ignore_ascii_case("objectClass") {
|
||||||
|
if value.contains(&self.obj_user) {
|
||||||
|
principal.typ = Type::Individual;
|
||||||
|
} else if value.contains(&self.obj_group) {
|
||||||
|
principal.typ = Type::Group;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
principal
|
||||||
|
}
|
||||||
|
}
|
69
crates/directory/src/ldap/mod.rs
Normal file
69
crates/directory/src/ldap/mod.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
use bb8::Pool;
|
||||||
|
use ldap3::LdapConnSettings;
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
|
pub mod lookup;
|
||||||
|
pub mod pool;
|
||||||
|
|
||||||
|
pub struct LdapDirectory {
|
||||||
|
pool: Pool<LdapConnectionManager>,
|
||||||
|
mappings: LdapMappings,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LdapMappings {
|
||||||
|
base_dn: String,
|
||||||
|
filter_login: LdapFilter,
|
||||||
|
filter_name: LdapFilter,
|
||||||
|
filter_email: LdapFilter,
|
||||||
|
filter_id: LdapFilter,
|
||||||
|
filter_verify: LdapFilter,
|
||||||
|
filter_expand: LdapFilter,
|
||||||
|
obj_user: String,
|
||||||
|
obj_group: String,
|
||||||
|
attr_name: Vec<String>,
|
||||||
|
attr_description: Vec<String>,
|
||||||
|
attr_secret: Vec<String>,
|
||||||
|
attr_groups: Vec<String>,
|
||||||
|
attr_id: Vec<String>,
|
||||||
|
attr_email_address: Vec<String>,
|
||||||
|
attr_quota: Vec<String>,
|
||||||
|
attrs_principal: Vec<String>,
|
||||||
|
attrs_email: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LdapFilter {
|
||||||
|
filter: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LdapFilter {
|
||||||
|
pub fn build(&self, value: &str) -> String {
|
||||||
|
self.filter.join(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct LdapConnectionManager {
|
||||||
|
address: String,
|
||||||
|
settings: LdapConnSettings,
|
||||||
|
bind_dn: Option<Bind>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Bind {
|
||||||
|
dn: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LdapConnectionManager {
|
||||||
|
pub fn new(address: String, settings: LdapConnSettings, bind_dn: Option<Bind>) -> Self {
|
||||||
|
Self {
|
||||||
|
address,
|
||||||
|
settings,
|
||||||
|
bind_dn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bind {
|
||||||
|
pub fn new(dn: String, password: String) -> Self {
|
||||||
|
Self { dn, password }
|
||||||
|
}
|
||||||
|
}
|
33
crates/directory/src/ldap/pool.rs
Normal file
33
crates/directory/src/ldap/pool.rs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
use bb8::ManageConnection;
|
||||||
|
use ldap3::{exop::WhoAmI, Ldap, LdapConnAsync, LdapError};
|
||||||
|
|
||||||
|
use super::LdapConnectionManager;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl ManageConnection for LdapConnectionManager {
|
||||||
|
type Connection = Ldap;
|
||||||
|
type Error = LdapError;
|
||||||
|
|
||||||
|
/// Attempts to create a new connection.
|
||||||
|
async fn connect(&self) -> Result<Self::Connection, Self::Error> {
|
||||||
|
let (conn, mut ldap) =
|
||||||
|
LdapConnAsync::with_settings(self.settings.clone(), &self.address).await?;
|
||||||
|
ldap3::drive!(conn);
|
||||||
|
|
||||||
|
if let Some(bind) = &self.bind_dn {
|
||||||
|
ldap.simple_bind(&bind.dn, &bind.password).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ldap)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines if the connection is still connected to the database.
|
||||||
|
async fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Self::Error> {
|
||||||
|
conn.extended(WhoAmI).await.map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synchronously determine if the connection is no longer usable, if possible.
|
||||||
|
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
|
||||||
|
conn.is_closed()
|
||||||
|
}
|
||||||
|
}
|
167
crates/directory/src/lib.rs
Normal file
167
crates/directory/src/lib.rs
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
use bb8::RunError;
|
||||||
|
use imap::ImapError;
|
||||||
|
use ldap3::LdapError;
|
||||||
|
use mail_send::Credentials;
|
||||||
|
|
||||||
|
pub mod imap;
|
||||||
|
pub mod ldap;
|
||||||
|
pub mod smtp;
|
||||||
|
pub mod sql;
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Principal {
|
||||||
|
pub id: u32,
|
||||||
|
pub name: String,
|
||||||
|
pub secret: Option<String>,
|
||||||
|
pub typ: Type,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub quota: u32,
|
||||||
|
pub member_of: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
|
pub enum Type {
|
||||||
|
Individual,
|
||||||
|
Group,
|
||||||
|
Resource,
|
||||||
|
Location,
|
||||||
|
#[default]
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum DirectoryError {
|
||||||
|
Ldap(LdapError),
|
||||||
|
Sql(sqlx::Error),
|
||||||
|
Imap(ImapError),
|
||||||
|
Smtp(mail_send::Error),
|
||||||
|
TimedOut,
|
||||||
|
Unsupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait Directory {
|
||||||
|
async fn authenticate(&self, credentials: &Credentials<String>) -> Result<Option<Principal>>;
|
||||||
|
async fn principal_by_name(&self, name: &str) -> Result<Option<Principal>>;
|
||||||
|
async fn principal_by_id(&self, id: u32) -> Result<Option<Principal>>;
|
||||||
|
async fn member_of(&self, principal: &Principal) -> Result<Vec<u32>>;
|
||||||
|
async fn emails_by_id(&self, id: u32) -> Result<Vec<String>>;
|
||||||
|
async fn ids_by_email(&self, email: &str) -> Result<Vec<u32>>;
|
||||||
|
async fn rcpt(&self, address: &str) -> crate::Result<bool>;
|
||||||
|
async fn vrfy(&self, address: &str) -> Result<Vec<String>>;
|
||||||
|
async fn expn(&self, address: &str) -> Result<Vec<String>>;
|
||||||
|
async fn query(&self, query: &str, params: &[&str]) -> Result<bool>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, DirectoryError>;
|
||||||
|
|
||||||
|
impl From<LdapError> for DirectoryError {
|
||||||
|
fn from(error: LdapError) -> Self {
|
||||||
|
DirectoryError::Ldap(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RunError<LdapError>> for DirectoryError {
|
||||||
|
fn from(error: RunError<LdapError>) -> Self {
|
||||||
|
match error {
|
||||||
|
RunError::User(error) => DirectoryError::Ldap(error),
|
||||||
|
RunError::TimedOut => DirectoryError::TimedOut,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RunError<ImapError>> for DirectoryError {
|
||||||
|
fn from(error: RunError<ImapError>) -> Self {
|
||||||
|
match error {
|
||||||
|
RunError::User(error) => DirectoryError::Imap(error),
|
||||||
|
RunError::TimedOut => DirectoryError::TimedOut,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RunError<mail_send::Error>> for DirectoryError {
|
||||||
|
fn from(error: RunError<mail_send::Error>) -> Self {
|
||||||
|
match error {
|
||||||
|
RunError::User(error) => DirectoryError::Smtp(error),
|
||||||
|
RunError::TimedOut => DirectoryError::TimedOut,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<sqlx::Error> for DirectoryError {
|
||||||
|
fn from(error: sqlx::Error) -> Self {
|
||||||
|
DirectoryError::Sql(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ImapError> for DirectoryError {
|
||||||
|
fn from(error: ImapError) -> Self {
|
||||||
|
DirectoryError::Imap(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<mail_send::Error> for DirectoryError {
|
||||||
|
fn from(error: mail_send::Error) -> Self {
|
||||||
|
DirectoryError::Smtp(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DirectoryError {
|
||||||
|
pub fn unsupported(protocol: &str, method: &str) -> Self {
|
||||||
|
tracing::warn!(
|
||||||
|
context = "remote",
|
||||||
|
event = "error",
|
||||||
|
protocol = protocol,
|
||||||
|
method = method,
|
||||||
|
"Method not supported by directory"
|
||||||
|
);
|
||||||
|
DirectoryError::Unsupported
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use ldap3::{LdapConnAsync, LdapConnSettings, Scope, SearchEntry};
|
||||||
|
|
||||||
|
use crate::ldap::{Bind, LdapConnectionManager};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ldap() {
|
||||||
|
let manager = LdapConnectionManager::new(
|
||||||
|
"ldap://localhost:3893".to_string(),
|
||||||
|
LdapConnSettings::new(),
|
||||||
|
Bind::new(
|
||||||
|
"cn=serviceuser,ou=svcaccts,dc=example,dc=com".into(),
|
||||||
|
"mysecret".into(),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
let pool = bb8::Pool::builder()
|
||||||
|
.min_idle(None)
|
||||||
|
.max_size(10)
|
||||||
|
.max_lifetime(std::time::Duration::from_secs(30 * 60).into())
|
||||||
|
.idle_timeout(std::time::Duration::from_secs(10 * 60).into())
|
||||||
|
.connection_timeout(std::time::Duration::from_secs(30))
|
||||||
|
.test_on_check_out(true)
|
||||||
|
.build(manager)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut ldap = pool.get().await.unwrap();
|
||||||
|
|
||||||
|
let (rs, _res) = ldap
|
||||||
|
.search(
|
||||||
|
"dc=example,dc=com",
|
||||||
|
Scope::Subtree,
|
||||||
|
"(&(objectClass=posixAccount)(cn=johndoe))",
|
||||||
|
vec!["cocomiel", "cn", "uidNumber"], //Vec::<String>::new(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.success()
|
||||||
|
.unwrap();
|
||||||
|
for entry in rs {
|
||||||
|
println!("{:#?}", SearchEntry::construct(entry));
|
||||||
|
}
|
||||||
|
ldap.unbind().await.unwrap()
|
||||||
|
}
|
||||||
|
}
|
119
crates/directory/src/smtp/lookup.rs
Normal file
119
crates/directory/src/smtp/lookup.rs
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
use mail_send::{smtp::AssertReply, Credentials};
|
||||||
|
use smtp_proto::Severity;
|
||||||
|
|
||||||
|
use crate::{Directory, DirectoryError, Principal};
|
||||||
|
|
||||||
|
use super::{SmtpClient, SmtpDirectory};
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Directory for SmtpDirectory {
|
||||||
|
async fn authenticate(
|
||||||
|
&self,
|
||||||
|
credentials: &Credentials<String>,
|
||||||
|
) -> crate::Result<Option<Principal>> {
|
||||||
|
self.pool.get().await?.authenticate(credentials).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn principal_by_name(&self, _name: &str) -> crate::Result<Option<Principal>> {
|
||||||
|
Err(DirectoryError::unsupported("smtp", "principal_by_name"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn principal_by_id(&self, _id: u32) -> crate::Result<Option<Principal>> {
|
||||||
|
Err(DirectoryError::unsupported("smtp", "principal_by_id"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn member_of(&self, _principal: &Principal) -> crate::Result<Vec<u32>> {
|
||||||
|
Err(DirectoryError::unsupported("smtp", "member_of"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn emails_by_id(&self, _id: u32) -> crate::Result<Vec<String>> {
|
||||||
|
Err(DirectoryError::unsupported("smtp", "emails_by_id"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ids_by_email(&self, _address: &str) -> crate::Result<Vec<u32>> {
|
||||||
|
Err(DirectoryError::unsupported("smtp", "ids_by_email"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rcpt(&self, address: &str) -> crate::Result<bool> {
|
||||||
|
let mut conn = self.pool.get().await?;
|
||||||
|
if !conn.sent_mail_from {
|
||||||
|
conn.client
|
||||||
|
.cmd(b"MAIL FROM:<>\r\n")
|
||||||
|
.await?
|
||||||
|
.assert_positive_completion()?;
|
||||||
|
conn.sent_mail_from = true;
|
||||||
|
}
|
||||||
|
let reply = conn
|
||||||
|
.client
|
||||||
|
.cmd(format!("RCPT TO:<{address}>\r\n").as_bytes())
|
||||||
|
.await?;
|
||||||
|
match reply.severity() {
|
||||||
|
Severity::PositiveCompletion => {
|
||||||
|
conn.num_rcpts += 1;
|
||||||
|
if conn.num_rcpts >= conn.max_rcpt {
|
||||||
|
let _ = conn.client.rset().await;
|
||||||
|
conn.num_rcpts = 0;
|
||||||
|
conn.sent_mail_from = false;
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
Severity::PermanentNegativeCompletion => Ok(false),
|
||||||
|
_ => Err(mail_send::Error::UnexpectedReply(reply).into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
|
||||||
|
self.pool
|
||||||
|
.get()
|
||||||
|
.await?
|
||||||
|
.expand(&format!("VRFY {address}\r\n"))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
|
||||||
|
self.pool
|
||||||
|
.get()
|
||||||
|
.await?
|
||||||
|
.expand(&format!("EXPN {address}\r\n"))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(&self, _query: &str, _params: &[&str]) -> crate::Result<bool> {
|
||||||
|
Err(DirectoryError::unsupported("smtp", "query"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SmtpClient {
|
||||||
|
async fn authenticate(
|
||||||
|
&mut self,
|
||||||
|
credentials: &Credentials<String>,
|
||||||
|
) -> crate::Result<Option<Principal>> {
|
||||||
|
match self
|
||||||
|
.client
|
||||||
|
.authenticate(credentials, &self.capabilities)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => Ok(Some(Principal::default())),
|
||||||
|
Err(err) => match &err {
|
||||||
|
mail_send::Error::AuthenticationFailed(err) if err.code() == 535 => {
|
||||||
|
self.num_auth_failures += 1;
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
_ => Err(err.into()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn expand(&mut self, command: &str) -> crate::Result<Vec<String>> {
|
||||||
|
let reply = self.client.cmd(command.as_bytes()).await?;
|
||||||
|
match reply.code() {
|
||||||
|
250 | 251 => Ok(reply
|
||||||
|
.message()
|
||||||
|
.split('\n')
|
||||||
|
.map(|p| p.to_string())
|
||||||
|
.collect::<Vec<String>>()),
|
||||||
|
550 | 551 | 553 | 500 | 502 => Err(DirectoryError::Unsupported),
|
||||||
|
_ => Err(mail_send::Error::UnexpectedReply(reply).into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
crates/directory/src/smtp/mod.rs
Normal file
28
crates/directory/src/smtp/mod.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
pub mod lookup;
|
||||||
|
pub mod pool;
|
||||||
|
|
||||||
|
use bb8::Pool;
|
||||||
|
use mail_send::SmtpClientBuilder;
|
||||||
|
use smtp_proto::EhloResponse;
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tokio_rustls::client::TlsStream;
|
||||||
|
|
||||||
|
pub struct SmtpDirectory {
|
||||||
|
pool: Pool<SmtpConnectionManager>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SmtpConnectionManager {
|
||||||
|
builder: SmtpClientBuilder<String>,
|
||||||
|
max_rcpt: usize,
|
||||||
|
max_auth_errors: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SmtpClient {
|
||||||
|
client: mail_send::SmtpClient<TlsStream<TcpStream>>,
|
||||||
|
capabilities: EhloResponse<String>,
|
||||||
|
max_rcpt: usize,
|
||||||
|
max_auth_errors: usize,
|
||||||
|
num_rcpts: usize,
|
||||||
|
num_auth_failures: usize,
|
||||||
|
sent_mail_from: bool,
|
||||||
|
}
|
41
crates/directory/src/smtp/pool.rs
Normal file
41
crates/directory/src/smtp/pool.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
use bb8::ManageConnection;
|
||||||
|
use mail_send::{smtp::AssertReply, Error};
|
||||||
|
|
||||||
|
use super::{SmtpClient, SmtpConnectionManager};
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl ManageConnection for SmtpConnectionManager {
|
||||||
|
type Connection = SmtpClient;
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
/// Attempts to create a new connection.
|
||||||
|
async fn connect(&self) -> Result<Self::Connection, Self::Error> {
|
||||||
|
let mut client = self.builder.connect().await?;
|
||||||
|
let capabilities = client
|
||||||
|
.capabilities(&self.builder.local_host, self.builder.is_lmtp)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(SmtpClient {
|
||||||
|
capabilities,
|
||||||
|
client,
|
||||||
|
max_auth_errors: self.max_auth_errors,
|
||||||
|
max_rcpt: self.max_rcpt,
|
||||||
|
num_rcpts: 0,
|
||||||
|
num_auth_failures: 0,
|
||||||
|
sent_mail_from: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines if the connection is still connected to the database.
|
||||||
|
async fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Self::Error> {
|
||||||
|
conn.client
|
||||||
|
.cmd(b"NOOP\r\n")
|
||||||
|
.await?
|
||||||
|
.assert_positive_completion()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synchronously determine if the connection is no longer usable, if possible.
|
||||||
|
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
|
||||||
|
conn.num_auth_failures >= conn.max_auth_errors
|
||||||
|
}
|
||||||
|
}
|
157
crates/directory/src/sql/lookup.rs
Normal file
157
crates/directory/src/sql/lookup.rs
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
use mail_send::Credentials;
|
||||||
|
use sqlx::{any::AnyRow, Column, Row};
|
||||||
|
|
||||||
|
use crate::{Directory, Principal, Type};
|
||||||
|
|
||||||
|
use super::{SqlDirectory, SqlMappings};
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Directory for SqlDirectory {
|
||||||
|
async fn authenticate(
|
||||||
|
&self,
|
||||||
|
credentials: &Credentials<String>,
|
||||||
|
) -> crate::Result<Option<Principal>> {
|
||||||
|
let (username, secret) = match credentials {
|
||||||
|
Credentials::Plain { username, secret } => (username, secret),
|
||||||
|
Credentials::OAuthBearer { token } => (token, token),
|
||||||
|
Credentials::XOauth2 { username, secret } => (username, secret),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(row) = sqlx::query(&self.mappings.query_login)
|
||||||
|
.bind(username)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
self.mappings.row_to_principal(row).map(|p| {
|
||||||
|
if p.secret.as_ref().map_or(false, |s| s == secret) {
|
||||||
|
Some(p)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn principal_by_name(&self, name: &str) -> crate::Result<Option<Principal>> {
|
||||||
|
if let Some(row) = sqlx::query(&self.mappings.query_name)
|
||||||
|
.bind(name)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
self.mappings.row_to_principal(row).map(Some)
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn principal_by_id(&self, id: u32) -> crate::Result<Option<Principal>> {
|
||||||
|
if let Some(row) = sqlx::query(&self.mappings.query_id)
|
||||||
|
.bind(id as i64)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
self.mappings.row_to_principal(row).map(Some)
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn member_of(&self, principal: &Principal) -> crate::Result<Vec<u32>> {
|
||||||
|
sqlx::query_scalar::<_, i64>(&self.mappings.query_members)
|
||||||
|
.bind(principal.id as i64)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map(|ids| ids.into_iter().map(|id| id as u32).collect())
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn emails_by_id(&self, id: u32) -> crate::Result<Vec<String>> {
|
||||||
|
sqlx::query_scalar::<_, String>(&self.mappings.query_emails)
|
||||||
|
.bind(id as i64)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ids_by_email(&self, address: &str) -> crate::Result<Vec<u32>> {
|
||||||
|
sqlx::query_scalar::<_, i64>(&self.mappings.query_recipients)
|
||||||
|
.bind(address)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map(|ids| ids.into_iter().map(|id| id as u32).collect())
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rcpt(&self, address: &str) -> crate::Result<bool> {
|
||||||
|
sqlx::query_scalar::<_, i64>(&self.mappings.query_recipients)
|
||||||
|
.bind(address)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await
|
||||||
|
.map(|id| id.is_some())
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
|
||||||
|
sqlx::query_scalar::<_, String>(&self.mappings.query_verify)
|
||||||
|
.bind(address)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
|
||||||
|
sqlx::query_scalar::<_, String>(&self.mappings.query_expand)
|
||||||
|
.bind(address)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(&self, query: &str, params: &[&str]) -> crate::Result<bool> {
|
||||||
|
let mut q = sqlx::query(query);
|
||||||
|
for param in params {
|
||||||
|
q = q.bind(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
q.fetch_optional(&self.pool)
|
||||||
|
.await
|
||||||
|
.map(|r| r.is_some())
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SqlMappings {
|
||||||
|
pub fn row_to_principal(&self, row: AnyRow) -> crate::Result<Principal> {
|
||||||
|
let mut principal = Principal {
|
||||||
|
id: u32::MAX,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
for col in row.columns() {
|
||||||
|
let name = col.name();
|
||||||
|
let idx = col.ordinal();
|
||||||
|
if name.eq_ignore_ascii_case(&self.column_id) {
|
||||||
|
principal.id = row.try_get::<i64, _>(idx)? as u32;
|
||||||
|
} else if name.eq_ignore_ascii_case(&self.column_name) {
|
||||||
|
principal.name = row.try_get::<Option<String>, _>(idx)?.unwrap_or_default();
|
||||||
|
} else if name.eq_ignore_ascii_case(&self.column_secret) {
|
||||||
|
principal.secret = row.try_get::<Option<String>, _>(idx)?;
|
||||||
|
} else if name.eq_ignore_ascii_case(&self.column_type) {
|
||||||
|
if let Some(typ) = row.try_get::<Option<String>, _>(idx)? {
|
||||||
|
match typ.as_str() {
|
||||||
|
"individual" | "person" | "user" => principal.typ = Type::Individual,
|
||||||
|
"group" => principal.typ = Type::Group,
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if name.eq_ignore_ascii_case(&self.column_description) {
|
||||||
|
principal.description = row.try_get::<Option<String>, _>(idx)?;
|
||||||
|
} else if name.eq_ignore_ascii_case(&self.column_quota) {
|
||||||
|
principal.quota = row.try_get::<i64, _>(idx)? as u32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(principal)
|
||||||
|
}
|
||||||
|
}
|
25
crates/directory/src/sql/mod.rs
Normal file
25
crates/directory/src/sql/mod.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use sqlx::{Any, Pool};
|
||||||
|
|
||||||
|
pub mod lookup;
|
||||||
|
|
||||||
|
pub struct SqlDirectory {
|
||||||
|
pool: Pool<Any>,
|
||||||
|
mappings: SqlMappings,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct SqlMappings {
|
||||||
|
query_login: String,
|
||||||
|
query_name: String,
|
||||||
|
query_id: String,
|
||||||
|
query_members: String,
|
||||||
|
query_recipients: String,
|
||||||
|
query_emails: String,
|
||||||
|
query_verify: String,
|
||||||
|
query_expand: String,
|
||||||
|
column_name: String,
|
||||||
|
column_description: String,
|
||||||
|
column_secret: String,
|
||||||
|
column_id: String,
|
||||||
|
column_quota: String,
|
||||||
|
column_type: String,
|
||||||
|
}
|
|
@ -7,8 +7,9 @@ resolver = "2"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
store = { path = "../store" }
|
store = { path = "../store" }
|
||||||
jmap_proto = { path = "../jmap-proto" }
|
jmap_proto = { path = "../jmap-proto" }
|
||||||
utils = { path = "../utils" }
|
|
||||||
smtp = { path = "../smtp" }
|
smtp = { path = "../smtp" }
|
||||||
|
utils = { path = "../utils" }
|
||||||
|
directory = { path = "../directory" }
|
||||||
smtp-proto = { git = "https://github.com/stalwartlabs/smtp-proto" }
|
smtp-proto = { git = "https://github.com/stalwartlabs/smtp-proto" }
|
||||||
mail-parser = { git = "https://github.com/stalwartlabs/mail-parser", features = ["full_encoding", "serde_support", "ludicrous_mode"] }
|
mail-parser = { git = "https://github.com/stalwartlabs/mail-parser", features = ["full_encoding", "serde_support", "ludicrous_mode"] }
|
||||||
mail-builder = { git = "https://github.com/stalwartlabs/mail-builder", features = ["ludicrous_mode"] }
|
mail-builder = { git = "https://github.com/stalwartlabs/mail-builder", features = ["ludicrous_mode"] }
|
||||||
|
|
Loading…
Reference in a new issue