FEA: migrated mail setup to use lettre

Now we don't depend on cli mail client mail(x), but use smtp.
This commit is contained in:
Keanu D?lle 2022-03-08 12:46:19 +01:00
parent bbf971b345
commit 4e5ee0d146
10 changed files with 360 additions and 306 deletions

View File

@ -22,6 +22,7 @@ iban_validate = "4.0.1"
base64 = "0.13.0"
email-address-parser = "1.0.1"
bigdecimal = "0.1.2"
lettre = { version = "0.10.0-rc.4", features = ["tokio1", "tokio1-native-tls"] }
[dependencies.rocket]
version = "0.5.0-rc.1"

View File

@ -0,0 +1,14 @@
use lettre::{AsyncSmtpTransport, Message, Tokio1Executor};
use lettre::message::MessageBuilder;
use lettre::transport::smtp::authentication::Credentials;
use rocket::futures::SinkExt;
use rocket::State;
use crate::Settings;
pub fn setup(settings: &Settings) -> Result<AsyncSmtpTransport<Tokio1Executor>, lettre::transport::smtp::Error> {
let creds = Credentials::new(settings.mail.smtp_username.clone(), settings.mail.smtp_password.clone());
let relay = AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&settings.mail.smtp_host.clone())?;
Ok(relay.credentials(creds).build())
}

View File

@ -1,2 +1,3 @@
pub mod queue;
pub mod worker;
pub mod worker;
pub mod mailer;

View File

