Initial Commit
This commit is contained in:
commit
3851d9df21
13 changed files with 2015 additions and 0 deletions
74
.forgejo/workflows/build.yml
Normal file
74
.forgejo/workflows/build.yml
Normal 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
78
.github/workflows/build.yml
vendored
Normal 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
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
/config.toml
|
16
.rustfmt.toml
Normal file
16
.rustfmt.toml
Normal 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
1478
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal 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
21
LICENSE
Normal 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
11
README.md
Normal 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
43
config.toml.example
Normal 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
125
src/config.rs
Normal 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
46
src/main.rs
Normal 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
95
src/relay.rs
Normal 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
12
systemd/uketoru.service
Normal 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
|
Loading…
Reference in a new issue