Spiegel von
https://github.com/dani-garcia/vaultwarden.git
synchronisiert 2025-03-12 16:47:03 +01:00
Add AWS SES for sending emails
Dieser Commit ist enthalten in:
Ursprung
9a9786e370
Commit
ed26fa3640
6 geänderte Dateien mit 93 neuen und 13 gelöschten Zeilen
35
Cargo.lock
generiert
35
Cargo.lock
generiert
|
@ -405,9 +405,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-runtime"
|
name = "aws-runtime"
|
||||||
version = "1.5.4"
|
version = "1.5.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bee7643696e7fdd74c10f9eb42848a87fe469d35eae9c3323f80aa98f350baac"
|
checksum = "76dd04d39cc12844c0994f2c9c5a6f5184c22e9188ec1ff723de41910a21dcad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-credential-types",
|
"aws-credential-types",
|
||||||
"aws-sigv4",
|
"aws-sigv4",
|
||||||
|
@ -488,6 +488,28 @@ dependencies = [
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aws-sdk-sesv2"
|
||||||
|
version = "1.65.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5771f0ab7f960545a569d2a916363423e2faebeb01f0cc22716d25c6088f0a7b"
|
||||||
|
dependencies = [
|
||||||
|
"aws-credential-types",
|
||||||
|
"aws-runtime",
|
||||||
|
"aws-smithy-async",
|
||||||
|
"aws-smithy-http",
|
||||||
|
"aws-smithy-json",
|
||||||
|
"aws-smithy-runtime",
|
||||||
|
"aws-smithy-runtime-api",
|
||||||
|
"aws-smithy-types",
|
||||||
|
"aws-types",
|
||||||
|
"bytes",
|
||||||
|
"http 0.2.12",
|
||||||
|
"once_cell",
|
||||||
|
"regex-lite",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-sdk-sso"
|
name = "aws-sdk-sso"
|
||||||
version = "1.52.0"
|
version = "1.52.0"
|
||||||
|
@ -557,9 +579,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-sigv4"
|
name = "aws-sigv4"
|
||||||
version = "1.2.7"
|
version = "1.2.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "690118821e46967b3c4501d67d7d52dd75106a9c54cf36cefa1985cedbe94e05"
|
checksum = "0bc5bbd1e4a2648fd8c5982af03935972c24a2f9846b396de661d351ee3ce837"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-credential-types",
|
"aws-credential-types",
|
||||||
"aws-smithy-eventstream",
|
"aws-smithy-eventstream",
|
||||||
|
@ -749,9 +771,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-types"
|
name = "aws-types"
|
||||||
version = "1.3.4"
|
version = "1.3.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b0df5a18c4f951c645300d365fec53a61418bcf4650f604f85fe2a665bfaa0c2"
|
checksum = "dfbd0a668309ec1f66c0f6bda4840dd6d4796ae26d699ebc266d7cc95c6d040f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-credential-types",
|
"aws-credential-types",
|
||||||
"aws-smithy-async",
|
"aws-smithy-async",
|
||||||
|
@ -4936,6 +4958,7 @@ dependencies = [
|
||||||
"aws-config",
|
"aws-config",
|
||||||
"aws-sdk-dsql",
|
"aws-sdk-dsql",
|
||||||
"aws-sdk-s3",
|
"aws-sdk-s3",
|
||||||
|
"aws-sdk-sesv2",
|
||||||
"bigdecimal",
|
"bigdecimal",
|
||||||
"bytes",
|
"bytes",
|
||||||
"cached",
|
"cached",
|
||||||
|
|
|
@ -20,10 +20,11 @@ build = "build.rs"
|
||||||
enable_syslog = []
|
enable_syslog = []
|
||||||
mysql = ["diesel/mysql", "diesel_migrations/mysql"]
|
mysql = ["diesel/mysql", "diesel_migrations/mysql"]
|
||||||
postgresql = ["diesel/postgres", "diesel_migrations/postgres"]
|
postgresql = ["diesel/postgres", "diesel_migrations/postgres"]
|
||||||
aws = ["dsql", "s3"]
|
aws = ["dsql", "s3", "ses"]
|
||||||
dsql = ["postgresql", "dep:aws-config", "dep:aws-sdk-dsql"]
|
dsql = ["postgresql", "dep:aws-config", "dep:aws-sdk-dsql"]
|
||||||
sqlite = ["diesel/sqlite", "diesel_migrations/sqlite", "dep:libsqlite3-sys"]
|
sqlite = ["diesel/sqlite", "diesel_migrations/sqlite", "dep:libsqlite3-sys"]
|
||||||
s3 = ["dep:aws-config", "dep:aws-sdk-s3"]
|
s3 = ["dep:aws-config", "dep:aws-sdk-s3"]
|
||||||
|
ses = ["dep:aws-config", "dep:aws-sdk-sesv2"]
|
||||||
# Enable to use a vendored and statically linked openssl
|
# Enable to use a vendored and statically linked openssl
|
||||||
vendored_openssl = ["openssl/vendored"]
|
vendored_openssl = ["openssl/vendored"]
|
||||||
# Enable MiMalloc memory allocator to replace the default malloc
|
# Enable MiMalloc memory allocator to replace the default malloc
|
||||||
|
@ -91,10 +92,11 @@ diesel-derive-newtype = "2.1.2"
|
||||||
# Bundled/Static SQLite
|
# Bundled/Static SQLite
|
||||||
libsqlite3-sys = { version = "0.31.0", features = ["bundled"], optional = true }
|
libsqlite3-sys = { version = "0.31.0", features = ["bundled"], optional = true }
|
||||||
|
|
||||||
# AWS / Amazon Aurora DSQL
|
# AWS Services
|
||||||
aws-config = { version = "1.5.12", features = ["behavior-version-latest"], optional = true }
|
aws-config = { version = "1.5.12", features = ["behavior-version-latest"], optional = true }
|
||||||
aws-sdk-s3 = { version = "1.72.0", features = ["behavior-version-latest"], optional = true }
|
aws-sdk-s3 = { version = "1.72.0", features = ["behavior-version-latest"], optional = true }
|
||||||
aws-sdk-dsql = { version = "1.2.0", features = ["behavior-version-latest"], optional = true }
|
aws-sdk-dsql = { version = "1.2.0", features = ["behavior-version-latest"], optional = true }
|
||||||
|
aws-sdk-sesv2 = { version = "1.65.0", features = ["behavior-version-latest"], optional = true }
|
||||||
|
|
||||||
# Crypto-related libraries
|
# Crypto-related libraries
|
||||||
rand = "0.9.0"
|
rand = "0.9.0"
|
||||||
|
|
3
build.rs
3
build.rs
|
@ -15,6 +15,8 @@ fn main() {
|
||||||
println!("cargo:rustc-cfg=query_logger");
|
println!("cargo:rustc-cfg=query_logger");
|
||||||
#[cfg(feature = "s3")]
|
#[cfg(feature = "s3")]
|
||||||
println!("cargo:rustc-cfg=s3");
|
println!("cargo:rustc-cfg=s3");
|
||||||
|
#[cfg(feature = "ses")]
|
||||||
|
println!("cargo:rustc-cfg=ses");
|
||||||
#[cfg(feature = "aws")]
|
#[cfg(feature = "aws")]
|
||||||
println!("cargo:rustc-cfg=aws");
|
println!("cargo:rustc-cfg=aws");
|
||||||
|
|
||||||
|
@ -31,6 +33,7 @@ fn main() {
|
||||||
println!("cargo::rustc-check-cfg=cfg(dsql)");
|
println!("cargo::rustc-check-cfg=cfg(dsql)");
|
||||||
println!("cargo::rustc-check-cfg=cfg(query_logger)");
|
println!("cargo::rustc-check-cfg=cfg(query_logger)");
|
||||||
println!("cargo::rustc-check-cfg=cfg(s3)");
|
println!("cargo::rustc-check-cfg=cfg(s3)");
|
||||||
|
println!("cargo::rustc-check-cfg=cfg(ses)");
|
||||||
println!("cargo::rustc-check-cfg=cfg(aws)");
|
println!("cargo::rustc-check-cfg=cfg(aws)");
|
||||||
|
|
||||||
// Rerun when these paths are changed.
|
// Rerun when these paths are changed.
|
||||||
|
|
|
@ -741,12 +741,14 @@ make_config! {
|
||||||
smtp_accept_invalid_certs: bool, true, def, false;
|
smtp_accept_invalid_certs: bool, true, def, false;
|
||||||
/// Accept Invalid Hostnames (Know the risks!) |> DANGEROUS: Allow invalid hostnames. This option introduces significant vulnerabilities to man-in-the-middle attacks!
|
/// Accept Invalid Hostnames (Know the risks!) |> DANGEROUS: Allow invalid hostnames. This option introduces significant vulnerabilities to man-in-the-middle attacks!
|
||||||
smtp_accept_invalid_hostnames: bool, true, def, false;
|
smtp_accept_invalid_hostnames: bool, true, def, false;
|
||||||
|
/// Use AWS SES |> Whether to send mail via AWS Simple Email Service (SES)
|
||||||
|
use_aws_ses: bool, true, def, false;
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Email 2FA Settings
|
/// Email 2FA Settings
|
||||||
email_2fa: _enable_email_2fa {
|
email_2fa: _enable_email_2fa {
|
||||||
/// Enabled |> Disabling will prevent users from setting up new email 2FA and using existing email 2FA configured
|
/// Enabled |> Disabling will prevent users from setting up new email 2FA and using existing email 2FA configured
|
||||||
_enable_email_2fa: bool, true, auto, |c| c._enable_smtp && (c.smtp_host.is_some() || c.use_sendmail);
|
_enable_email_2fa: bool, true, auto, |c| c._enable_smtp && (c.smtp_host.is_some() || c.use_sendmail || c.use_aws_ses);
|
||||||
/// Email token size |> Number of digits in an email 2FA token (min: 6, max: 255). Note that the Bitwarden clients are hardcoded to mention 6 digit codes regardless of this setting.
|
/// Email token size |> Number of digits in an email 2FA token (min: 6, max: 255). Note that the Bitwarden clients are hardcoded to mention 6 digit codes regardless of this setting.
|
||||||
email_token_size: u8, true, def, 6;
|
email_token_size: u8, true, def, 6;
|
||||||
/// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token.
|
/// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token.
|
||||||
|
@ -951,6 +953,9 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if cfg.use_aws_ses {
|
||||||
|
#[cfg(not(ses))]
|
||||||
|
err!("`USE_AWS_SES` is set, but the `ses` feature is not enabled in this build");
|
||||||
} else {
|
} else {
|
||||||
if cfg.smtp_host.is_some() == cfg.smtp_from.is_empty() {
|
if cfg.smtp_host.is_some() == cfg.smtp_from.is_empty() {
|
||||||
err!("Both `SMTP_HOST` and `SMTP_FROM` need to be set for email support without `USE_SENDMAIL`")
|
err!("Both `SMTP_HOST` and `SMTP_FROM` need to be set for email support without `USE_SENDMAIL`")
|
||||||
|
@ -961,7 +966,7 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cfg.smtp_host.is_some() || cfg.use_sendmail) && !is_valid_email(&cfg.smtp_from) {
|
if (cfg.smtp_host.is_some() || cfg.use_sendmail || cfg.use_aws_ses) && !is_valid_email(&cfg.smtp_from) {
|
||||||
err!(format!("SMTP_FROM '{}' is not a valid email address", cfg.smtp_from))
|
err!(format!("SMTP_FROM '{}' is not a valid email address", cfg.smtp_from))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -970,7 +975,7 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg._enable_email_2fa && !(cfg.smtp_host.is_some() || cfg.use_sendmail) {
|
if cfg._enable_email_2fa && !(cfg.smtp_host.is_some() || cfg.use_sendmail || cfg.use_aws_ses) {
|
||||||
err!("To enable email 2FA, a mail transport must be configured")
|
err!("To enable email 2FA, a mail transport must be configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1288,7 +1293,7 @@ impl Config {
|
||||||
}
|
}
|
||||||
pub fn mail_enabled(&self) -> bool {
|
pub fn mail_enabled(&self) -> bool {
|
||||||
let inner = &self.inner.read().unwrap().config;
|
let inner = &self.inner.read().unwrap().config;
|
||||||
inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail)
|
inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail || inner.use_aws_ses)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_duo_akey(&self) -> String {
|
pub async fn get_duo_akey(&self) -> String {
|
||||||
|
|
47
src/mail.rs
47
src/mail.rs
|
@ -95,6 +95,44 @@ fn smtp_transport() -> AsyncSmtpTransport<Tokio1Executor> {
|
||||||
smtp_client.build()
|
smtp_client.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(ses)]
|
||||||
|
async fn send_with_aws_ses(email: Message) -> std::io::Result<()> {
|
||||||
|
use std::io::Error;
|
||||||
|
use aws_sdk_sesv2::{types::{EmailContent, RawMessage}, Client};
|
||||||
|
use crate::aws::aws_sdk_config;
|
||||||
|
|
||||||
|
fn sesv2_client() -> std::io::Result<Client> {
|
||||||
|
static AWS_SESV2_CLIENT: std::sync::LazyLock<std::io::Result<Client>> = std::sync::LazyLock::new(|| {
|
||||||
|
Ok(Client::new(aws_sdk_config()?))
|
||||||
|
});
|
||||||
|
|
||||||
|
(*AWS_SESV2_CLIENT)
|
||||||
|
.as_ref()
|
||||||
|
.map(|client| client.clone())
|
||||||
|
.map_err(|e| match e.get_ref() {
|
||||||
|
Some(inner) => Error::new(e.kind(), inner),
|
||||||
|
None => Error::from(e.kind()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sesv2_client()?
|
||||||
|
.send_email()
|
||||||
|
.content(
|
||||||
|
EmailContent::builder().raw(
|
||||||
|
RawMessage::builder()
|
||||||
|
.data(email.formatted().into())
|
||||||
|
.build()
|
||||||
|
.map_err(|e| Error::other(format!("Failed to build AWS SESv2 RawMessage: {e:#?}")))?
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::other(e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// This will sanitize the string values by stripping all the html tags to prevent XSS and HTML Injections
|
// This will sanitize the string values by stripping all the html tags to prevent XSS and HTML Injections
|
||||||
fn sanitize_data(data: &mut serde_json::Value) {
|
fn sanitize_data(data: &mut serde_json::Value) {
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
@ -605,6 +643,15 @@ async fn send_with_selected_transport(email: Message) -> EmptyResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if CONFIG.use_aws_ses() {
|
||||||
|
#[cfg(ses)]
|
||||||
|
match send_with_aws_ses(email).await {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => err!("Failed to send email", format!("Failed to send email using AWS SES: {e:?}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(ses))]
|
||||||
|
err!("Failed to send email", "Failed to send email using AWS SES: `ses` feature is not enabled");
|
||||||
} else {
|
} else {
|
||||||
match smtp_transport().send(email).await {
|
match smtp_transport().send(email).await {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
|
|
|
@ -45,7 +45,7 @@ use tokio::{
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use tokio::signal::unix::SignalKind;
|
use tokio::signal::unix::SignalKind;
|
||||||
|
|
||||||
#[cfg(any(dsql, s3))]
|
#[cfg(any(dsql, s3, ses))]
|
||||||
mod aws;
|
mod aws;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
|
Laden …
Tabelle hinzufügen
In neuem Issue referenzieren