Initial commit

This commit is contained in:
Philip (a-0) 2022-08-29 21:28:00 +02:00
commit efc38e161c
10 changed files with 3455 additions and 0 deletions

62
src/config.rs Normal file
View file

@ -0,0 +1,62 @@
use serde::{Serialize, Deserialize};
use std::{fs, path::Path};
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub struct Config {
pub bind_address: Option<String>,
pub bind_port: Option<String>,
pub client_storage_path: Option<String>,
pub receiver_whitelist: Option<Vec<String>>,
pub clients: Vec<ClientConfig>,
pub mappings: Vec<Mapping>,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub struct ClientConfig {
pub mxid: String,
pub password: String,
pub device_id: Option<String>,
pub access_token: Option<String>,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub struct Mapping {
pub from: Option<String>,
pub to: Option<String>,
pub mxid_sender: Option<String>,
pub room_id: String,
}
pub fn load_config(file_path: &str) -> Config {
let yaml_str = fs::read_to_string(file_path).expect("Error while reading config file");
let mut cfg: Config = serde_yaml::from_str(&yaml_str).expect("Config file not in expected format");
// Set default values if necessary
if cfg.bind_address.is_none() {
cfg.bind_address = Some("127.0.0.1".to_string());
}
if cfg.bind_port.is_none() {
cfg.bind_port = Some("25".to_string());
}
if cfg.client_storage_path.is_none() {
let path = Path::new(file_path).parent().unwrap().join("client_storage");
cfg.client_storage_path = Some(path.to_str().unwrap().to_string());
}
cfg
}
pub fn write_config(config: &Config, old_config: &Config, file_path: &str) {
let new_yaml_str = serde_yaml::to_string(config).expect("Failed to serialize new config.");
let old_yaml_str = serde_yaml::to_string(old_config).expect("Failed to serialize old config.");
if new_yaml_str != old_yaml_str {
rotate_configs(file_path);
fs::write(file_path, &new_yaml_str).expect("Error writing new config to file");
}
}
fn rotate_configs(base_path: &str) {
for i in 9..2 {
std::fs::copy(&format!("{}.{}", base_path, i - 1), &format!("{}.{}", base_path, i)).expect("");
}
std::fs::copy(base_path, &format!("{}.1", base_path)).expect("");
}

63
src/main.rs Normal file
View file

@ -0,0 +1,63 @@
use std::sync::Arc;
use async_trait::async_trait;
use matrix_sdk::Client;
use postbus::{Handler, SmtpService, SmtpState, command::Mailbox};
use crate::config::Mapping;
mod matrix;
mod config;
#[tokio::main]
async fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() != 2 {
panic!("The path to the configuration file must be supplied as (only) command line argument!");
}
let mut config = crate::config::load_config(&args[1]);
let immutable_config = config.clone();
let clients = matrix::get_clients(&mut config).await;
crate::config::write_config(&config, &immutable_config, &args[1]);
let service = SmtpService::create(format!("{}:{}", config.bind_address.unwrap(), config.bind_port.unwrap()).parse().unwrap(), "Name".into(),
Arc::new(ToMatrixConverter {
receiver_whitelist: config.receiver_whitelist,
matrix_clients: clients,
mappings: config.mappings,
}));
service.listen().await;
}
struct ToMatrixConverter {
// If None, any incoming mail will be accepted.
// If Some(x), any mail with a mail address in x in its recipients will be accepted.
// "Not accepted" means the sender will get a "RCPT failed" reply
receiver_whitelist: Option<Vec<String>>,
matrix_clients: Vec<Client>,
mappings: Vec<Mapping>,
}
#[async_trait]
impl Handler for ToMatrixConverter {
// Checks whether receipient is local (and the mail should thus be handled by this service)
async fn recipient_local(&self, recipient: &Mailbox) -> bool {
match &self.receiver_whitelist {
None => true,
Some(list) => {
let addr = format!("{}@{}", recipient.local, recipient.domain.0);
for entry in list {
if entry.eq(&addr) {
return true;
}
}
false
}
}
}
async fn save(&self, state: &SmtpState) -> bool {
for recipient in &state.recipients {
matrix::send(&state.from, &recipient, &state.data, &self.matrix_clients, &self.mappings).await;
}
true
}
}

133
src/matrix.rs Normal file
View file

@ -0,0 +1,133 @@
use matrix_sdk::{Client, ruma::{UserId, RoomId, events::{room::message::RoomMessageEventContent}}, Session, room::Room, config::SyncSettings};
use postbus::command::Mailbox;
use crate::config::{Mapping, Config};
pub async fn get_clients(config: &mut Config) -> Vec<Client> {
let mut clients: Vec<Client> = vec![];
// Try to build a matrix_sdk::Client object for each configured client
for conf in &mut config.clients {
let user_id = match UserId::parse(&conf.mxid) {
Ok(id) => id,
Err(_e) => continue,
};
let client = match Client::builder().user_id(&user_id).build().await {
Ok(c) => c,
Err(_e) => continue,
};
// Attempt to restore a previous login
if let Some(token) = &conf.access_token {
if let Some(dev_id) = &conf.device_id {
match client.restore_login(Session { access_token: token.to_string(), user_id: (&user_id).to_owned(), device_id: dev_id.as_str().into()}).await {
Ok(_) => (),
Err(_e) => continue,
};
}
}
// If login restoration did not work, log in using the password
if !client.logged_in().await {
match conf.password.len() {
0 => continue,
_ => match client.login(
user_id,
&conf.password,
(&conf).device_id.as_deref(),
Some("matrixmailer")
).await {
Ok(_) => (),
Err(_) => continue,
}
}
}
clients.push(client.clone());
}
clients
}
async fn client_from_mxid(mxid: &str, clients: &Vec<Client>) -> Option<Client> {
for client in clients {
if client.user_id().await.unwrap().as_str() == mxid {
return Some(client.to_owned());
}
}
None
}
pub async fn send(from_mail: &Option<Mailbox>, recipient: &Mailbox, content: &str, clients: &Vec<Client>, mappings: &Vec<Mapping>) {
for mapping in mappings {
let mut applies = true;
if let Some(from) = &mapping.from {
if let Some(mailbox) = from_mail {
if from != &format!("{}@{}", mailbox.local, mailbox.domain.0) {
applies = false;
}
}
}
if let Some(to) = &mapping.to {
if to != &format!("{}@{}", recipient.local, recipient.domain.0) {
applies = false;
}
}
if applies {
let local = &recipient.local;
let domain = &recipient.domain.0;
let sender_mxid = match &mapping.mxid_sender {
Some(s) => s.to_owned(),
None => {
format!("@{}:{}", local, domain)
},
};
if let Some(c) = client_from_mxid(&sender_mxid, clients).await {
if let Ok(roomid) = RoomId::parse(&mapping.room_id) {
match c.sync_once(SyncSettings::default()).await {
Ok(_) => (),
Err(_e) => (),
};
let room = match c.get_room(&roomid) {
Some(Room::Joined(room)) => room,
Some(Room::Invited(room)) => {
match room.accept_invitation().await {
Ok(_) => (),
Err(_e) => println!("Could not accept invitation"),
};
// Continuously check whether the room has been joined.
// Joins can be significantly delayed, depending on the room and the server
let mut joined = false;
while !joined {
match c.get_room(&roomid) {
Some(Room::Joined(_)) => joined = true,
_ => (),
}
match c.sync_once(SyncSettings::default()).await {
Ok(_) => (),
Err(_e) => (),
};
std::thread::sleep(std::time::Duration::from_millis(250));
}
if let Some(Room::Joined(joined)) = c.get_room(&roomid) {
joined
}
else {
continue;
}
},
_ => {
println!("Room either left or unfound.");
continue;
}
};
match room.send(RoomMessageEventContent::text_html(content, content), None).await {
Ok(_) => (),
Err(_e) => println!("Sending message failed"),
};
}
}
}
}
}