API and mail sending implementation

main
Ashelyn Dawn 1 month ago committed by Ashelyn Rose
commit e2f5f10cf7
No known key found for this signature in database
GPG Key ID: D1980B8C6F349BC1

2
.gitignore vendored

@ -0,0 +1,2 @@
/target
config.toml

2560
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,14 @@
[package]
name = "contact-rs"
version = "0.1.0"
edition = "2021"
[dependencies]
rocket = { version = "0.5.0", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.114"
toml = "0.8.9"
governor = "0.6.3"
mail-send = "0.4.7"
mail-builder = "0.3.1"

@ -0,0 +1,56 @@
use std::{num::NonZeroU32, path::Path};
use std::{fs, io};
use serde::Deserialize;
#[derive(Deserialize)]
pub struct Config {
#[serde(rename="form")]
pub forms: Vec<Form>,
pub mailer: EmailConfig,
pub rate_limit: RateConfig,
}
#[derive(Deserialize)]
pub struct Form {
pub slug: String,
pub name: String,
pub recipient_email: String,
pub subject: String,
pub fields: Vec<Field>,
}
#[derive(Deserialize)]
pub struct Field {
pub name: String,
pub pattern: Option<String>,
pub required: bool,
}
#[derive(Deserialize)]
pub struct EmailConfig {
pub from_address: String,
pub host: String,
pub port: u16,
pub implicit_tls: bool,
pub username: String,
pub password: String
}
#[derive(Deserialize)]
pub struct RateConfig {
pub burst_max: NonZeroU32,
pub replenish_seconds: u32,
}
impl Config {
pub fn parse(path: &Path) -> Result<Self, io::Error> {
let config_str = fs::read_to_string(path)?;
match toml::from_str(&config_str) {
Ok(config) => return Ok(config),
// TODO: Print warning about parse error
Err(_err) => Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid syntax for config file")),
}
}
}

@ -0,0 +1,99 @@
use std::convert::Infallible;
use std::net::IpAddr;
use std::collections::HashMap;
use rocket::request::{self, FromRequest};
use rocket::serde::json::Json;
use rocket::http::Status;
use rocket::Request;
use mail_builder::MessageBuilder;
pub struct ClientIp(Option<IpAddr>);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for ClientIp {
type Error = Infallible;
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
request::Outcome::Success(ClientIp(request.client_ip()))
}
}
#[post("/api/contact/<slug>", data = "<fields>")]
pub async fn handler(state: &rocket::State<crate::State>, client_ip: ClientIp, slug: &str, fields: Json<HashMap<&str, &str>>) -> Result<String, (Status, String)> {
{
let rate_limit = &state.rate_limit;
if client_ip.0.is_none() {
return Err((Status::InternalServerError, "Could not identify client IP".to_string()))
}
let client_ip = client_ip.0.unwrap();
let rate_limit_outcome = rate_limit.check_key(&client_ip);
if let Err(_not_until) = rate_limit_outcome {
return Err((Status::TooManyRequests, "Rate limit error".to_string()));
}
println!("Passed rate limit");
};
{
let email_client = &state.smtp_builder;
let mail_conf = &state.config.mailer;
let form_conf = &state.config.forms.iter().find(|form| {
form.slug == slug
});
if form_conf.is_none() {
return Err((Status::NotFound, "Not found".to_string()));
}
let form_conf = form_conf.unwrap();
let mut message_text = format!(
"You have recieved a message on {}:
Form fields:", form_conf.name);
for field in &form_conf.fields {
let value_opt = fields.get(field.name.as_str());
let required = field.required;
if value_opt.is_none() && required {
return Err((Status::BadRequest, format!("Missing field: {}", field.name)))
}
message_text.push_str(&format!(" - {}: {}\n", field.name, value_opt.unwrap_or(&"[empty]")));
}
let message = MessageBuilder::new()
.from(mail_conf.from_address.clone())
.to(form_conf.recipient_email.clone())
.subject(form_conf.subject.clone())
.text_body(message_text);
println!("Message composed");
let connection_result = email_client.connect().await;
if let Err(err) = connection_result {
println!("Error connecting to server: {}", err.to_string());
return Err((Status::InternalServerError, "Could not send message".to_string()));
} else {
let mut connection = connection_result.unwrap();
println!("Connected to mail server");
if let Err(err) = connection.send(message).await {
println!("Error sending email: {}", err.to_string());
return Err((Status::InternalServerError, "Could not send message".to_string()));
}
}
println!("Message sent");
return Ok("Message sent".to_string());
};
}

@ -0,0 +1,60 @@
#[macro_use] extern crate rocket;
use std::{net::IpAddr, path::Path, time::Duration};
use config::Config;
use governor::{clock::DefaultClock, state::keyed::DashMapStateStore, Quota, RateLimiter};
use mail_send::{SmtpClientBuilder, Credentials};
use rocket::data::{Limits, ToByteUnit};
mod config;
mod endpoints;
pub struct State {
pub rate_limit: RateLimiter<IpAddr,DashMapStateStore<IpAddr>,DefaultClock>,
pub smtp_builder: SmtpClientBuilder<String>,
pub config: Config,
}
#[launch]
fn rocket() -> _ {
let args: Vec<String> = std::env::args().collect();
if args.len() - 1 != 1 {
panic!("Expected 1 argument, got {}", args.len() - 1)
}
let path = Path::new(&args[1]);
let config = crate::config::Config::parse(path).unwrap();
let smtp_builder = SmtpClientBuilder::new(config.mailer.host.clone(), config.mailer.port)
.timeout(Duration::from_secs(5))
.implicit_tls(config.mailer.implicit_tls)
.credentials(Credentials::Plain {
username: config.mailer.username.clone(),
secret: config.mailer.password.clone()
});
let quota = Quota::with_period(
Duration::from_secs(config.rate_limit.replenish_seconds.into())
).unwrap().allow_burst(config.rate_limit.burst_max);
let rate_limit : RateLimiter<IpAddr, _, _> = RateLimiter::dashmap(quota);
let state = State {
config,
rate_limit,
smtp_builder
};
let rocket_conf = rocket::config::Config {
limits: Limits::default()
.limit("json", 2.kibibytes()),
..rocket::config::Config::default()
};
rocket::custom(rocket_conf)
.manage(state)
.mount("/", routes![
endpoints::handler
])
}
Loading…
Cancel
Save