Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(types): add email types for sending emails #3020

Merged
merged 3 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/router/src/core/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,10 @@ pub async fn connect_account(

use crate::services::email::types as email_types;

let email_contents = email_types::WelcomeEmail {
let email_contents = email_types::VerifyEmail {
recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?,
settings: state.conf.clone(),
subject: "Welcome to the Hyperswitch community!",
};

let send_email_result = state
Expand Down
32 changes: 14 additions & 18 deletions crates/router/src/services/email/assets/magic_link.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,16 @@
<title>Login to Hyperswitch</title>
<body style="background-color: #ececec">
<style>
.apple-footer a {
{
text-decoration: none !important;
color: #999 !important;
border: none !important;
}
}
.apple-email a {
{
text-decoration: none !important;
color: #448bff !important;
border: none !important;
}
}
.apple-footer a {{
text-decoration: none !important;
color: #999 !important;
border: none !important;
}}
.apple-email a {{
text-decoration: none !important;
color: #448bff !important;
border: none !important;
}}
</style>
<div
id="wrapper"
Expand Down Expand Up @@ -106,8 +102,8 @@
>
Welcome to Hyperswitch!
<p style="font-size: 18px">Dear {user_name},</p>
<span style="font-size: 18px"
>We are thrilled to welcome you into our community!
<span style="font-size: 18px">
We are thrilled to welcome you into our community!
</span>
</td>
</tr>
Expand Down Expand Up @@ -140,8 +136,8 @@
align="center"
>
<br />
Simply click on the link below, and you'll be granted instant access
to your Hyperswitch account. Note that this link expires in 24 hours
Simply click on the link below, and you'll be granted instant access
to your Hyperswitch account. Note that this link expires in 24 hours
and can only be used once.<br />
</td>
</tr>
Expand Down
141 changes: 128 additions & 13 deletions crates/router/src/services/email/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ use masking::ExposeInterface;

use crate::{configs, consts};
#[cfg(feature = "olap")]
use crate::{core::errors::UserErrors, services::jwt, types::domain::UserEmail};
use crate::{core::errors::UserErrors, services::jwt, types::domain};

pub enum EmailBody {
Verify { link: String },
Reset { link: String, user_name: String },
MagicLink { link: String, user_name: String },
InviteUser { link: String, user_name: String },
}

pub mod html {
Expand All @@ -19,6 +22,27 @@ pub mod html {
EmailBody::Verify { link } => {
format!(include_str!("assets/verify.html"), link = link)
}
EmailBody::Reset { link, user_name } => {
format!(
include_str!("assets/reset.html"),
link = link,
username = user_name
)
}
EmailBody::MagicLink { link, user_name } => {
format!(
include_str!("assets/magic_link.html"),
user_name = user_name,
link = link
)
}
EmailBody::InviteUser { link, user_name } => {
format!(
include_str!("assets/invite.html"),
username = user_name,
link = link
)
}
}
}
}
Expand All @@ -31,7 +55,7 @@ pub struct EmailToken {

impl EmailToken {
pub async fn new_token(
email: UserEmail,
email: domain::UserEmail,
settings: &configs::settings::Settings,
) -> CustomResult<String, UserErrors> {
let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS);
Expand All @@ -44,35 +68,126 @@ impl EmailToken {
}
}

pub struct WelcomeEmail {
pub recipient_email: UserEmail,
pub settings: std::sync::Arc<configs::settings::Settings>,
}

pub fn get_email_verification_link(
pub fn get_link_with_token(
base_url: impl std::fmt::Display,
token: impl std::fmt::Display,
action: impl std::fmt::Display,
) -> String {
format!("{base_url}/user/verify_email/?token={token}")
format!("{base_url}/user/{action}/?token={token}")
}

pub struct VerifyEmail {
pub recipient_email: domain::UserEmail,
pub settings: std::sync::Arc<configs::settings::Settings>,
pub subject: &'static str,
}

/// Currently only HTML is supported
#[async_trait::async_trait]
impl EmailData for WelcomeEmail {
impl EmailData for VerifyEmail {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings)
.await
.change_context(EmailError::TokenGenerationFailure)?;

let verify_email_link = get_email_verification_link(&self.settings.server.base_url, token);
let verify_email_link =
get_link_with_token(&self.settings.server.base_url, token, "verify_email");

let body = html::get_html_body(EmailBody::Verify {
link: verify_email_link,
});
let subject = "Welcome to the Hyperswitch community!".to_string();

Ok(EmailContents {
subject,
subject: self.subject.to_string(),
body: external_services::email::IntermediateString::new(body),
recipient: self.recipient_email.clone().into_inner(),
})
}
}

pub struct ResetPassword {
pub recipient_email: domain::UserEmail,
pub user_name: domain::UserName,
pub settings: std::sync::Arc<configs::settings::Settings>,
pub subject: &'static str,
}

#[async_trait::async_trait]
impl EmailData for ResetPassword {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings)
.await
.change_context(EmailError::TokenGenerationFailure)?;

let reset_password_link =
get_link_with_token(&self.settings.server.base_url, token, "set_password");

let body = html::get_html_body(EmailBody::Reset {
link: reset_password_link,
user_name: self.user_name.clone().get_secret().expose(),
});

Ok(EmailContents {
subject: self.subject.to_string(),
body: external_services::email::IntermediateString::new(body),
recipient: self.recipient_email.clone().into_inner(),
})
}
}

pub struct MagicLink {
pub recipient_email: domain::UserEmail,
pub user_name: domain::UserName,
pub settings: std::sync::Arc<configs::settings::Settings>,
pub subject: &'static str,
}

#[async_trait::async_trait]
impl EmailData for MagicLink {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings)
.await
.change_context(EmailError::TokenGenerationFailure)?;

let magic_link_login = get_link_with_token(&self.settings.server.base_url, token, "login");

let body = html::get_html_body(EmailBody::MagicLink {
link: magic_link_login,
user_name: self.user_name.clone().get_secret().expose(),
});

Ok(EmailContents {
subject: self.subject.to_string(),
body: external_services::email::IntermediateString::new(body),
recipient: self.recipient_email.clone().into_inner(),
})
}
}

pub struct InviteUser {
pub recipient_email: domain::UserEmail,
pub user_name: domain::UserName,
pub settings: std::sync::Arc<configs::settings::Settings>,
pub subject: &'static str,
}

#[async_trait::async_trait]
impl EmailData for InviteUser {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings)
.await
.change_context(EmailError::TokenGenerationFailure)?;

let invite_user_link =
get_link_with_token(&self.settings.server.base_url, token, "set_password");

let body = html::get_html_body(EmailBody::MagicLink {
link: invite_user_link,
user_name: self.user_name.clone().get_secret().expose(),
});

Ok(EmailContents {
subject: self.subject.to_string(),
body: external_services::email::IntermediateString::new(body),
recipient: self.recipient_email.clone().into_inner(),
})
Expand Down
Loading