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

View file

@ -0,0 +1,74 @@
name: Build
on:
workflow_dispatch:
push:
branches:
- master
env:
CARGO_TERM_COLOR: always
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.cargo/bin
RUSTFLAGS: -C target-feature=-crt-static
jobs:
checkfmt:
name: Check format
runs-on: docker
container:
image: alpine
strategy:
matrix:
toolchain:
- nightly
steps:
- name: Install packages
run: apk add --no-cache rustup nodejs git
- name: Checkout repository
uses: actions/checkout@v4
- name: Install rustup
run: rustup-init -y --default-toolchain none
- name: Install toolchain
run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
- name: Check format
run: cargo fmt --check
lint:
name: Lint Uketoru
runs-on: docker
container:
image: alpine
strategy:
matrix:
toolchain:
- stable
steps:
- name: Install packages
run: apk add --no-cache rustup nodejs git build-base openssl-dev pkgconfig
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize rustup
run: rustup-init -y --default-toolchain none
- name: Install toolchain
run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
- name: Lint Uketoru
run: cargo clippy --release -- -Dwarnings
build:
name: Build Uketoru
runs-on: docker
container:
image: alpine
strategy:
matrix:
toolchain:
- stable
steps:
- name: Install packages
run: apk add --no-cache rustup nodejs git build-base pkgconfig openssl-dev
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize rustup
run: rustup-init -y --default-toolchain none
- name: Install toolchain
run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
- name: Build Uketoru
run: cargo build --release

78
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,78 @@
name: Build
on:
workflow_dispatch:
pull_request:
types:
- opened
- ready_for_review
- synchronize
branches:
- master
env:
CARGO_TERM_COLOR: always
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.cargo/bin
RUSTFLAGS: -C target-feature=-crt-static
jobs:
checkfmt:
name: Check format
runs-on: docker
container:
image: alpine
strategy:
matrix:
toolchain:
- nightly
steps:
- name: Install packages
run: apk add --no-cache rustup nodejs git
- name: Checkout repository
uses: actions/checkout@v4
- name: Install rustup
run: rustup-init -y --default-toolchain none
- name: Install toolchain
run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
- name: Check format
run: cargo fmt --check
lint:
name: Lint Uketoru
runs-on: docker
container:
image: alpine
strategy:
matrix:
toolchain:
- stable
steps:
- name: Install packages
run: apk add --no-cache rustup nodejs git build-base openssl-dev pkgconfig
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize rustup
run: rustup-init -y --default-toolchain none
- name: Install toolchain
run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
- name: Lint Uketoru
run: cargo clippy --release -- -Dwarnings
build:
name: Build Uketoru
runs-on: docker
container:
image: alpine
strategy:
matrix:
toolchain:
- stable
steps:
- name: Install packages
run: apk add --no-cache rustup nodejs git build-base pkgconfig openssl-dev
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize rustup
run: rustup-init -y --default-toolchain none
- name: Install toolchain
run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
- name: Build Uketoru
run: cargo build --release

2
.gitignore vendored Normal file
View file

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

16
.rustfmt.toml Normal file
View file

@ -0,0 +1,16 @@
unstable_features = true
indent_style = "Block"
imports_indent = "Block"
imports_layout = "HorizontalVertical"
imports_granularity = "Crate"
brace_style = "PreferSameLine"
match_arm_leading_pipes = "Never"
match_arm_blocks = false
condense_wildcard_suffixes = true
overflow_delimited_expr = false
spaces_around_ranges = true
reorder_imports = true
hard_tabs = false
max_width = 130
fn_call_width = 120
chain_width = 90

1478
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

14
Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "uketoru"
license = "MIT"
authors = [ "Xavier Moffett <sapphirus@azorium.net>" ]
version = "0.1.0"
edition = "2021"
rust-version="1.82"
[dependencies]
axum = { version = "0.7.7", default-features = false, features = [ "json", "http1", "tokio" ] }
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
lettre = "0.11.10"
toml = "0.8.19"

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (C) 2024 Xavier Moffett <sapphirus@azorium.net>
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.

11
README.md Normal file
View file

@ -0,0 +1,11 @@
# Uketoru [![Build Workflow](https://git.sapphirus.org/Sapphirus/Uketoru/badges/workflows/build.yml/badge.svg?label=build&logo=github+actions&logoColor=d1d7e0&style=flat-square)](https://git.sapphirus.org/Sapphirus/Uketoru/actions?workflow=build.yml)
Uketoru provides a simple, yet configurable middleware to relay messages to a SMTP server.
## See Also
[SMS to URL Forwarder](https://github.com/bogkonstantin/android_income_sms_gateway_webhook)
## License
[MIT license](./LICENSE)

43
config.toml.example Normal file
View file

@ -0,0 +1,43 @@
[relay]
# Declare a token here to enable token authentication.
# Leaving this value undeclared will disable this functionality.
#
# token = "TOKEN_HERE"
#
# Listen value declaration structured with the hostname:port
#
# listen = "0.0.0.0:3000"
#
[smtp]
# SMTP Transport Type. Available types: TLS and StartTLS.
# By default, TLS is selected when this value is undeclared.
#
# transport = "StartTLS"
#
# Hostname for the SMTP Mailer this gateway is relaying messages towards.
# Note: This value is required.
#
server = "smtp.example.com"
# Declare this value if the SMTP Mailer is on a different port. e.g. SUBMISSION 587
#
# port = 587
#
# E-mail Address being used to relay messages.
# Note: This value is required.
#
address = "mail@example.com"
# Sender name for the relay address.
# The default naem is Uketoru.
#
# name = "Sender Name"
#
# Username credential used to authenticate against the SMTP mailer.
# If this value is undeclared, it will default to the e-mail adddress specified.
#
# username = "user"
#
# Pasword credential used to authenticate against the SMTP mailer.
#
#password = "password"

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}"))),
}
}

12
systemd/uketoru.service Normal file
View file

@ -0,0 +1,12 @@
[Unit]
Description=Uketoru SMTP relay
After=network.target
[Service]
Type=simple
WorkingDirectory=/etc/uketoru
ExecStart=/usr/bin/uketoru
KillMode=process
[Install]
WantedBy=multi-user.target