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

attach images in email #2784

Merged
merged 2 commits into from
Oct 19, 2022
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: 3 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,9 @@
## but might need to be changed in case it trips some anti-spam filters
# HELO_NAME=

## Embed images as email attachments
# SMTP_EMBED_IMAGES=false

## SMTP debugging
## When set to true this will output very detailed SMTP messages.
## WARNING: This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting!
Expand Down
1 change: 1 addition & 0 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub use crate::api::{
notifications::{start_notification_server, Notify, UpdateType},
web::catchers as web_catchers,
web::routes as web_routes,
web::static_files,
};
use crate::util;

Expand Down
2 changes: 1 addition & 1 deletion src/api/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ fn alive(_conn: DbConn) -> Json<String> {
}

#[get("/vw_static/<filename>")]
fn static_files(filename: String) -> Result<(ContentType, &'static [u8]), Error> {
pub fn static_files(filename: String) -> Result<(ContentType, &'static [u8]), Error> {
match filename.as_ref() {
"mail-github.png" => Ok((ContentType::PNG, include_bytes!("../static/images/mail-github.png"))),
"logo-gray.png" => Ok((ContentType::PNG, include_bytes!("../static/images/logo-gray.png"))),
Expand Down
12 changes: 12 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,10 @@ make_config! {
smtp_timeout: u64, true, def, 15;
/// Server name sent during HELO |> By default this value should be is on the machine's hostname, but might need to be changed in case it trips some anti-spam filters
helo_name: String, true, option;
/// Embed images as email attachments.
smtp_embed_images: bool, true, def, true;
/// Internal
_smtp_img_src: String, false, gen, |c| generate_smtp_img_src(c.smtp_embed_images, &c.domain);
/// Enable SMTP debugging (Know the risks!) |> DANGEROUS: Enabling this will output very detailed SMTP messages. This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting!
smtp_debug: bool, false, def, false;
/// Accept Invalid Certs (Know the risks!) |> DANGEROUS: Allow invalid certificates. This option introduces significant vulnerabilities to man-in-the-middle attacks!
Expand Down Expand Up @@ -759,6 +763,14 @@ fn extract_url_path(url: &str) -> String {
}
}

fn generate_smtp_img_src(embed_images: bool, domain: &str) -> String {
if embed_images {
"cid:".to_string()
} else {
format!("{domain}/vw_static/")
}
}

/// Generate the correct URL for the icon service.
/// This will be used within icons.rs to call the external icon service.
fn generate_icon_service_url(icon_service: &str) -> String {
Expand Down
55 changes: 52 additions & 3 deletions src/mail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use chrono::NaiveDateTime;
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};

use lettre::{
message::{Mailbox, Message, MultiPart},
message::{Attachment, Body, Mailbox, Message, MultiPart, SinglePart},
transport::smtp::authentication::{Credentials, Mechanism as SmtpAuthMechanism},
transport::smtp::client::{Tls, TlsParameters},
transport::smtp::extension::ClientId,
Expand Down Expand Up @@ -117,7 +117,14 @@ pub async fn send_password_hint(address: &str, hint: Option<String>) -> EmptyRes
"email/pw_hint_none"
};

let (subject, body_html, body_text) = get_text(template_name, json!({ "hint": hint, "url": CONFIG.domain() }))?;
let (subject, body_html, body_text) = get_text(
template_name,
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"hint": hint,
}),
)?;

send_email(address, &subject, body_html, body_text).await
}
Expand All @@ -130,6 +137,7 @@ pub async fn send_delete_account(address: &str, uuid: &str) -> EmptyResult {
"email/delete_account",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"user_id": uuid,
"email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
"token": delete_token,
Expand All @@ -147,6 +155,7 @@ pub async fn send_verify_email(address: &str, uuid: &str) -> EmptyResult {
"email/verify_email",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"user_id": uuid,
"email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
"token": verify_email_token,
Expand All @@ -161,6 +170,7 @@ pub async fn send_welcome(address: &str) -> EmptyResult {
"email/welcome",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
}),
)?;

