feat: smtp email sender
This commit is contained in:
parent
72a2ab9815
commit
119493e850
108
Cargo.lock
generated
108
Cargo.lock
generated
@ -1,6 +1,6 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
@ -512,6 +512,16 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chumsky"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
"stacker",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cipher"
|
name = "cipher"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
@ -896,6 +906,22 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "email-encoding"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ea3d894bbbab314476b265f9b2d46bf24b123a36dd0e96b06a1b49545b9d9dcc"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "email_address"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.35"
|
version = "0.8.35"
|
||||||
@ -1150,7 +1176,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"windows",
|
"windows 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1350,6 +1376,17 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hostname"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"windows 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
@ -1840,6 +1877,34 @@ dependencies = [
|
|||||||
"spin",
|
"spin",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lettre"
|
||||||
|
version = "0.11.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0161e452348e399deb685ba05e55ee116cae9410f4f51fe42d597361444521d9"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"chumsky",
|
||||||
|
"email-encoding",
|
||||||
|
"email_address",
|
||||||
|
"fastrand",
|
||||||
|
"futures-io",
|
||||||
|
"futures-util",
|
||||||
|
"hostname",
|
||||||
|
"httpdate",
|
||||||
|
"idna",
|
||||||
|
"mime",
|
||||||
|
"native-tls",
|
||||||
|
"nom",
|
||||||
|
"percent-encoding",
|
||||||
|
"quoted_printable",
|
||||||
|
"socket2",
|
||||||
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.167"
|
version = "0.2.167"
|
||||||
@ -1912,6 +1977,7 @@ dependencies = [
|
|||||||
"config",
|
"config",
|
||||||
"fedimint-tonic-lnd",
|
"fedimint-tonic-lnd",
|
||||||
"ipnetwork",
|
"ipnetwork",
|
||||||
|
"lettre",
|
||||||
"lnvps_db",
|
"lnvps_db",
|
||||||
"log",
|
"log",
|
||||||
"nostr",
|
"nostr",
|
||||||
@ -2686,6 +2752,15 @@ dependencies = [
|
|||||||
"prost",
|
"prost",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psm"
|
||||||
|
version = "0.1.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.37"
|
version = "1.0.37"
|
||||||
@ -2695,6 +2770,12 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quoted_printable"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
@ -3674,6 +3755,19 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stacker"
|
||||||
|
version = "0.1.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"psm",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "state"
|
name = "state"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
@ -4445,6 +4539,16 @@ dependencies = [
|
|||||||
"windows-targets 0.48.5",
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
@ -28,3 +28,4 @@ rand = "0.8.5"
|
|||||||
clap = { version = "4.5.21", features = ["derive"] }
|
clap = { version = "4.5.21", features = ["derive"] }
|
||||||
ssh2 = "0.9.4"
|
ssh2 = "0.9.4"
|
||||||
ssh-key = "0.6.7"
|
ssh-key = "0.6.7"
|
||||||
|
lettre = { version = "0.11.10", features = ["tokio1-native-tls"] }
|
||||||
|
@ -38,7 +38,12 @@ async fn main() -> Result<(), Error> {
|
|||||||
db.migrate().await?;
|
db.migrate().await?;
|
||||||
|
|
||||||
let exchange = ExchangeRateCache::new();
|
let exchange = ExchangeRateCache::new();
|
||||||
let lnd = connect(settings.lnd.url, settings.lnd.cert, settings.lnd.macaroon).await?;
|
let lnd = connect(
|
||||||
|
settings.lnd.url.clone(),
|
||||||
|
settings.lnd.cert.clone(),
|
||||||
|
settings.lnd.macaroon.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
{
|
{
|
||||||
let setup_script = include_str!("../../dev_setup.sql");
|
let setup_script = include_str!("../../dev_setup.sql");
|
||||||
@ -52,13 +57,18 @@ async fn main() -> Result<(), Error> {
|
|||||||
.get_provisioner(db.clone(), lnd.clone(), exchange.clone());
|
.get_provisioner(db.clone(), lnd.clone(), exchange.clone());
|
||||||
worker_provisioner.init().await?;
|
worker_provisioner.init().await?;
|
||||||
|
|
||||||
let mut worker = Worker::new(
|
let mut worker = Worker::new(db.clone(), worker_provisioner, &settings, status.clone());
|
||||||
db.clone(),
|
|
||||||
worker_provisioner,
|
|
||||||
settings.delete_after,
|
|
||||||
status.clone(),
|
|
||||||
);
|
|
||||||
let sender = worker.sender();
|
let sender = worker.sender();
|
||||||
|
|
||||||
|
// send a startup notification
|
||||||
|
if let Some(admin) = &settings.smtp.and_then(|s| s.admin) {
|
||||||
|
sender.send(WorkJob::SendNotification {
|
||||||
|
title: Some("Startup".to_string()),
|
||||||
|
message: "System is starting!".to_string(),
|
||||||
|
user_id: *admin,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
if let Err(e) = worker.handle().await {
|
if let Err(e) = worker.handle().await {
|
||||||
|
@ -6,25 +6,48 @@ use lnvps_db::LNVpsDb;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub listen: Option<String>,
|
pub listen: Option<String>,
|
||||||
pub db: String,
|
pub db: String,
|
||||||
pub lnd: LndConfig,
|
pub lnd: LndConfig,
|
||||||
|
|
||||||
|
/// Main control process impl
|
||||||
pub provisioner: ProvisionerConfig,
|
pub provisioner: ProvisionerConfig,
|
||||||
|
|
||||||
/// Number of days after an expired VM is deleted
|
/// Number of days after an expired VM is deleted
|
||||||
pub delete_after: u16,
|
pub delete_after: u16,
|
||||||
|
|
||||||
|
/// SMTP settings for sending emails
|
||||||
|
pub smtp: Option<SmtpConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct LndConfig {
|
pub struct LndConfig {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub cert: PathBuf,
|
pub cert: PathBuf,
|
||||||
pub macaroon: PathBuf,
|
pub macaroon: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct SmtpConfig {
|
||||||
|
/// Admin user id, for sending system notifications
|
||||||
|
pub admin: Option<u64>,
|
||||||
|
|
||||||
|
/// Email server host:port
|
||||||
|
pub server: String,
|
||||||
|
|
||||||
|
/// From header to use, otherwise empty
|
||||||
|
pub from: Option<String>,
|
||||||
|
|
||||||
|
/// Username for SMTP connection
|
||||||
|
pub username: String,
|
||||||
|
|
||||||
|
/// Password for SMTP connection
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub enum ProvisionerConfig {
|
pub enum ProvisionerConfig {
|
||||||
Proxmox {
|
Proxmox {
|
||||||
/// Readonly mode, don't spawn any VM's
|
/// Readonly mode, don't spawn any VM's
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
use crate::host::get_host_client;
|
use crate::host::get_host_client;
|
||||||
use crate::host::proxmox::{VmInfo, VmStatus};
|
use crate::host::proxmox::{VmInfo, VmStatus};
|
||||||
use crate::provisioner::Provisioner;
|
use crate::provisioner::Provisioner;
|
||||||
|
use crate::settings::{Settings, SmtpConfig};
|
||||||
use crate::status::{VmRunningState, VmState, VmStateCache};
|
use crate::status::{VmRunningState, VmState, VmStateCache};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use chrono::{Days, Utc};
|
use chrono::{Days, Utc};
|
||||||
|
use lettre::message::MessageBuilder;
|
||||||
|
use lettre::transport::smtp::authentication::Credentials;
|
||||||
|
use lettre::transport::smtp::SmtpTransportBuilder;
|
||||||
|
use lettre::AsyncTransport;
|
||||||
|
use lettre::{AsyncSmtpTransport, SmtpTransport, Tokio1Executor, Transport};
|
||||||
use lnvps_db::LNVpsDb;
|
use lnvps_db::LNVpsDb;
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
|
use rocket::futures::SinkExt;
|
||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
||||||
|
|
||||||
@ -17,11 +24,15 @@ pub enum WorkJob {
|
|||||||
/// This job starts a vm if stopped and also creates the vm if it doesn't exist yet
|
/// This job starts a vm if stopped and also creates the vm if it doesn't exist yet
|
||||||
CheckVm { vm_id: u64 },
|
CheckVm { vm_id: u64 },
|
||||||
/// Send a notification to the users chosen contact preferences
|
/// Send a notification to the users chosen contact preferences
|
||||||
SendNotification { user_id: u64, message: String },
|
SendNotification {
|
||||||
|
user_id: u64,
|
||||||
|
message: String,
|
||||||
|
title: Option<String>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Worker {
|
pub struct Worker {
|
||||||
delete_after: u16,
|
settings: WorkerSettings,
|
||||||
db: Box<dyn LNVpsDb>,
|
db: Box<dyn LNVpsDb>,
|
||||||
provisioner: Box<dyn Provisioner>,
|
provisioner: Box<dyn Provisioner>,
|
||||||
vm_state_cache: VmStateCache,
|
vm_state_cache: VmStateCache,
|
||||||
@ -29,11 +40,25 @@ pub struct Worker {
|
|||||||
rx: UnboundedReceiver<WorkJob>,
|
rx: UnboundedReceiver<WorkJob>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct WorkerSettings {
|
||||||
|
pub delete_after: u16,
|
||||||
|
pub smtp: Option<SmtpConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<WorkerSettings> for &Settings {
|
||||||
|
fn into(self) -> WorkerSettings {
|
||||||
|
WorkerSettings {
|
||||||
|
delete_after: self.delete_after,
|
||||||
|
smtp: self.smtp.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Worker {
|
impl Worker {
|
||||||
pub fn new<D: LNVpsDb + Clone + 'static, P: Provisioner + 'static>(
|
pub fn new<D: LNVpsDb + Clone + 'static, P: Provisioner + 'static>(
|
||||||
db: D,
|
db: D,
|
||||||
provisioner: P,
|
provisioner: P,
|
||||||
delete_after: u16,
|
settings: impl Into<WorkerSettings>,
|
||||||
vm_state_cache: VmStateCache,
|
vm_state_cache: VmStateCache,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let (tx, rx) = unbounded_channel();
|
let (tx, rx) = unbounded_channel();
|
||||||
@ -41,7 +66,7 @@ impl Worker {
|
|||||||
db: Box::new(db),
|
db: Box::new(db),
|
||||||
provisioner: Box::new(provisioner),
|
provisioner: Box::new(provisioner),
|
||||||
vm_state_cache,
|
vm_state_cache,
|
||||||
delete_after,
|
settings: settings.into(),
|
||||||
tx,
|
tx,
|
||||||
rx,
|
rx,
|
||||||
}
|
}
|
||||||
@ -77,7 +102,11 @@ impl Worker {
|
|||||||
self.provisioner.stop_vm(db_vm.id).await?;
|
self.provisioner.stop_vm(db_vm.id).await?;
|
||||||
}
|
}
|
||||||
// Delete VM if expired > 3 days
|
// Delete VM if expired > 3 days
|
||||||
if db_vm.expires.add(Days::new(self.delete_after as u64)) < Utc::now() {
|
if db_vm
|
||||||
|
.expires
|
||||||
|
.add(Days::new(self.settings.delete_after as u64))
|
||||||
|
< Utc::now()
|
||||||
|
{
|
||||||
info!("Deleting expired VM {}", db_vm.id);
|
info!("Deleting expired VM {}", db_vm.id);
|
||||||
self.provisioner.delete_vm(db_vm.id).await?;
|
self.provisioner.delete_vm(db_vm.id).await?;
|
||||||
}
|
}
|
||||||
@ -139,6 +168,44 @@ impl Worker {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn send_notification(
|
||||||
|
&self,
|
||||||
|
user_id: u64,
|
||||||
|
message: String,
|
||||||
|
title: Option<String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let user = self.db.get_user(user_id).await?;
|
||||||
|
if let Some(smtp) = self.settings.smtp.as_ref() {
|
||||||
|
if user.contact_email && user.email.is_some() {
|
||||||
|
// send email
|
||||||
|
let mut b = MessageBuilder::new().to(user.email.unwrap().parse()?);
|
||||||
|
if let Some(t) = title {
|
||||||
|
b = b.subject(t);
|
||||||
|
}
|
||||||
|
if let Some(f) = &smtp.from {
|
||||||
|
b = b.from(f.parse()?);
|
||||||
|
}
|
||||||
|
let msg = b.body(message)?;
|
||||||
|
|
||||||
|
let mut sender = AsyncSmtpTransport::<Tokio1Executor>::relay(&smtp.server)?
|
||||||
|
.credentials(Credentials::new(
|
||||||
|
smtp.username.to_string(),
|
||||||
|
smtp.password.to_string(),
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
sender.send(msg).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if user.contact_nip4 {
|
||||||
|
// send DM
|
||||||
|
}
|
||||||
|
if user.contact_nip17 {
|
||||||
|
// send dm
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn handle(&mut self) -> Result<()> {
|
pub async fn handle(&mut self) -> Result<()> {
|
||||||
while let Some(job) = self.rx.recv().await {
|
while let Some(job) = self.rx.recv().await {
|
||||||
match job {
|
match job {
|
||||||
@ -147,7 +214,15 @@ impl Worker {
|
|||||||
error!("Failed to check VM {}: {}", vm_id, e);
|
error!("Failed to check VM {}: {}", vm_id, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WorkJob::SendNotification { .. } => {}
|
WorkJob::SendNotification {
|
||||||
|
user_id,
|
||||||
|
message,
|
||||||
|
title,
|
||||||
|
} => {
|
||||||
|
if let Err(e) = self.send_notification(user_id, message, title).await {
|
||||||
|
error!("Failed to send notification {}: {}", user_id, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
WorkJob::CheckVms => {
|
WorkJob::CheckVms => {
|
||||||
if let Err(e) = self.check_vms().await {
|
if let Err(e) = self.check_vms().await {
|
||||||
error!("Failed to check VMs: {}", e);
|
error!("Failed to check VMs: {}", e);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user