add mail #108

Merged
philipp merged 1 commits from mail into staging 2024-01-01 15:50:45 +01:00
10 changed files with 407 additions and 0 deletions
Showing only changes of commit 0ad342b147 - Show all commits

216
Cargo.lock generated
View File

@ -333,6 +333,16 @@ dependencies = [
"phf_codegen", "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]] [[package]]
name = "cipher" name = "cipher"
version = "0.4.4" version = "0.4.4"
@ -366,6 +376,16 @@ dependencies = [
"version_check", "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]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.6" version = "0.8.6"
@ -554,6 +574,22 @@ dependencies = [
"serde", "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]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.33" version = "0.8.33"
@ -664,6 +700,21 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 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]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.1" version = "1.2.1"
@ -948,6 +999,17 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "http" name = "http"
version = "0.2.11" version = "0.2.11"
@ -1186,6 +1248,31 @@ dependencies = [
"spin 0.5.2", "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]] [[package]]
name = "libc" name = "libc"
version = "0.2.151" version = "0.2.151"
@ -1246,6 +1333,12 @@ dependencies = [
"tracing-subscriber", "tracing-subscriber",
] ]
[[package]]
name = "match_cfg"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]] [[package]]
name = "matchers" name = "matchers"
version = "0.1.0" version = "0.1.0"
@ -1333,6 +1426,24 @@ dependencies = [
"version_check", "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]] [[package]]
name = "nix" name = "nix"
version = "0.27.1" version = "0.27.1"
@ -1471,6 +1582,50 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" 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]] [[package]]
name = "overload" name = "overload"
version = "0.1.1" version = "0.1.1"
@ -1732,6 +1887,15 @@ dependencies = [
"yansi", "yansi",
] ]
[[package]]
name = "psm"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "pure-rust-locales" name = "pure-rust-locales"
version = "0.7.0" version = "0.7.0"
@ -1747,6 +1911,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "quoted_printable"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -1968,6 +2138,7 @@ dependencies = [
"env_logger", "env_logger",
"futures", "futures",
"ics", "ics",
"lettre",
"log", "log",
"rocket", "rocket",
"rocket_dyn_templates", "rocket_dyn_templates",
@ -2067,6 +2238,15 @@ dependencies = [
"winapi-util", "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]] [[package]]
name = "scoped-tls" name = "scoped-tls"
version = "1.0.1" version = "1.0.1"
@ -2089,6 +2269,29 @@ dependencies = [
"untrusted", "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]] [[package]]
name = "serde" name = "serde"
version = "1.0.193" version = "1.0.193"
@ -2473,6 +2676,19 @@ dependencies = [
"memchr", "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]] [[package]]
name = "state" name = "state"
version = "0.6.0" version = "0.6.0"

View File

@ -22,3 +22,5 @@ chrono-tz = "0.8"
tera = { version = "1.18", features = ["date-locale"], optional = true} tera = { version = "1.18", features = ["date-locale"], optional = true}
ics = "0.5" ics = "0.5"
futures = "0.3" futures = "0.3"
lettre = "0.11"

View File

@ -2,3 +2,4 @@
secret_key = "/NtVGizglEoyoxBLzsRDWTy4oAG1qDw4J4O+CWJSv+fypD7W9sam8hUY4j90EZsbZk8wEradS5zBoWtWKi3k8w==" secret_key = "/NtVGizglEoyoxBLzsRDWTy4oAG1qDw4J4O+CWJSv+fypD7W9sam8hUY4j90EZsbZk8wEradS5zBoWtWKi3k8w=="
rss_key = "rss-key-for-ci" rss_key = "rss-key-for-ci"
limits = { file = "10 MiB", data-form = "10 MiB"} limits = { file = "10 MiB", data-form = "10 MiB"}
smtp_pw = "8kIjlLH79Ky6D3jQ"

65
src/model/mail.rs Normal file
View File

@ -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 <no-reply@rudernlinz.at>"
.parse()
.unwrap(),
)
.reply_to(
"ASKÖ Ruderverein Donau Linz <info@rudernlinz.at>"
.parse()
.unwrap(),
)
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.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
}
}

View File

@ -13,6 +13,7 @@ pub mod location;
pub mod log; pub mod log;
pub mod logbook; pub mod logbook;
pub mod logtype; pub mod logtype;
pub mod mail;
pub mod planned_event; pub mod planned_event;
pub mod role; pub mod role;
pub mod rower; pub mod rower;

View File

@ -14,4 +14,32 @@ impl Role {
.await .await
.unwrap() .unwrap()
} }
pub async fn find_by_id(db: &SqlitePool, name: i32) -> Option<Self> {
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<String> {
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()
}
} }

64
src/tera/admin/mail.rs Normal file
View File

@ -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<SqlitePool>,
admin: AdminUser,
flash: Option<FlashMessage<'_>>,
) -> 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<TempFile<'a>>,
}
#[post("/mail", data = "<data>")]
async fn update(
db: &State<SqlitePool>,
data: Form<MailToSend<'_>>,
config: &State<Config>,
_admin: AdminUser,
) -> Flash<Redirect> {
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<Route> {
routes![index, update]
}
#[cfg(test)]
mod test {}

View File

@ -7,6 +7,7 @@ use crate::{
}; };
pub mod boat; pub mod boat;
pub mod mail;
pub mod planned_event; pub mod planned_event;
pub mod user; pub mod user;
@ -28,6 +29,7 @@ pub fn routes() -> Vec<Route> {
let mut ret = Vec::new(); let mut ret = Vec::new();
ret.append(&mut user::routes()); ret.append(&mut user::routes());
ret.append(&mut boat::routes()); ret.append(&mut boat::routes());
ret.append(&mut mail::routes());
ret.append(&mut planned_event::routes()); ret.append(&mut planned_event::routes());
ret.append(&mut routes![rss, show_rss]); ret.append(&mut routes![rss, show_rss]);
ret ret

View File

@ -206,6 +206,7 @@ fn unauthorized_error() -> Redirect {
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
pub struct Config { pub struct Config {
rss_key: String, rss_key: String,
smtp_pw: String,
} }
pub fn config(rocket: Rocket<Build>) -> Rocket<Build> { pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {

View File

@ -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 %}
<div class="max-w-screen-lg w-full">
<h1 class="h1">Mail</h1>
<form action="/admin/mail" method="post" enctype="multipart/form-data">
<select name="role_id">
{% for role in roles %}
<option value="{{ role.id }}">{{ role.name }}</option>
{% endfor %}
</select>
<input type="text" name="subject" />
<textarea name="body" rows="4" cols="50"></textarea>
<input type="file" name="files" multiple />
<input type="submit" />
</form>
</div>
{% endblock content %}