Expand All @@ -175,6 +185,7 @@ pub async fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult
"email/welcome_must_verify",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"user_id": uuid,
"token": verify_email_token,
}),
Expand All @@ -188,6 +199,7 @@ pub async fn send_2fa_removed_from_org(address: &str, org_name: &str) -> EmptyRe
"email/send_2fa_removed_from_org",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"org_name": org_name,
}),
)?;
Expand All @@ -200,6 +212,7 @@ pub async fn send_single_org_removed_from_org(address: &str, org_name: &str) ->
"email/send_single_org_removed_from_org",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"org_name": org_name,
}),
)?;
Expand Down Expand Up @@ -228,6 +241,7 @@ pub async fn send_invite(
"email/send_org_invite",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"org_id": org_id.as_deref().unwrap_or("_"),
"org_user_id": org_user_id.as_deref().unwrap_or("_"),
"email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
Expand Down Expand Up @@ -260,6 +274,7 @@ pub async fn send_emergency_access_invite(
"email/send_emergency_access_invite",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"emer_id": emer_id.unwrap_or_else(|| "_".to_string()),
"email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
"grantor_name": grantor_name,
Expand All @@ -275,6 +290,7 @@ pub async fn send_emergency_access_invite_accepted(address: &str, grantee_email:
"email/emergency_access_invite_accepted",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"grantee_email": grantee_email,
}),
)?;
Expand All @@ -287,6 +303,7 @@ pub async fn send_emergency_access_invite_confirmed(address: &str, grantor_name:
"email/emergency_access_invite_confirmed",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"grantor_name": grantor_name,
}),
)?;
Expand All @@ -299,6 +316,7 @@ pub async fn send_emergency_access_recovery_approved(address: &str, grantor_name
"email/emergency_access_recovery_approved",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"grantor_name": grantor_name,
}),
)?;
Expand All @@ -316,6 +334,7 @@ pub async fn send_emergency_access_recovery_initiated(
"email/emergency_access_recovery_initiated",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"grantee_name": grantee_name,
"atype": atype,
"wait_time_days": wait_time_days,
Expand All @@ -335,6 +354,7 @@ pub async fn send_emergency_access_recovery_reminder(
"email/emergency_access_recovery_reminder",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"grantee_name": grantee_name,
"atype": atype,
"days_left": days_left,
Expand All @@ -349,6 +369,7 @@ pub async fn send_emergency_access_recovery_rejected(address: &str, grantor_name
"email/emergency_access_recovery_rejected",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"grantor_name": grantor_name,
}),
)?;
Expand All @@ -361,6 +382,7 @@ pub async fn send_emergency_access_recovery_timed_out(address: &str, grantee_nam
"email/emergency_access_recovery_timed_out",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"grantee_name": grantee_name,
"atype": atype,
}),
Expand All @@ -374,6 +396,7 @@ pub async fn send_invite_accepted(new_user_email: &str, address: &str, org_name:
"email/invite_accepted",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"email": new_user_email,
"org_name": org_name,
}),
Expand All @@ -387,6 +410,7 @@ pub async fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult
"email/invite_confirmed",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"org_name": org_name,
}),
)?;
Expand All @@ -403,6 +427,7 @@ pub async fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTi
"email/new_device_logged_in",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"ip": ip,
"device": device,
"datetime": crate::util::format_naive_datetime_local(dt, fmt),
Expand All @@ -421,6 +446,7 @@ pub async fn send_incomplete_2fa_login(address: &str, ip: &str, dt: &NaiveDateTi
"email/incomplete_2fa_login",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"ip": ip,
"device": device,
"datetime": crate::util::format_naive_datetime_local(dt, fmt),
Expand All @@ -436,6 +462,7 @@ pub async fn send_token(address: &str, token: &str) -> EmptyResult {
"email/twofactor_email",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"token": token,
}),
)?;
Expand All @@ -448,6 +475,7 @@ pub async fn send_change_email(address: &str, token: &str) -> EmptyResult {
"email/change_email",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"token": token,
}),
)?;
Expand All @@ -460,6 +488,7 @@ pub async fn send_test(address: &str) -> EmptyResult {
"email/smtp_test",
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
}),
)?;

Expand All @@ -468,12 +497,32 @@ pub async fn send_test(address: &str) -> EmptyResult {

async fn send_email(address: &str, subject: &str, body_html: String, body_text: String) -> EmptyResult {
let smtp_from = &CONFIG.smtp_from();

let body = if CONFIG.smtp_embed_images() {
let logo_gray_body = Body::new(crate::api::static_files("logo-gray.png".to_string()).unwrap().1.to_vec());
let mail_github_body = Body::new(crate::api::static_files("mail-github.png".to_string()).unwrap().1.to_vec());
MultiPart::alternative().singlepart(SinglePart::plain(body_text)).multipart(
MultiPart::related()
.singlepart(SinglePart::html(body_html))
.singlepart(
Attachment::new_inline(String::from("logo-gray.png"))
.body(logo_gray_body, "image/png".parse().unwrap()),
)
.singlepart(
Attachment::new_inline(String::from("mail-github.png"))
.body(mail_github_body, "image/png".parse().unwrap()),
),
)
} else {
MultiPart::alternative_plain_html(body_text, body_html)
};

let email = Message::builder()
.message_id(Some(format!("<{}@{}>", crate::util::get_uuid(), smtp_from.split('@').collect::<Vec<&str>>()[1])))
.to(Mailbox::new(None, Address::from_str(address)?))
.from(Mailbox::new(Some(CONFIG.smtp_from_name()), Address::from_str(smtp_from)?))
.subject(subject)
.multipart(MultiPart::alternative_plain_html(body_text, body_html))?;
.multipart(body)?;

match mailer().send(email).await {
Ok(_) => Ok(()),
Expand Down
2 changes: 1 addition & 1 deletion src/static/templates/email/email_footer.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
<table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/vaultwarden" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/vw_static/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/vaultwarden" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{img_src}}mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
</tr>
</table>
</td>
Expand Down
2 changes: 1 addition & 1 deletion src/static/templates/email/email_header.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
<table class="body-wrap" cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;" bgcolor="#f6f6f6">
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
<td valign="middle" class="aligncenter middle logo" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;" align="center">
<img src="{{url}}/vw_static/logo-gray.png" alt="" width="190" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" />
<img src="{{img_src}}logo-gray.png" alt="Vaultwarden" width="190" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" />
</td>
</tr>
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
Expand Down