Git hosting and a container registry in one Rust binary (axum + Astro)
Mail: STARTTLS on 587, irongit admin test-email
2 files changed, +27 -6
+13-6backend/src/mail.rs
| @@ -6,10 +6,14 @@ use lettre::{ | ||
| 6 | 6 | transport::smtp::authentication::Credentials, |
| 7 | 7 | }; |
| 8 | 8 | |
| 9 | -use crate::state::AppState; | |
| 9 | +use crate::{config::Config, state::AppState}; | |
| 10 | 10 | |
| 11 | 11 | pub async fn send(state: &AppState, to: &str, subject: &str, body: &str) -> anyhow::Result<()> { |
| 12 | - let Some(smtp) = &state.config.smtp else { | |
| 12 | + send_with(&state.config, to, subject, body).await | |
| 13 | +} | |
| 14 | + | |
| 15 | +pub async fn send_with(config: &Config, to: &str, subject: &str, body: &str) -> anyhow::Result<()> { | |
| 16 | + let Some(smtp) = &config.smtp else { | |
| 13 | 17 | tracing::info!(target: "mail", to, subject, body, "SMTP not configured; email logged instead of sent"); |
| 14 | 18 | return Ok(()); |
| 15 | 19 | }; |
| @@ -19,10 +23,13 @@ pub async fn send(state: &AppState, to: &str, subject: &str, body: &str) -> anyh | ||
| 19 | 23 | .subject(subject) |
| 20 | 24 | .header(ContentType::TEXT_PLAIN) |
| 21 | 25 | .body(body.to_string())?; |
| 22 | - let transport = AsyncSmtpTransport::<Tokio1Executor>::relay(&smtp.host)? | |
| 23 | - .port(smtp.port) | |
| 24 | - .credentials(Credentials::new(smtp.username.clone(), smtp.password.clone())) | |
| 25 | - .build(); | |
| 26 | + // 465 is TLS from the first byte; 587 (and anything else) upgrades with STARTTLS. | |
| 27 | + let builder = if smtp.port == 465 { | |
| 28 | + AsyncSmtpTransport::<Tokio1Executor>::relay(&smtp.host)? | |
| 29 | + } else { | |
| 30 | + AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&smtp.host)? | |
| 31 | + }; | |
| 32 | + let transport = builder.port(smtp.port).credentials(Credentials::new(smtp.username.clone(), smtp.password.clone())).build(); | |
| 26 | 33 | transport.send(message).await?; |
| 27 | 34 | tracing::info!(to, subject, "email sent"); |
| 28 | 35 | Ok(()) |
+14-0backend/src/main.rs
| @@ -78,6 +78,8 @@ enum AdminCommand { | ||
| 78 | 78 | Demote { username: String }, |
| 79 | 79 | /// Write, read and delete a test object in R2 to verify credentials. |
| 80 | 80 | CheckStorage, |
| 81 | + /// Send a test email through the configured SMTP server. | |
| 82 | + TestEmail { to: String }, | |
| 81 | 83 | /// Publish a CLI build to R2 so `ig upgrade` and install.sh serve it. |
| 82 | 84 | PublishCli { |
| 83 | 85 | /// Path to the built `ig` binary. |
| @@ -175,6 +177,18 @@ async fn admin(command: AdminCommand) -> anyhow::Result<()> { | ||
| 175 | 177 | AdminCommand::Promote { username } => set_admin(&db, &username, true).await, |
| 176 | 178 | AdminCommand::Demote { username } => set_admin(&db, &username, false).await, |
| 177 | 179 | AdminCommand::CheckStorage => check_storage(&config).await, |
| 180 | + AdminCommand::TestEmail { to } => { | |
| 181 | + anyhow::ensure!(config.smtp.is_some(), "SMTP is not configured (set SMTP_HOST, SMTP_USERNAME and SMTP_PASSWORD)"); | |
| 182 | + mail::send_with( | |
| 183 | + &config, | |
| 184 | + &to, | |
| 185 | + "irongit test email", | |
| 186 | + &format!("This is a test from {}. If you can read it, verification emails will arrive too.", config.base_url()), | |
| 187 | + ) | |
| 188 | + .await?; | |
| 189 | + println!("sent a test email to {to}"); | |
| 190 | + Ok(()) | |
| 191 | + } | |
| 178 | 192 | AdminCommand::PublishCli { path, version, target } => { |
| 179 | 193 | let storage = Storage::new(&config.r2)?; |
| 180 | 194 | api::release::publish(&db, &storage, &path, &version, &target).await |