Initial Commit
All checks were successful
Build / Check format (push) Successful in 36s
Build / Lint Uketoru (push) Successful in 1m13s
Build / Build Uketoru (push) Successful in 1m45s

This commit is contained in:
Xavier Moffett 2024-10-23 19:19:53 -04:00
commit 3851d9df21
Signed by: Sapphirus
GPG key ID: A6C061B2CEA1A7AC
13 changed files with 2015 additions and 0 deletions

125
src/config.rs Normal file
View file

@ -0,0 +1,125 @@
/*
* Uketoru
*
* Copyright (C) 2024 Xavier Moffett <sapphirus@azorium.net>
* SPDX-License-Identifier: MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use lettre::transport::smtp::authentication::Credentials;
use serde::Deserialize;
use std::{fs::File, io::Read, sync::LazyLock};
pub static CONFIG: LazyLock<Config> = LazyLock::new(load);
#[allow(clippy::upper_case_acronyms)]
#[derive(Deserialize, Debug, Clone, Default)]
pub enum TransportType {
#[default]
TLS,
StartTLS,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Smtp {
#[serde(default = "TransportType::default")]
transport: TransportType,
address: String,
to: Option<String>,
name: Option<String>,
username: Option<String>,
#[serde(default)]
password: String,
server: String,
port: Option<u16>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Relay {
listen: Option<String>,
token: Option<String>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Config {
relay: Relay,
smtp: Smtp,
}
impl Config {
pub fn client(&self) -> &Smtp {
&self.smtp
}
pub fn relay(&self) -> &Relay {
&self.relay
}
}
impl Relay {
pub fn token(&self) -> Option<&str> {
self.token.as_deref()
}
pub fn listen_addr(&self) -> &str {
self.listen.as_deref().unwrap_or("0.0.0.0:3000")
}
}
impl Smtp {
pub fn transport(&self) -> &TransportType {
&self.transport
}
pub fn credentials(&self) -> Credentials {
match &self.username {
Some(username) => Credentials::new(username.to_owned(), self.password.to_owned()),
None => Credentials::new(self.address.to_owned(), self.password.to_owned()),
}
}
pub fn server(&self) -> &str {
&self.server
}
pub fn port(&self) -> u16 {
self.port.unwrap_or(25)
}
pub fn address(&self) -> &str {
&self.address
}
pub fn to(&self) -> &str {
self.to.as_deref().unwrap_or(&self.address)
}
pub fn name(&self) -> &str {
self.name.as_deref().unwrap_or("Uketoru")
}
}
fn load() -> Config {
let mut string = String::new();
let mut config = File::open("config.toml").expect("Opening config.toml");
config.read_to_string(&mut string).expect("Reading config.toml");
toml::from_str::<Config>(&string).expect("Invalid config.toml")
}

46
src/main.rs Normal file
View file

@ -0,0 +1,46 @@
/*
* Uketoru
*
* Copyright (C) 2024 Xavier Moffett <sapphirus@azorium.net>
* SPDX-License-Identifier: MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use axum::{middleware, routing::post, serve, Router};
use std::io::Error;
use tokio::net::TcpListener;
use crate::{config::CONFIG, relay::*};
mod config;
mod relay;
#[tokio::main]
async fn main() -> Result<(), Error> {
let router = Router::new()
.route("/api", post(message).get(default))
.fallback(default)
.layer(middleware::from_fn(validate));
let address = CONFIG.relay().listen_addr();
let listen = TcpListener::bind(address).await?;
println!("Uketoru listening on {address}");
serve(listen, router).await
}

95
src/relay.rs Normal file
View file

@ -0,0 +1,95 @@
/*
* Uketoru
*
* Copyright (C) 2024 Xavier Moffett <sapphirus@azorium.net>
* SPDX-License-Identifier: MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use axum::{
body::Body,
extract::Request,
http::{header, StatusCode},
middleware::Next,
response::Response,
Json,
};
use lettre::{message::header::ContentType, Message, SmtpTransport, Transport};
use serde::Deserialize;
use crate::config::{TransportType, CONFIG};
#[derive(Deserialize)]
pub struct RelayMessage {
from: String,
text: String,
}
pub async fn default() -> (StatusCode, Body) {
(StatusCode::NOT_FOUND, Body::from("Not Found"))
}
pub async fn validate(req: Request, next: Next) -> Result<Response, StatusCode> {
let token = match CONFIG.relay().token() {
Some(token) => token,
None => return Ok(next.run(req).await),
};
let auth = match req.headers().get(header::AUTHORIZATION) {
Some(header) => header.to_str().map_err(|_| StatusCode::BAD_REQUEST)?,
None => Err(StatusCode::UNAUTHORIZED)?,
};
if !auth.starts_with("Bearer") || auth.len() < 7 {
Err(StatusCode::BAD_REQUEST)?
} else if &auth[7 ..] != token {
Err(StatusCode::UNAUTHORIZED)?
}
Ok(next.run(req).await)
}
pub async fn message(Json(msg): Json<RelayMessage>) -> (StatusCode, Body) {
let client = CONFIG.client();
let message = Message::builder()
.from(format!("{} <{}>", msg.from, client.address()).parse().expect("Invalid address"))
.to(format!("{} <{}>", client.name(), client.to()).parse().expect("Invalid address"))
.subject(format!("Message received from {} via Uketoru", msg.from))
.header(ContentType::TEXT_PLAIN)
.body(msg.text)
.expect("Message builder");
let transport = match CONFIG.client().transport() {
TransportType::TLS => SmtpTransport::relay(CONFIG.client().server()),
TransportType::StartTLS => SmtpTransport::starttls_relay(CONFIG.client().server()),
};
let transport = match transport {
Ok(transport) => transport,
Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, Body::from(format!("Internal Error: {err}"))),
};
match transport
.port(CONFIG.client().port())
.credentials(CONFIG.client().credentials())
.build()
.send(&message)
{
Ok(_) => (StatusCode::OK, Body::empty()),
Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, Body::from(format!("Internal Error: {err}"))),
}
}