diff --git a/Cargo.lock b/Cargo.lock index c089e69..59c212a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -333,6 +333,16 @@ dependencies = [ "phf_codegen", ] +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown", + "stacker", +] + [[package]] name = "cipher" version = "0.4.4" @@ -366,6 +376,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -554,6 +574,22 @@ dependencies = [ "serde", ] +[[package]] +name = "email-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75" +dependencies = [ + "base64", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -664,6 +700,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -948,6 +999,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + [[package]] name = "http" version = "0.2.11" @@ -1186,6 +1248,31 @@ dependencies = [ "spin 0.5.2", ] +[[package]] +name = "lettre" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a48c2e9831b370bc2d7233c2620298c45f3a158ed6b4b8d7416b2ada5a268fd8" +dependencies = [ + "base64", + "chumsky", + "email-encoding", + "email_address", + "fastrand", + "futures-util", + "hostname", + "httpdate", + "idna", + "mime", + "native-tls", + "nom", + "once_cell", + "quoted_printable", + "socket2", + "tokio", + "url", +] + [[package]] name = "libc" version = "0.2.151" @@ -1246,6 +1333,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + [[package]] name = "matchers" version = "0.1.0" @@ -1333,6 +1426,24 @@ dependencies = [ "version_check", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nix" version = "0.27.1" @@ -1471,6 +1582,50 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl" +version = "0.10.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.42", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "overload" version = "0.1.1" @@ -1732,6 +1887,15 @@ dependencies = [ "yansi", ] +[[package]] +name = "psm" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +dependencies = [ + "cc", +] + [[package]] name = "pure-rust-locales" version = "0.7.0" @@ -1747,6 +1911,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0" + [[package]] name = "rand" version = "0.8.5" @@ -1968,6 +2138,7 @@ dependencies = [ "env_logger", "futures", "ics", + "lettre", "log", "rocket", "rocket_dyn_templates", @@ -2067,6 +2238,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -2089,6 +2269,29 @@ dependencies = [ "untrusted", ] +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.193" @@ -2473,6 +2676,19 @@ dependencies = [ "memchr", ] +[[package]] +name = "stacker" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "winapi", +] + [[package]] name = "state" version = "0.6.0" diff --git a/Cargo.toml b/Cargo.toml index e22d584..c911cf6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,5 @@ chrono-tz = "0.8" tera = { version = "1.18", features = ["date-locale"], optional = true} ics = "0.5" futures = "0.3" +lettre = "0.11" + diff --git a/Rocket.toml b/Rocket.toml index e572218..6f5e652 100644 --- a/Rocket.toml +++ b/Rocket.toml @@ -2,3 +2,4 @@ secret_key = "/NtVGizglEoyoxBLzsRDWTy4oAG1qDw4J4O+CWJSv+fypD7W9sam8hUY4j90EZsbZk8wEradS5zBoWtWKi3k8w==" rss_key = "rss-key-for-ci" limits = { file = "10 MiB", data-form = "10 MiB"} +smtp_pw = "8kIjlLH79Ky6D3jQ" diff --git a/src/model/mail.rs b/src/model/mail.rs new file mode 100644 index 0000000..ef24f05 --- /dev/null +++ b/src/model/mail.rs @@ -0,0 +1,65 @@ +use std::error::Error; + +use lettre::{ + message::{ + header::{self, ContentType}, + MultiPart, SinglePart, + }, + transport::smtp::authentication::Credentials, + Message, SmtpTransport, Transport, +}; +use sqlx::SqlitePool; + +use crate::tera::admin::mail::MailToSend; + +use super::role::Role; + +pub struct Mail {} + +impl Mail { + pub async fn send(db: &SqlitePool, data: MailToSend<'_>, smtp_pw: String) -> bool { + let mut email = Message::builder() + .from( + "ASKÖ Ruderverein Donau Linz " + .parse() + .unwrap(), + ) + .reply_to( + "ASKÖ Ruderverein Donau Linz " + .parse() + .unwrap(), + ) + .to("ASKÖ Ruderverein Donau Linz " + .parse() + .unwrap()); + let role = Role::find_by_id(db, data.role_id).await.unwrap(); + for rec in role.mails_from_role(db).await { + let splitted = rec.split(','); + for single_rec in splitted { + email = email.bcc(single_rec.parse().unwrap()); + } + } + + // TODO: handle attachments + + let email = email + .subject(data.subject) + .header(ContentType::TEXT_PLAIN) + .body(String::from(data.body)) + .unwrap(); + + let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw); + + let mailer = SmtpTransport::relay("mail.your-server.de") + .unwrap() + .credentials(creds) + .build(); + + // Send the email + match mailer.send(&email) { + Ok(_) => return true, + Err(e) => println!("{:?}", e.source()), + }; + false + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs index 8062225..ed5ddbf 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -13,6 +13,7 @@ pub mod location; pub mod log; pub mod logbook; pub mod logtype; +pub mod mail; pub mod planned_event; pub mod role; pub mod rower; diff --git a/src/model/role.rs b/src/model/role.rs index 8ad982f..d37ca49 100644 --- a/src/model/role.rs +++ b/src/model/role.rs @@ -14,4 +14,32 @@ impl Role { .await .unwrap() } + + pub async fn find_by_id(db: &SqlitePool, name: i32) -> Option { + sqlx::query_as!( + Self, + " +SELECT id, name +FROM role +WHERE id like ? + ", + name + ) + .fetch_one(db) + .await + .ok() + } + + pub async fn mails_from_role(&self, db: &SqlitePool) -> Vec { + let query = format!( + "SELECT u.mail + FROM user u + JOIN user_role ur ON u.id = ur.user_id + JOIN role r ON ur.role_id = r.id + WHERE r.id = {}", + self.id + ); + + sqlx::query_scalar(&query).fetch_all(db).await.unwrap() + } } diff --git a/src/tera/admin/mail.rs b/src/tera/admin/mail.rs new file mode 100644 index 0000000..e45c260 --- /dev/null +++ b/src/tera/admin/mail.rs @@ -0,0 +1,64 @@ +use rocket::form::Form; +use rocket::fs::TempFile; +use rocket::response::{Flash, Redirect}; +use rocket::{get, request::FlashMessage, routes, Route, State}; +use rocket::{post, FromForm}; +use rocket_dyn_templates::{tera::Context, Template}; +use sqlx::SqlitePool; + +use crate::model::mail::Mail; +use crate::model::role::Role; +use crate::model::user::AdminUser; +use crate::model::user::UserWithRoles; +use crate::tera::Config; + +#[get("/mail")] +async fn index( + db: &State, + admin: AdminUser, + flash: Option>, +) -> Template { + let mut context = Context::new(); + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + let roles = Role::all(db).await; + + context.insert( + "loggedin_user", + &UserWithRoles::from_user(admin.user, db).await, + ); + context.insert("roles", &roles); + + Template::render("admin/mail", context.into_json()) +} + +#[derive(FromForm, Debug)] +pub struct MailToSend<'a> { + pub(crate) role_id: i32, + pub(crate) subject: String, + pub(crate) body: String, + pub(crate) files: Vec>, +} + +#[post("/mail", data = "")] +async fn update( + db: &State, + data: Form>, + config: &State, + _admin: AdminUser, +) -> Flash { + let d = data.into_inner(); + if Mail::send(db, d, config.smtp_pw.clone()).await { + return Flash::success(Redirect::to("/admin/mail"), "Mail versendet"); + } else { + return Flash::error(Redirect::to("/admin/mail"), "Fehler"); + } +} + +pub fn routes() -> Vec { + routes![index, update] +} + +#[cfg(test)] +mod test {} diff --git a/src/tera/admin/mod.rs b/src/tera/admin/mod.rs index 0bf05e7..cb4e24d 100644 --- a/src/tera/admin/mod.rs +++ b/src/tera/admin/mod.rs @@ -7,6 +7,7 @@ use crate::{ }; pub mod boat; +pub mod mail; pub mod planned_event; pub mod user; @@ -28,6 +29,7 @@ pub fn routes() -> Vec { let mut ret = Vec::new(); ret.append(&mut user::routes()); ret.append(&mut boat::routes()); + ret.append(&mut mail::routes()); ret.append(&mut planned_event::routes()); ret.append(&mut routes![rss, show_rss]); ret diff --git a/src/tera/mod.rs b/src/tera/mod.rs index 7fa08bf..e9d510c 100644 --- a/src/tera/mod.rs +++ b/src/tera/mod.rs @@ -206,6 +206,7 @@ fn unauthorized_error() -> Redirect { #[serde(crate = "rocket::serde")] pub struct Config { rss_key: String, + smtp_pw: String, } pub fn config(rocket: Rocket) -> Rocket { diff --git a/templates/admin/mail.html.tera b/templates/admin/mail.html.tera new file mode 100644 index 0000000..be16840 --- /dev/null +++ b/templates/admin/mail.html.tera @@ -0,0 +1,27 @@ +{% import "includes/macros" as macros %} +{% import "includes/forms/boat" as boat %} + +{% extends "base" %} + +{% block content %} + {% if flash %} + {{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }} + {% endif %} + +
+

Mail

+
+ + + + + +
+ +
+ +{% endblock content %}