@ -1,55 +1,74 @@
use std::sync::RwLock;
use std::collections::VecDeque;
use chrono::NaiveDateTime;
use std::io::{Write, Read};
use std::io::{Read, Write};
use std::ops::Add;
use std::sync::RwLock;
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
use lettre::Message;
use crate::helper::mail_queue::worker::send_mail;
use crate::Settings;
#[derive(Deserialize, Serialize)]
pub struct MailQueue{
pub(crate) mails: RwLock<VecDeque<Mail>>
pub struct MessageWatcher {
message: Message,
tries: i8,
next_try_after: Option<chrono::DateTime<Utc>>,
}
#[derive(Deserialize, Serialize, Clone)]
pub struct Mail{
pub(crate) uuid: uuid::Uuid,
pub(crate) from: String,
pub(crate) to: Vec<String>,
pub(crate) subject: String,
pub(crate) cc: Vec<String>,
pub(crate) bcc: Vec<String>,
pub(crate) reply_to: Option<String>,
pub(crate) body: String,
pub(crate) deliver_until: Option<NaiveDateTime>
}
impl MailQueue{
pub fn add_mail(&self, mail: Mail) -> Result<(), std::sync::PoisonError<std::sync::RwLockWriteGuard<'_, std::collections::VecDeque<Mail>>>>{
self.mails.write()?.push_back(mail);
match self.save_to_disk(){
Ok(_) => {}
Err(e) => error!("Couldn't save mail_queue to disk: {}", e)
impl From<Message> for MessageWatcher {
fn from(msg: Message) -> Self {
MessageWatcher {
message: msg,
tries: 0,
next_try_after: None,
}
}
}
pub struct MailQueue {
pub(crate) mails: RwLock<VecDeque<MessageWatcher>>,
}
impl MailQueue {
pub fn add_mail(&self, mail: Message) -> Result<(), std::sync::PoisonError<std::sync::RwLockWriteGuard<'_, std::collections::VecDeque<MessageWatcher>>>> {
self.mails.write()?.push_back(mail.into());
Ok(())
}
pub fn get_next(&self) -> Result<Option<Mail>, std::sync::PoisonError<std::sync::RwLockWriteGuard<'_, std::collections::VecDeque<Mail>>>>{
// TODO: Check if mail expired (deliver_until check)
pub fn add_msg_watcher(&self, msg_watcher: MessageWatcher) -> Result<(), std::sync::PoisonError<std::sync::RwLockWriteGuard<'_, std::collections::VecDeque<MessageWatcher>>>> {
self.mails.write()?.push_back(msg_watcher);
Ok(())
}
pub fn get_next(&self) -> Result<Option<MessageWatcher>, std::sync::PoisonError<std::sync::RwLockWriteGuard<'_, std::collections::VecDeque<MessageWatcher>>>> {
Ok(self.mails.write()?.pop_front())
}
pub fn process_next(&self) -> Result<(), std::sync::PoisonError<std::sync::RwLockWriteGuard<'_, std::collections::VecDeque<Mail>>>>{
let mail = self.get_next()?;
match mail{
Some(mail) => {
match send_mail(mail.clone()){
Ok(_) => {
match self.save_to_disk(){
Ok(_) => {}
Err(e) => error!("Couldn't save mail_queue to disk: {}", e)
}
},
pub async fn process_next(&self, settings: &Settings) -> Result<(), std::sync::PoisonError<std::sync::RwLockWriteGuard<'_, std::collections::VecDeque<MessageWatcher>>>> {
let mail_watcher = self.get_next()?;
match mail_watcher {
Some(mut mail_watcher) => {
if let Some(next_try_after) = mail_watcher.next_try_after {
let current = Utc::now();
if next_try_after.ge(&current) { //Not due
self.add_msg_watcher(mail_watcher)?;
return Ok(())
}
}
match send_mail(mail_watcher.message.clone(), settings).await {
Ok(_) => {},
Err(_) => {
//TODO: remove emails after x attempts
return self.add_mail(mail)
//Do not re-add mail after third try:
if mail_watcher.tries < 3 {
if mail_watcher.tries == 0 {
mail_watcher.next_try_after = Some(Utc::now().add(Duration::minutes(1))); //First retry after 1 minute
} else if mail_watcher.tries == 1 {
mail_watcher.next_try_after = Some(Utc::now().add(Duration::minutes(10))); //Second retry after 10 minutes
} else if mail_watcher.tries == 2 {
mail_watcher.next_try_after = Some(Utc::now().add(Duration::minutes(600))); //Third retry after 10 hours
}
mail_watcher.tries = mail_watcher.tries + 1;
self.add_msg_watcher(mail_watcher)?;
}
}
}
Ok(())
@ -58,7 +77,7 @@ impl MailQueue{
}
}
pub fn load_or_create_new() -> MailQueue {
match std::fs::File::open("mail_queue.txt"){
/*match std::fs::File::open("mail_queue.txt"){
Ok(mut file) => {
let mut contents = String::new();
match file.read_to_string(&mut contents){
@ -76,33 +95,16 @@ impl MailQueue{
}
}
Err(e) => {
warn!("Couldn't read mail queue from disk: {}. Creating new empty queue.", e);
MailQueue{
mails: RwLock::new(VecDeque::new())
}
warn!("Couldn't read mail queue from disk: {}. Creating new empty queue.", e);*/
MailQueue {
mails: RwLock::new(VecDeque::new())
}/*
}
}
}
}*/
}/*
fn save_to_disk(&self) -> std::io::Result<()> {
let serialized = serde_json::to_string(&self)?;
let mut file = std::fs::File::create("mail_queue.txt")?;
file.write_all(serialized.as_bytes())
}
}
impl Mail{
/// Create Mail with from, to, subject, cc, bcc, reply_to, deliver_until
pub(crate) fn new(from: String, to: Vec<String>, subject: String, cc: Vec<String>, bcc: Vec<String>, reply_to: Option<String>, body: String, deliver_until: Option<NaiveDateTime>) -> Mail {
Mail{
uuid: uuid::Uuid::new_v4(),
from,
to,
subject,
cc,
bcc,
reply_to,
body,
deliver_until
}
}
}
}*/
}

View File

@ -1,84 +1,23 @@
use crate::helper::mail_queue::queue::Mail;
use std::process::Command;
use email_address_parser::EmailAddress;
use lettre::{AsyncTransport, Message};
use std::process::{Command};
use crate::helper::mail_queue::mailer::setup;
use crate::Settings;
//TODO: replace this crap with lettre lib
pub fn render_string(str: String) -> String{
str.replace("\"", "\\\"").replace("\'", "\\\'")
}
pub fn send_mail(mail: Mail) -> Result<(), ()> {
if (mail.to.is_empty() && mail.bcc.is_empty() && mail.cc.is_empty()) || mail.subject.is_empty() || mail.from.is_empty() || mail.body.is_empty() {
error!("Couldn't deliver mail {} because to, subject, from or body missing! Deleting mail!", mail.uuid);
return Err(())
}
let body = render_string(mail.body);
let subject = mail.subject.escape_default().to_string();
let mut arg = String::from("echo \"");
arg.push_str(&body);
arg.push_str("\" | mailx --append='FROM: ");
arg.push_str(&mail.from);
arg.push_str("' ");
if !mail.cc.is_empty(){
arg.push_str("--append='CC:");
for mail in mail.cc{
if EmailAddress::is_valid(&mail, None) {
arg.push_str(&(mail + ","));
}else{
warn!("E-Mail worker - invalid E-Mail address: {}", mail);
}
}
arg.push_str("' ")
}
if !mail.bcc.is_empty(){
arg.push_str("--append='BCC:");
for mail in mail.bcc{
if EmailAddress::is_valid(&mail, None) {
arg.push_str(&(mail + ","));
}else{
warn!("E-Mail worker - invalid E-Mail address: {}", mail);
}
}
arg.push_str("' ")
}
match mail.reply_to{
Some(reply_to) => {
if EmailAddress::is_valid(&reply_to, None) {
arg.push_str(&format!("--append='Reply-To: {} ' ", reply_to))
}else{
warn!("E-Mail worker - invalid E-Mail address: {}", reply_to);
}
},
None => ()
}
arg.push_str("-s \"");
arg.push_str(&subject);
arg.push_str("\" ");
for receiver in mail.to{
if EmailAddress::is_valid(&receiver, None) {
arg.push_str(&(receiver+" "));
}else{
warn!("E-Mail worker - invalid E-Mail address: {}", receiver);
}
}
debug!("sending email with {}", arg);
match Command::new("sh").arg("-c").arg(arg).output(){
Ok(output) => {
if !output.status.success(){
error!("Couldn't send mail: {} {} {}", output.status, String::from_utf8_lossy(&output.stderr), String::from_utf8_lossy(&output.stdout));
Err(())
}else {
Ok(())
}
},
pub async fn send_mail(mail: Message, settings: &Settings) -> Result<(), ()> {
let mailer = match setup(settings) {
Ok(res) => res,
Err(e) => {
error!("Couldn't send mail: {}", e);
error!("Error setting up AsyncSmtpTransportBuilder: {}", e);
return Err(());
}
};
match mailer.send(mail).await {
Ok(_) => Ok(()),
Err(e) => {
error!("Error sending E-mail: {}", e);
Err(())
}
}

View File

@ -5,26 +5,30 @@ use rocket::State;
use crate::helper::session_cookies::model::SessionCookie;
use crate::Settings;
pub fn get_timezone(settings: &State<Settings>, cookie: &SessionCookie) -> Tz {
pub fn get_timezone(settings: &State<Settings>, cookie: Option<&SessionCookie>) -> Tz {
let default_tz: Tz = match settings.application.default_timezone.parse() {
Ok(tz) => tz,
Err(e) => {
panic!("Invalid configuration! Couldn't parse default_timezone: {}", e);
}
};
match cookie.user.timezone.clone() {
Some(tz) => match tz.parse() {
Ok(tz) => tz,
Err(e) => {
warn!("Unparsable timezone for user: {}, falling back to default timezone. Error is {}", cookie.user.id, e);
default_tz
}
},
None => default_tz
if let Some(cookie) = cookie {
match cookie.user.timezone.clone() {
Some(tz) => match tz.parse() {
Ok(tz) => tz,
Err(e) => {
warn!("Unparsable timezone for user: {}, falling back to default timezone. Error is {}", cookie.user.id, e);
default_tz
}
},
None => default_tz
}
} else {
default_tz
}
}
pub fn datetime_str_to_utc(settings: &State<Settings>, cookie: &SessionCookie, user_datetime: &str) -> Result<DateTime<Utc>, ParseError> {
pub fn datetime_str_to_utc(settings: &State<Settings>, cookie: Option<&SessionCookie>, user_datetime: &str) -> Result<DateTime<Utc>, ParseError> {
let tz = get_timezone(settings, cookie);
let naive = NaiveDateTime::parse_from_str(user_datetime, "%Y-%m-%dT%H:%M")?;
@ -32,7 +36,7 @@ pub fn datetime_str_to_utc(settings: &State<Settings>, cookie: &SessionCookie, u
Ok(local_datetime.with_timezone(&Utc))
}
pub fn utc_to_local_user_time(settings: &State<Settings>, cookie: &SessionCookie, utc: DateTime<Utc>) -> String {
pub fn utc_to_local_user_time(settings: &State<Settings>, cookie: Option<&SessionCookie>, utc: DateTime<Utc>) -> String {
let tz = get_timezone(settings, cookie);
let local_time = utc.with_timezone(&tz);
local_time.format("%Y-%m-%dT%H:%M").to_string()
@ -92,7 +96,7 @@ mod tests {
let utcdate: DateTime<Utc> = Utc.ymd(2022, 1, 23).and_hms(12, 0, 0);
let rocket = rocket::build().manage(get_test_settings());
let res = utc_to_local_user_time(State::get(&rocket).unwrap(), &get_test_cookies(), utcdate);
let res = utc_to_local_user_time(State::get(&rocket).unwrap(), Some(&get_test_cookies()), utcdate);
assert_eq!(res, "2022-01-23T13:00");
}
@ -100,38 +104,38 @@ mod tests {
#[test]
pub fn test_datetime_str_to_utc_error() {
let rocket = rocket::build().manage(get_test_settings());
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), &get_test_cookies(), "2022-01-23").is_err());
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), Some(&get_test_cookies()), "2022-01-23").is_err());
}
#[test]
pub fn test_datetime_str_to_utc_error2() {
let rocket = rocket::build().manage(get_test_settings());
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), &get_test_cookies(), "2022-01-23T").is_err());
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), Some(&get_test_cookies()), "2022-01-23T").is_err());
}
#[test]
pub fn test_datetime_str_to_utc_error3() {
let rocket = rocket::build().manage(get_test_settings());
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), &get_test_cookies(), "2022-01-23T15:00:00").is_err());
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), Some(&get_test_cookies()), "2022-01-23T15:00:00").is_err());
}
#[test]
pub fn test_datetime_str_to_utc_error4() {
let rocket = rocket::build().manage(get_test_settings());
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), &get_test_cookies(), "2022-01-23T15").is_err());
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), Some(&get_test_cookies()), "2022-01-23T15").is_err());
}
#[test]
pub fn test_datetime_str_to_utc_error5() {
let rocket = rocket::build().manage(get_test_settings());
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), &get_test_cookies(), "Black Mamba").is_err());
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), Some(&get_test_cookies()), "Black Mamba").is_err());
}
#[test]
pub fn test_datetime_str_to_utc_correct() {
let rocket = rocket::build().manage(get_test_settings());
let utcdate: DateTime<Utc> = Utc.ymd(2022, 1, 23).and_hms(12, 0, 0);
assert_eq!(datetime_str_to_utc(State::get(&rocket).unwrap(), &get_test_cookies(), "2022-01-23T13:00").unwrap(), utcdate);
assert_eq!(datetime_str_to_utc(State::get(&rocket).unwrap(), Some(&get_test_cookies()), "2022-01-23T13:00").unwrap(), utcdate);
}
#[test]

