Config can now be serialized / deserialized

This commit is contained in:
Daniel García 2019-02-02 01:09:21 +01:00
parent 20d8d800f3
commit 86ed75bf7c
No known key found for this signature in database
GPG Key ID: FC8A7D14C3CD543A
8 changed files with 207 additions and 261 deletions

View File

@ -24,6 +24,8 @@ pub fn routes() -> Vec<Route> {
invite_user,
delete_user,
deauth_user,
get_config,
post_config,
]
}
@ -136,11 +138,11 @@ fn invite_user(data: JsonUpcase<InviteData>, _token: AdminToken, conn: DbConn) -
err!("Invitations are not allowed")
}
if let Some(ref mail_config) = CONFIG.mail() {
if CONFIG.mail_enabled() {
let mut user = User::new(email);
user.save(&conn)?;
let org_name = "bitwarden_rs";
mail::send_invite(&user.email, &user.uuid, None, None, &org_name, None, mail_config)
mail::send_invite(&user.email, &user.uuid, None, None, &org_name, None)
} else {
let mut invitation = Invitation::new(data.Email);
invitation.save(&conn)
@ -169,6 +171,20 @@ fn deauth_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
user.save(&conn)
}
#[get("/config")]
fn get_config(_token: AdminToken) -> EmptyResult {
unimplemented!("Get config")
}
#[post("/config", data = "<data>")]
fn post_config(data: JsonUpcase<Value>, _token: AdminToken) -> EmptyResult {
let data: Value = data.into_inner().data;
info!("CONFIG: {:#?}", data);
unimplemented!("Update config")
}
pub struct AdminToken {}
impl<'a, 'r> FromRequest<'a, 'r> for AdminToken {

View File

@ -419,8 +419,8 @@ fn password_hint(data: JsonUpcase<PasswordHintData>, conn: DbConn) -> EmptyResul
None => return Ok(()),
};
if let Some(ref mail_config) = CONFIG.mail() {
mail::send_password_hint(&data.Email, hint, mail_config)?;
if CONFIG.mail_enabled() {
mail::send_password_hint(&data.Email, hint)?;
} else if CONFIG.show_password_hint() {
if let Some(hint) = hint {
err!(format!("Your password hint is: {}", &hint));

View File

@ -486,9 +486,9 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
}
for email in data.Emails.iter() {
let mut user_org_status = match CONFIG.mail() {
Some(_) => UserOrgStatus::Invited as i32,
None => UserOrgStatus::Accepted as i32, // Automatically mark user as accepted if no email invites
let mut user_org_status = match CONFIG.mail_enabled() {
true => UserOrgStatus::Invited as i32,
false => UserOrgStatus::Accepted as i32, // Automatically mark user as accepted if no email invites
};
let user = match User::find_by_mail(&email, &conn) {
None => {
@ -496,7 +496,7 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
err!(format!("User email does not exist: {}", email))
}
if CONFIG.mail().is_none() {
if !CONFIG.mail_enabled() {
let mut invitation = Invitation::new(email.clone());
invitation.save(&conn)?;
}
@ -535,7 +535,7 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
new_user.save(&conn)?;
if let Some(ref mail_config) = CONFIG.mail() {
if CONFIG.mail_enabled() {
let org_name = match Organization::find_by_uuid(&org_id, &conn) {
Some(org) => org.name,
None => err!("Error looking up organization"),
@ -548,7 +548,6 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
Some(new_user.uuid),
&org_name,
Some(headers.user.email.clone()),
mail_config,
)?;
}
}
@ -562,7 +561,7 @@ fn reinvite_user(org_id: String, user_org: String, headers: AdminHeaders, conn:
err!("Invitations are not allowed.")
}
if CONFIG.mail().is_none() {
if !CONFIG.mail_enabled() {
err!("SMTP is not configured.")
}
@ -585,7 +584,7 @@ fn reinvite_user(org_id: String, user_org: String, headers: AdminHeaders, conn:
None => err!("Error looking up organization."),
};
if let Some(ref mail_config) = CONFIG.mail() {
if CONFIG.mail_enabled() {
mail::send_invite(
&user.email,
&user.uuid,
@ -593,7 +592,6 @@ fn reinvite_user(org_id: String, user_org: String, headers: AdminHeaders, conn:
Some(user_org.uuid),
&org_name,
Some(headers.user.email),
mail_config,
)?;
} else {
let mut invitation = Invitation::new(user.email.clone());
@ -637,7 +635,7 @@ fn accept_invite(_org_id: String, _org_user_id: String, data: JsonUpcase<AcceptD
None => err!("Invited user not found"),
}
if let Some(ref mail_config) = CONFIG.mail() {
if CONFIG.mail_enabled() {
let mut org_name = String::from("bitwarden_rs");
if let Some(org_id) = &claims.org_id {
org_name = match Organization::find_by_uuid(&org_id, &conn) {
@ -647,10 +645,10 @@ fn accept_invite(_org_id: String, _org_user_id: String, data: JsonUpcase<AcceptD
};
if let Some(invited_by_email) = &claims.invited_by_email {
// User was invited to an organization, so they must be confirmed manually after acceptance
mail::send_invite_accepted(&claims.email, invited_by_email, &org_name, mail_config)?;
mail::send_invite_accepted(&claims.email, invited_by_email, &org_name)?;
} else {
// User was invited from /admin, so they are automatically confirmed
mail::send_invite_confirmed(&claims.email, &org_name, mail_config)?;
mail::send_invite_confirmed(&claims.email, &org_name)?;
}
}
@ -686,7 +684,7 @@ fn confirm_invite(
None => err!("Invalid key provided"),
};
if let Some(ref mail_config) = CONFIG.mail() {
if CONFIG.mail_enabled() {
let org_name = match Organization::find_by_uuid(&org_id, &conn) {
Some(org) => org.name,
None => err!("Error looking up organization."),
@ -695,7 +693,7 @@ fn confirm_invite(
Some(user) => user.email,
None => err!("Error looking up user."),
};
mail::send_invite_confirmed(&address, &org_name, mail_config)?;
mail::send_invite_confirmed(&address, &org_name)?;
}
user_to_confirm.save(&conn)

View File

@ -3,15 +3,14 @@ use rocket_contrib::json::Json;
use serde_json;
use serde_json::Value;
use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
use crate::auth::Headers;
use crate::crypto;
use crate::db::{
models::{TwoFactor, TwoFactorType, User},
DbConn,
};
use crate::crypto;
use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
use crate::auth::Headers;
use crate::error::{Error, MapResult};
use rocket::Route;
@ -508,32 +507,31 @@ fn jsonify_yubikeys(yubikeys: Vec<String>) -> serde_json::Value {
result
}
fn verify_yubikey_otp(otp: String) -> JsonResult {
if !CONFIG.yubico_cred_set() {
err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled")
fn get_yubico_credentials() -> Result<(String, String), Error> {
match (CONFIG.yubico_client_id(), CONFIG.yubico_secret_key()) {
(Some(id), Some(secret)) => Ok((id, secret)),
_ => err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled"),
}
}
fn verify_yubikey_otp(otp: String) -> EmptyResult {
let (yubico_id, yubico_secret) = get_yubico_credentials()?;
let yubico = Yubico::new();
let config = Config::default()
.set_client_id(CONFIG.yubico_client_id())
.set_key(CONFIG.yubico_secret_key());
let config = Config::default().set_client_id(yubico_id).set_key(yubico_secret);
let result = match CONFIG.yubico_server() {
match CONFIG.yubico_server() {
Some(server) => yubico.verify(otp, config.set_api_hosts(vec![server])),
None => yubico.verify(otp, config),
};
match result {
Ok(_answer) => Ok(Json(json!({}))),
Err(_e) => err!("Failed to verify OTP"),
}
.map_res("Failed to verify OTP")
.and(Ok(()))
}
#[post("/two-factor/get-yubikey", data = "<data>")]
fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
if !CONFIG.yubico_cred_set() {
err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled")
}
// Make sure the credentials are set
get_yubico_credentials()?;
let data: PasswordData = data.into_inner().data;
let user = headers.user;
@ -597,11 +595,7 @@ fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn:
continue;
}
let result = verify_yubikey_otp(yubikey.to_owned());
if let Err(_e) = result {
err!("Invalid Yubikey OTP provided");
}
verify_yubikey_otp(yubikey.to_owned()).map_res("Invalid Yubikey OTP provided")?;
}
let yubikey_ids: Vec<String> = yubikeys.into_iter().map(|x| (&x[..12]).to_owned()).collect();

View File

@ -355,7 +355,7 @@ pub fn start_notification_server() -> WebSocketUsers {
thread::spawn(move || {
WebSocket::new(factory)
.unwrap()
.listen(&CONFIG.websocket_url())
.listen((CONFIG.websocket_address().as_str(), CONFIG.websocket_port()))
.unwrap();
});
}

View File

@ -3,77 +3,149 @@ use std::sync::RwLock;
use handlebars::Handlebars;
use crate::error::Error;
use crate::util::IntoResult;
lazy_static! {
pub static ref CONFIG: Config = Config::load();
pub static ref CONFIG: Config = Config::load().unwrap_or_else(|e| {
println!("Error loading config:\n\t{:?}\n", e);
exit(12)
});
}
macro_rules! make_config {
( $( $name:ident: $ty:ty ),+ $(,)* ) => {
( $( $name:ident : $ty:ty $(, $default_fn:expr)? );+ $(;)* ) => {
pub struct Config { inner: RwLock<_Config> }
pub struct Config { inner: RwLock<Inner> }
#[derive(Default)]
struct _Config {
_templates: Handlebars,
$(pub $name: $ty),+
struct Inner {
templates: Handlebars,
config: _Config,
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct _Config { $(pub $name: $ty),+ }
paste::item! {
#[allow(unused)]
impl Config {
#[allow(unused)]
impl Config {
$(
pub fn $name(&self) -> $ty {
self.inner.read().unwrap().config.$name.clone()
}
pub fn [<set_ $name>](&self, value: $ty) {
self.inner.write().unwrap().config.$name = value;
}
)+
pub fn load() -> Result<Self, Error> {
use crate::util::get_env;
dotenv::dotenv().ok();
let mut config = _Config::default();
$(
pub fn $name(&self) -> $ty {
self.inner.read().unwrap().$name.clone()
}
pub fn [<set_ $name>](&self, value: $ty) {
self.inner.write().unwrap().$name = value;
}
config.$name = make_config!{ @expr &stringify!($name).to_uppercase(), $ty, &config, $($default_fn)? };
)+
Ok(Config {
inner: RwLock::new(Inner {
templates: load_templates(&config.templates_folder),
config,
}),
})
}
}
}
};
( @expr $name:expr, $ty:ty, $config:expr, $default_fn:expr ) => {{
match get_env($name) {
Some(v) => v,
None => {
let f: &Fn(&_Config) -> _ = &$default_fn;
f($config).into_result()?
}
}
}};
( @expr $name:expr, $ty:ty, $config:expr, ) => {
get_env($name)
};
}
make_config! {
database_url: String,
icon_cache_folder: String,
attachments_folder: String,
data_folder: String, |_| "data".to_string();
database_url: String, |c| format!("{}/{}", c.data_folder, "db.sqlite3");
icon_cache_folder: String, |c| format!("{}/{}", c.data_folder, "icon_cache");
attachments_folder: String, |c| format!("{}/{}", c.data_folder, "attachments");
templates_folder: String, |c| format!("{}/{}", c.data_folder, "templates");
icon_cache_ttl: u64,
icon_cache_negttl: u64,
rsa_key_filename: String, |c| format!("{}/{}", c.data_folder, "rsa_key");
private_rsa_key: String, |c| format!("{}.der", c.rsa_key_filename);
private_rsa_key_pem: String, |c| format!("{}.pem", c.rsa_key_filename);
public_rsa_key: String, |c| format!("{}.pub.der", c.rsa_key_filename);
private_rsa_key: String,
private_rsa_key_pem: String,
public_rsa_key: String,
websocket_enabled: bool, |_| false;
websocket_address: String, |_| "0.0.0.0".to_string();
websocket_port: u16, |_| 3012;
web_vault_folder: String,
web_vault_enabled: bool,
web_vault_folder: String, |_| "web-vault/".to_string();
web_vault_enabled: bool, |_| true;
websocket_enabled: bool,
websocket_url: String,
icon_cache_ttl: u64, |_| 2_592_000;
icon_cache_negttl: u64, |_| 259_200;
extended_logging: bool,
log_file: Option<String>,
disable_icon_download: bool, |_| false;
signups_allowed: bool, |_| true;
invitations_allowed: bool, |_| true;
password_iterations: i32, |_| 100_000;
show_password_hint: bool, |_| true;
disable_icon_download: bool,
signups_allowed: bool,
invitations_allowed: bool,
admin_token: Option<String>,
password_iterations: i32,
show_password_hint: bool,
domain: String, |_| "http://localhost".to_string();
domain_set: bool, |_| false;
domain: String,
domain_set: bool,
reload_templates: bool, |_| false;
yubico_cred_set: bool,
yubico_client_id: String,
yubico_secret_key: String,
yubico_server: Option<String>,
extended_logging: bool, |_| true;
log_file: Option<String>;
mail: Option<MailConfig>,
templates_folder: String,
reload_templates: bool,
admin_token: Option<String>;
yubico_client_id: Option<String>;
yubico_secret_key: Option<String>;
yubico_server: Option<String>;
// Mail settings
smtp_host: Option<String>;
smtp_ssl: bool, |_| true;
smtp_port: u16, |c| if c.smtp_ssl {587} else {25};
smtp_from: String, |c| if c.smtp_host.is_some() { err!("Please specify SMTP_FROM to enable SMTP support") } else { Ok(String::new() )};
smtp_from_name: String, |_| "Bitwarden_RS".to_string();
smtp_username: Option<String>;
smtp_password: Option<String>;
}
impl Config {
pub fn mail_enabled(&self) -> bool {
self.inner.read().unwrap().config.smtp_host.is_some()
}
pub fn render_template<T: serde::ser::Serialize>(
&self,
name: &str,
data: &T,
) -> Result<String, crate::error::Error> {
if CONFIG.reload_templates() {
warn!("RELOADING TEMPLATES");
let hb = load_templates(CONFIG.templates_folder().as_ref());
hb.render(name, data).map_err(Into::into)
} else {
let hb = &CONFIG.inner.read().unwrap().templates;
hb.render(name, data).map_err(Into::into)
}
}
}
fn load_templates(path: &str) -> Handlebars {
@ -106,140 +178,3 @@ fn load_templates(path: &str) -> Handlebars {
hb
}
impl Config {
pub fn render_template<T: serde::ser::Serialize>(
&self,
name: &str,
data: &T,
) -> Result<String, crate::error::Error> {
if CONFIG.reload_templates() {
warn!("RELOADING TEMPLATES");
let hb = load_templates(CONFIG.templates_folder().as_ref());
hb.render(name, data).map_err(Into::into)
} else {
let hb = &CONFIG.inner.read().unwrap()._templates;
hb.render(name, data).map_err(Into::into)
}
}
fn load() -> Self {
use crate::util::{get_env, get_env_or};
dotenv::dotenv().ok();
let df = get_env_or("DATA_FOLDER", "data".to_string());
let key = get_env_or("RSA_KEY_FILENAME", format!("{}/{}", &df, "rsa_key"));
let domain = get_env("DOMAIN");
let yubico_client_id = get_env("YUBICO_CLIENT_ID");
let yubico_secret_key = get_env("YUBICO_SECRET_KEY");
let templates_folder = get_env_or("TEMPLATES_FOLDER", format!("{}/{}", &df, "templates"));
let cfg = _Config {
database_url: get_env_or("DATABASE_URL", format!("{}/{}", &df, "db.sqlite3")),
icon_cache_folder: get_env_or("ICON_CACHE_FOLDER", format!("{}/{}", &df, "icon_cache")),
attachments_folder: get_env_or("ATTACHMENTS_FOLDER", format!("{}/{}", &df, "attachments")),
_templates: load_templates(&templates_folder),
templates_folder,
reload_templates: get_env_or("RELOAD_TEMPLATES", false),
// icon_cache_ttl defaults to 30 days (30 * 24 * 60 * 60 seconds)
icon_cache_ttl: get_env_or("ICON_CACHE_TTL", 2_592_000),
// icon_cache_negttl defaults to 3 days (3 * 24 * 60 * 60 seconds)
icon_cache_negttl: get_env_or("ICON_CACHE_NEGTTL", 259_200),
private_rsa_key: format!("{}.der", &key),
private_rsa_key_pem: format!("{}.pem", &key),
public_rsa_key: format!("{}.pub.der", &key),
web_vault_folder: get_env_or("WEB_VAULT_FOLDER", "web-vault/".into()),
web_vault_enabled: get_env_or("WEB_VAULT_ENABLED", true),
websocket_enabled: get_env_or("WEBSOCKET_ENABLED", false),
websocket_url: format!(
"{}:{}",
get_env_or("WEBSOCKET_ADDRESS", "0.0.0.0".to_string()),
get_env_or("WEBSOCKET_PORT", 3012)
),
extended_logging: get_env_or("EXTENDED_LOGGING", true),
log_file: get_env("LOG_FILE"),
disable_icon_download: get_env_or("DISABLE_ICON_DOWNLOAD", false),
signups_allowed: get_env_or("SIGNUPS_ALLOWED", true),
admin_token: get_env("ADMIN_TOKEN"),
invitations_allowed: get_env_or("INVITATIONS_ALLOWED", true),
password_iterations: get_env_or("PASSWORD_ITERATIONS", 100_000),
show_password_hint: get_env_or("SHOW_PASSWORD_HINT", true),
domain_set: domain.is_some(),
domain: domain.unwrap_or("http://localhost".into()),
yubico_cred_set: yubico_client_id.is_some() && yubico_secret_key.is_some(),
yubico_client_id: yubico_client_id.unwrap_or("00000".into()),
yubico_secret_key: yubico_secret_key.unwrap_or("AAAAAAA".into()),
yubico_server: get_env("YUBICO_SERVER"),
mail: MailConfig::load(),
};
Config {
inner: RwLock::new(cfg),
}
}
}
#[derive(Debug, Clone)]
pub struct MailConfig {
pub smtp_host: String,
pub smtp_port: u16,
pub smtp_ssl: bool,
pub smtp_from: String,
pub smtp_from_name: String,
pub smtp_username: Option<String>,
pub smtp_password: Option<String>,
}
impl MailConfig {
fn load() -> Option<Self> {
use crate::util::{get_env, get_env_or};
// When SMTP_HOST is absent, we assume the user does not want to enable it.
let smtp_host = match get_env("SMTP_HOST") {
Some(host) => host,
None => return None,
};
let smtp_from = get_env("SMTP_FROM").unwrap_or_else(|| {
error!("Please specify SMTP_FROM to enable SMTP support.");
exit(1);
});
let smtp_from_name = get_env_or("SMTP_FROM_NAME", "Bitwarden_RS".into());
let smtp_ssl = get_env_or("SMTP_SSL", true);
let smtp_port = get_env("SMTP_PORT").unwrap_or_else(|| if smtp_ssl { 587u16 } else { 25u16 });
let smtp_username = get_env("SMTP_USERNAME");
let smtp_password = get_env("SMTP_PASSWORD").or_else(|| {
if smtp_username.as_ref().is_some() {
error!("SMTP_PASSWORD is mandatory when specifying SMTP_USERNAME.");
exit(1);
} else {
None
}
});
Some(MailConfig {
smtp_host,
smtp_port,
smtp_ssl,
smtp_from,
smtp_from_name,
smtp_username,
smtp_password,
})
}
}

View File

@ -6,25 +6,26 @@ use native_tls::{Protocol, TlsConnector};
use crate::api::EmptyResult;
use crate::auth::{encode_jwt, generate_invite_claims};
use crate::config::MailConfig;
use crate::error::Error;
use crate::CONFIG;
fn mailer(config: &MailConfig) -> SmtpTransport {
let client_security = if config.smtp_ssl {
fn mailer() -> SmtpTransport {
let host = CONFIG.smtp_host().unwrap();
let client_security = if CONFIG.smtp_ssl() {
let tls = TlsConnector::builder()
.min_protocol_version(Some(Protocol::Tlsv11))
.build()
.unwrap();
ClientSecurity::Required(ClientTlsParameters::new(config.smtp_host.clone(), tls))
ClientSecurity::Required(ClientTlsParameters::new(host.clone(), tls))
} else {
ClientSecurity::None
};
let smtp_client = SmtpClient::new((config.smtp_host.as_str(), config.smtp_port), client_security).unwrap();
let smtp_client = SmtpClient::new((host.as_str(), CONFIG.smtp_port()), client_security).unwrap();
let smtp_client = match (&config.smtp_username, &config.smtp_password) {
let smtp_client = match (&CONFIG.smtp_username(), &CONFIG.smtp_password()) {
(Some(user), Some(pass)) => smtp_client.credentials(Credentials::new(user.clone(), pass.clone())),
_ => smtp_client,
};
@ -52,7 +53,7 @@ fn get_text(template_name: &'static str, data: serde_json::Value) -> Result<(Str
Ok((subject, body))
}
pub fn send_password_hint(address: &str, hint: Option<String>, config: &MailConfig) -> EmptyResult {
pub fn send_password_hint(address: &str, hint: Option<String>) -> EmptyResult {
let template_name = if hint.is_some() {
"email/pw_hint_some"
} else {
@ -61,7 +62,7 @@ pub fn send_password_hint(address: &str, hint: Option<String>, config: &MailConf
let (subject, body) = get_text(template_name, json!({ "hint": hint }))?;
send_email(&address, &subject, &body, &config)
send_email(&address, &subject, &body)
}
pub fn send_invite(
@ -71,7 +72,6 @@ pub fn send_invite(
org_user_id: Option<String>,
org_name: &str,
invited_by_email: Option<String>,
config: &MailConfig,
) -> EmptyResult {
let claims = generate_invite_claims(
uuid.to_string(),
@ -94,10 +94,10 @@ pub fn send_invite(
}),
)?;
send_email(&address, &subject, &body, &config)
send_email(&address, &subject, &body)
}
pub fn send_invite_accepted(new_user_email: &str, address: &str, org_name: &str, config: &MailConfig) -> EmptyResult {
pub fn send_invite_accepted(new_user_email: &str, address: &str, org_name: &str) -> EmptyResult {
let (subject, body) = get_text(
"email/invite_accepted",
json!({
@ -107,10 +107,10 @@ pub fn send_invite_accepted(new_user_email: &str, address: &str, org_name: &str,
}),
)?;
send_email(&address, &subject, &body, &config)
send_email(&address, &subject, &body)
}
pub fn send_invite_confirmed(address: &str, org_name: &str, config: &MailConfig) -> EmptyResult {
pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult {
let (subject, body) = get_text(
"email/invite_confirmed",
json!({
@ -119,20 +119,20 @@ pub fn send_invite_confirmed(address: &str, org_name: &str, config: &MailConfig)
}),
)?;
send_email(&address, &subject, &body, &config)
send_email(&address, &subject, &body)
}
fn send_email(address: &str, subject: &str, body: &str, config: &MailConfig) -> EmptyResult {
fn send_email(address: &str, subject: &str, body: &str) -> EmptyResult {
let email = EmailBuilder::new()
.to(address)
.from((config.smtp_from.as_str(), config.smtp_from_name.as_str()))
.from((CONFIG.smtp_from().as_str(), CONFIG.smtp_from_name().as_str()))
.subject(subject)
.header(("Content-Type", "text/html"))
.body(body)
.build()
.map_err(|e| Error::new("Error building email", e.to_string()))?;
mailer(config)
mailer()
.send(email.into())
.map_err(|e| Error::new("Error sending email", e.to_string()))
.and(Ok(()))

View File

@ -140,18 +140,6 @@ where
}
}
pub fn try_parse_string_or<S, T, U>(string: impl Try<Ok = S, Error = U>, default: T) -> T
where
S: AsRef<str>,
T: FromStr,
{
if let Ok(Ok(value)) = string.into_result().map(|s| s.as_ref().parse::<T>()) {
value
} else {
default
}
}
//
// Env methods
//
@ -165,13 +153,6 @@ where
try_parse_string(env::var(key))
}
pub fn get_env_or<V>(key: &str, default: V) -> V
where
V: FromStr,
{
try_parse_string_or(env::var(key), default)
}
//
// Date util methods
//
@ -303,3 +284,25 @@ where
}
}
}
//
// Into Result
//
use crate::error::Error;
pub trait IntoResult<T> {
fn into_result(self) -> Result<T, Error>;
}
impl<T> IntoResult<T> for Result<T, Error> {
fn into_result(self) -> Result<T, Error> {
self
}
}
impl<T> IntoResult<T> for T {
fn into_result(self) -> Result<T, Error> {
Ok(self)
}
}