View File

@ -20,6 +20,7 @@ use std::process::Command;
use std::sync::Arc;
use rocket::fs::FileServer;
use rocket::tokio;
use rocket_dyn_templates::handlebars::Handlebars;
use rocket_dyn_templates::Template;
@ -77,16 +78,24 @@ fn rocket() -> _ {
// Initialize mail queue for second thread handling outgoing mails
// We are using Arc to access mail queue in all threads
let mail_queue = Arc::new(MailQueue::load_or_create_new());
let c_lock = Arc::clone(&mail_queue);
let c_lock = mail_queue.clone();
let settings_clone = settings.clone();
thread::spawn(move || {
loop {
match c_lock.process_next() {
Ok(_) => {}
Err(e) => error!("MailQueue poisoned: {}", e),
let mut rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
loop {
let c_lock2 = c_lock.clone();
let settings_clone2 = settings_clone.clone();
if c_lock2.mails.read().unwrap().len() != 0 {
tokio::spawn(async move {
c_lock2.process_next(&settings_clone2).await;
});
}
thread::sleep(time::Duration::from_millis(500)) // Only check for new mails ever 500 ms
}
thread::sleep(time::Duration::from_millis(500)) // Only check for new mails ever 500 ms
}
});
});
let mut mail_templates = MailTemplates {
@ -194,7 +203,6 @@ fn rocket() -> _ {
modules::api::events::delete::delete_event,
modules::api::events::update::close_event,
modules::api::events::update::approve_times,
// modules::api::events::read::read_billing_states_for_event,
modules::api::events::read::read_min_billing_states_for_event,
modules::event_management::event_unit_positions::event_unit_positions,
modules::event_management::event_unit_templates::event_unit_templates,

View File

@ -1,24 +1,22 @@
use rocket::State;
use crate::helper::settings::Settings;
use crate::helper::session_cookies::model::SessionCookie;
use crate::modules::api::model::api_outcome::{ApiErrorWrapper, ApiError};
use std::sync::Arc;
use lettre::Message;
use rocket::serde::json::Json;
use crate::modules::api::member_management::controller::parser::parse_member_cookie;
use crate::helper::mail_queue::queue::{Mail, MailQueue};
use rocket::State;
use std::sync::{Arc};
use crate::database::controller::api_communication_targets::{get_member_email_addresses, get_group_email_addresses, get_unit_email_addresses};
use crate::helper::translate_diesel_error::translate_diesel;
use crate::database::controller::members::check_access_to_resource;
use crate::database::controller::api_communication_targets::{get_group_email_addresses, get_member_email_addresses, get_unit_email_addresses};
use crate::database::controller::groups::get_group;
use crate::database::controller::members::check_access_to_resource;
use crate::database::controller::units::get_unit;
use crate::helper::mail_queue::queue::MailQueue;
use crate::helper::session_cookies::model::SessionCookie;
use crate::helper::settings::Settings;
use crate::helper::translate_diesel_error::translate_diesel;
use crate::modules::api::member_management::controller::parser::parse_member_cookie;
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
#[derive(Queryable, Clone, Deserialize, Serialize)]
pub struct ApiEmail{
pub struct ApiEmail {
pub(crate) to: Option<Vec<String>>,
pub(crate) to_members: Option<Vec<uuid::Uuid>>,
pub(crate) cc: Option<Vec<String>>,
@ -33,117 +31,164 @@ pub struct ApiEmail{
}
#[post("/api/communicator/email", format = "json", data = "<mail>")]
pub fn create_email(mq: &State<Arc<MailQueue>>, settings: &State<Settings>, cookie: SessionCookie, mail: Json<ApiEmail>) -> Result<(), Json<ApiErrorWrapper>>{
pub fn create_email(mq: &State<Arc<MailQueue>>, settings: &State<Settings>, cookie: SessionCookie, mail: Json<ApiEmail>) -> Result<(), Json<ApiErrorWrapper>> {
let caller = parse_member_cookie(cookie.member)?;
if !caller.has_permission(crate::permissions::modules::communicator::email::SEND.to_string()){
if !caller.has_permission(crate::permissions::modules::communicator::email::SEND.to_string()) {
return Err(Json(ApiError::new(403, "Keine Berechtigung Email zu versenden!".to_string()).to_wrapper()));
}
let mail = mail.into_inner();
let maildata = mail.into_inner();
let mut to: Vec<String> = vec![];
let mut cc: Vec<String> = vec![];
let mut bcc: Vec<String> = vec![];
let mut mail = Message::builder()
.from(settings.mail.from.clone().parse().unwrap())
.reply_to(settings.mail.reply_to.clone().parse().unwrap());
match mail.to{
Some(mut mail) => {
to.append(mail.as_mut())
}
None => {}
}
match mail.to_members{
Some(members) => {
for member_id in members{
match get_member_email_addresses(settings, member_id){
Ok(mut addresses) => {
to.append(addresses.as_mut());
}
Err(e) => {
return Err(translate_diesel(e))
}
if let Some(receivers) = maildata.to {
for receiver in receivers {
match receiver.parse() {
Ok(receiver) => {
mail = mail.to(receiver);
},
Err(e) => {
warn!("Couldn't parse to address: {}", e);
return Err(Json(ApiError::new(400, "Das An Feld enthält ungültige E-Mail Adressen!".to_string()).to_wrapper()));
}
}
}
None => {}
}
match mail.cc{
Some(mut mail) => {
cc.append(mail.as_mut())
}
None => {}
}
match mail.cc_members{
Some(members) => {
for member_id in members{
match get_member_email_addresses(settings, member_id){
Ok(mut addresses) => {
cc.append(addresses.as_mut());
}
Err(e) => {
return Err(translate_diesel(e))
}
}
}
}
None => {}
}
match mail.bcc{
Some(mut mail) => {
bcc.append(mail.as_mut())
}
None => {}
}
match mail.bcc_members{
Some(members) => {
for member_id in members{
match get_member_email_addresses(settings, member_id){
Ok(mut addresses) => {
bcc.append(addresses.as_mut());
}
Err(e) => {
return Err(translate_diesel(e))
}
}
}
}
None => {}
}
match mail.selected_groups{
None => {}
Some(groups) => {
for group in groups{
if !check_access_to_resource(settings, caller.entity_id, group, crate::permissions::modules::communicator::email::SEND){
match get_group(settings, group){
Ok(group) => {
return Err(Json(ApiError::new(403, format!("Keine Berechtigung eine Email an die Gruppe {} zu schicken!", group.name)).to_wrapper()))
}
Err(e) => {
return Err(translate_diesel(e))
if let Some(to_members) = maildata.to_members {
for member_id in to_members {
match get_member_email_addresses(settings, member_id) {
Ok(mut addresses) => {
for address in addresses {
match address.parse() {
Ok(address) => {
mail = mail.to(address);
},
Err(e) => {
error!("Couldn't parse members email address {}: {}", address, e);
//Do not return error here, otherwise whole delivery would fail if one member has an falsy email address
}
}
}
}
match get_group_email_addresses(settings, group){
Ok(mut emails) => {
bcc.append(emails.as_mut());
Err(e) => {
return Err(translate_diesel(e))
}
}
}
}
if let Some(ccers) = maildata.cc {
for cc in ccers {
match cc.parse() {
Ok(cc) => {
mail = mail.cc(cc);
},
Err(e) => {
warn!("Couldn't parse cc address: {}", e);
return Err(Json(ApiError::new(400, "Das CC Feld enthält ungültige E-Mail Adressen!".to_string()).to_wrapper()));
}
}
}
}
if let Some(cc_members) = maildata.cc_members {
for member_id in cc_members {
match get_member_email_addresses(settings, member_id) {
Ok(mut addresses) => {
for address in addresses {
match address.parse() {
Ok(address) => {
mail = mail.cc(address);
},
Err(e) => {
error!("Couldn't parse members email address {}: {}", address, e);
//Do not return error here, otherwise whole delivery would fail if one member has an falsy email address
}
}
}
}
Err(e) => {
return Err(translate_diesel(e))
}
}
}
}
if let Some(bccers) = maildata.bcc {
for bcc in bccers {
match bcc.parse() {
Ok(bcc) => {
mail = mail.bcc(bcc);
},
Err(e) => {
warn!("Couldn't parse bcc address: {}", e);
return Err(Json(ApiError::new(400, "Das BCC Feld enthält ungültige E-Mail Adressen!".to_string()).to_wrapper()));
}
}
}
}
if let Some(bcc_members) = maildata.bcc_members {
for member_id in bcc_members {
match get_member_email_addresses(settings, member_id) {
Ok(mut addresses) => {
for address in addresses {
match address.parse() {
Ok(address) => {
mail = mail.bcc(address);
},
Err(e) => {
error!("Couldn't parse members email address {}: {}", address, e);
//Do not return error here, otherwise whole delivery would fail if one member has an falsy email address
}
}
}
}
Err(e) => {
return Err(translate_diesel(e))
}
}
}
}
if let Some(groups) = maildata.selected_groups {
for group in groups {
if !check_access_to_resource(settings, caller.entity_id, group, crate::permissions::modules::communicator::email::SEND) {
match get_group(settings, group) {
Ok(group) => {
return Err(Json(ApiError::new(403, format!("Keine Berechtigung eine Email an die Gruppe {} zu schicken!", group.name)).to_wrapper()))
}
Err(e) => {
return Err(translate_diesel(e))
}
}
}
match get_group_email_addresses(settings, group) {
Ok(mut addresses) => {
for address in addresses {
match address.parse() {
Ok(address) => {
mail = mail.bcc(address);
},
Err(e) => {
error!("Couldn't parse members email address {}: {}", address, e);
//Do not return error here, otherwise whole delivery would fail if one member has an falsy email address
}
}
}
}
Err(e) => {
return Err(translate_diesel(e))
}
}
}
}
match mail.selected_units{
match maildata.selected_units {
None => {}
Some(units) => {
for unit in units{
if !check_access_to_resource(settings, caller.entity_id, unit, crate::permissions::modules::communicator::email::SEND){
match get_unit(settings, unit){
for unit in units {
if !check_access_to_resource(settings, caller.entity_id, unit, crate::permissions::modules::communicator::email::SEND) {
match get_unit(settings, unit) {
Ok(unit) => {
return Err(Json(ApiError::new(403, format!("Keine Berechtigung eine Email an die Einheit {} zu schicken!", unit.name)).to_wrapper()))
}
@ -152,9 +197,19 @@ pub fn create_email(mq: &State<Arc<MailQueue>>, settings: &State<Settings>, cook
}
}
}
match get_unit_email_addresses(settings, unit){
Ok(mut emails) => {
bcc.append(emails.as_mut());
match get_unit_email_addresses(settings, unit) {
Ok(mut addresses) => {
for address in addresses {
match address.parse() {
Ok(address) => {
mail = mail.bcc(address);
},
Err(e) => {
error!("Couldn't parse members email address {}: {}", address, e);
//Do not return error here, otherwise whole delivery would fail if one member has an falsy email address
}
}
}
}
Err(e) => {
return Err(translate_diesel(e))
@ -164,15 +219,10 @@ pub fn create_email(mq: &State<Arc<MailQueue>>, settings: &State<Settings>, cook
}
}
if to.is_empty() && cc.is_empty() && bcc.is_empty(){
return Err(Json(ApiError::new(422, "Es muss mindestens ein Empfänger angegeben werden!".to_string()).to_wrapper()))
}
let composed_mail = Mail::new(settings.mail.from.clone(), to,mail.subject,cc,bcc,mail.reply_to,mail.body,None);
let mail = mail.body(maildata.body).unwrap();
match mq.add_mail(composed_mail){
match mq.add_mail(mail) {
Ok(_) => Ok(()),
Err(_) => Err(Json(ApiError::new(500, "Couldn't add mail to mail queue!".to_string()).to_wrapper()))
}
}

View File

@ -1,6 +1,8 @@
use std::sync::Arc;
use chrono::NaiveDateTime;
use lettre::Message;
use lettre::message::Mailbox;
use rocket::serde::json::Json;
use rocket::State;
@ -11,7 +13,7 @@ use crate::database::controller::organisers::get_organiser;
use crate::database::controller::permissions::get_members_with_permission;
use crate::database::model::event_requests::EventRequest;
use crate::database::model::organisers::Organiser;
use crate::helper::mail_queue::queue::{Mail, MailQueue};
use crate::helper::mail_queue::queue::MailQueue;
use crate::helper::mail_templates::MailTemplates;
use crate::helper::session_cookies::model::SessionCookie;
use crate::helper::settings::Settings;
@ -166,15 +168,31 @@ fn send_event_request_published_emails(mt: &State<MailTemplates>, mq: &State<Arc
None => None
};
for receiver in receivers{
let emails = match get_member_email_addresses(settings, receiver){
Ok(emails) => emails,
for receiver in receivers {
let mut mail = Message::builder()
.from(settings.mail.from.clone().parse().unwrap())
.reply_to(settings.mail.reply_to.clone().parse().unwrap())
.subject("Einsatz Online - Passwort Zurücksetzen");
match get_member_email_addresses(settings, receiver) {
Ok(emails) => {
for email in emails {
match email.parse() {
Ok(email) => {
mail = mail.to(email);
},
Err(e) => {
error!("Couldn't parse email address {}: {}", email, e);
}
}
}
},
Err(e) => {
error!("Couldn't get email addresses for member: {}", e);
continue
}
};
let member = match get_member_by_uuid(receiver, settings){
let member = match get_member_by_uuid(receiver, settings) {
Some(member) => member,
None => {
error!("No member found for id {}", receiver);
@ -182,21 +200,21 @@ fn send_event_request_published_emails(mt: &State<MailTemplates>, mq: &State<Arc
}
};
let nerpe = NewEventRequestPublishedEmail{
let nerpe = NewEventRequestPublishedEmail {
firstname: member.firstname,
request: request.clone(),
frontpage: settings.application.url.clone(),
support_email: settings.application.user_support_email.clone(),
organiser: organiser.clone()
organiser: organiser.clone(),
};
let body = match mt.registry.render("new_event_request_published-de", &nerpe){
let mail = mail.body(match mt.registry.render("new_event_request_published-de", &nerpe) {
Ok(body) => body,
Err(e) => {
error!("Couldn't render email template: {}", e);
return},
};
return
},
}).unwrap();
let mail = Mail::new(settings.mail.from.clone(), emails, format!("[{}] - Neue Einsatzanfrage", settings.application.name.clone()), vec![], vec![], Some(settings.mail.reply_to.clone()), body, None); //TODO: Add deliver_until
match mq.add_mail(mail){
Ok(_) => {},
Err(_) => {}

View File

@ -1,13 +1,16 @@
use crate::helper::settings::Settings;
use rocket::State;
use crate::database::controller::users::get_user_by_email;
use crate::database::controller::password_resets::add_token;
use crate::helper::mail_templates::MailTemplates;
use crate::helper::mail_queue::queue::{Mail, MailQueue};
use std::sync::Arc;
use lettre::Message;
use rocket::State;
use crate::database::controller::password_resets::add_token;
use crate::database::controller::users::get_user_by_email;
use crate::helper::mail_queue::queue::MailQueue;
use crate::helper::mail_templates::MailTemplates;
use crate::helper::settings::Settings;
#[derive(Serialize)]
pub struct PasswortResetMail{
pub struct PasswortResetMail {
frontpage: String,
email: String,
reset_url: String,
@ -24,18 +27,32 @@ pub fn request_password_reset(settings: &State<Settings>, mt: &State<MailTemplat
Ok(token) => token,
Err(_) => return Err(()),
};
let pwrm = PasswortResetMail{
let pwrm = PasswortResetMail {
frontpage: settings.application.url.clone(),
email: email.clone(),
reset_url: format!("{}password_reset?token={}", settings.application.url.clone(), token)
reset_url: format!("{}password_reset?token={}", settings.application.url.clone(), token),
};
let body = match mt.registry.render("password-reset-de", &pwrm){
let body = match mt.registry.render("password-reset-de", &pwrm) {
Ok(body) => body,
Err(_) => return Err(()),
};
let mail = Mail::new(settings.mail.from.clone(), vec![email], format!("[{}] - Passwort Zuruecksetzen", settings.application.name.clone()), vec![], vec![], Some(settings.mail.reply_to.clone()), body, None); //TODO: Add deliver_until
match mq.add_mail(mail){
let email_address = match email.parse() {
Ok(email) => email,
Err(e) => {
warn!("Couldn't parse password reset email: {}", e);
return Err(())
}
};
let mail = Message::builder()
.from(settings.mail.from.clone().parse().unwrap())
.reply_to(settings.mail.reply_to.clone().parse().unwrap())
.to(email_address)
.subject("Einsatz Online - Passwort Zurücksetzen") //TODO: use application name setting
.body(body).unwrap();
match mq.add_mail(mail) {
Ok(_) => Ok(()),
Err(_) => Err(())
}