From fbb93b12231a62a6fe28a8369cd7eb26111d8ae2 Mon Sep 17 00:00:00 2001 From: fierylion Date: Mon, 18 Aug 2025 12:22:47 +0300 Subject: [PATCH 1/3] added: smtp configuration to enable smtp clients to connect --- .env | 12 ++ Dockerfile | 21 +-- docker-compose.yml | 3 + src/main.rs | 387 +++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 405 insertions(+), 18 deletions(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 0000000..874e6cd --- /dev/null +++ b/.env @@ -0,0 +1,12 @@ +# Enable both services +ENABLE_SMTP_GATEWAY=true +PORT=4500 +SMTP_HOST=0.0.0.0 +SMTP_PORT=1025 +RUST_LOG=info + +# SMTP backend configuration +TARGET_SMTP_SERVER=mail.sanalogistic.com +TARGET_SMTP_USERNAME=noreply@sanalogistic.com +TARGET_SMTP_PASSWORD=Sky@2007!! +TARGET_SMTP_FROM_NAME=Skyconnect Inc \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 446f3d9..54692c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,8 +19,8 @@ RUN mkdir -p src && \ # Build dependencies only - this is the caching Docker layer RUN cargo build --release -# Remove the dummy source -RUN rm ./target/release/deps/mail_sender* && \ +# Remove the dummy source (updated binary name) +RUN rm ./target/release/deps/email_service* && \ rm src/main.rs # Now copy the real source code @@ -28,7 +28,7 @@ COPY src src/ # Build the real application RUN cargo build --release && \ - strip /app/target/release/mail-sender + strip /app/target/release/email-service # Runtime stage using distroless FROM gcr.io/distroless/cc-debian12 @@ -38,14 +38,17 @@ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ WORKDIR /app -# Copy binary from builder -COPY --from=builder /app/target/release/mail-sender . +# Copy binary from builder (updated binary name) +COPY --from=builder /app/target/release/email-service . -# Set environment variables +# Set default environment variables ENV RUST_LOG=info ENV PORT=4500 +ENV SMTP_PORT=1025 +ENV SMTP_HOST=0.0.0.0 +ENV ENABLE_SMTP_GATEWAY=false -# Expose port - match the port specified in the ENV -EXPOSE 4500 +# Expose both HTTP API and SMTP gateway ports +EXPOSE 4500 1025 -ENTRYPOINT ["./mail-sender"] \ No newline at end of file +ENTRYPOINT ["./email-service"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 41b22be..e456ede 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,9 @@ services: restart: always networks: - traefik_network + ports: + - "4500:4500" + - "1025:1025" labels: - "traefik.docker.network=traefik_network" - "traefik.enable=true" diff --git a/src/main.rs b/src/main.rs index b53d938..53707c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,8 +6,13 @@ use lettre::{ AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor, }; use serde::{Deserialize, Serialize}; -use std::{net::SocketAddr, str::FromStr}; -use tracing::info; +use std::{ + io::{BufRead, BufReader, Write}, + net::{SocketAddr, TcpListener, TcpStream}, + str::FromStr, + thread, +}; +use tracing::{error, info, warn}; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; @@ -23,10 +28,45 @@ async fn main() { .with(tracing_subscriber::fmt::layer()) .init(); - // Build our application with a single route + // Check if SMTP gateway mode is enabled + let enable_smtp_gateway = std::env::var("ENABLE_SMTP_GATEWAY") + .unwrap_or_else(|_| "false".to_string()) + .parse::() + .unwrap_or(false); + + if enable_smtp_gateway { + // Start both HTTP API and SMTP server + let http_handle = tokio::spawn(start_http_server()); + let smtp_handle = tokio::task::spawn_blocking(start_smtp_server); + + tokio::select! { + result = http_handle => { + if let Err(e) = result { + error!("HTTP server failed: {}", e); + } + } + result = smtp_handle => { + if let Err(e) = result { + error!("SMTP server failed: {:?}", e); + } + } + } + } else { + // Start only HTTP server (your original service) + start_http_server().await; + } +} + +// ============================================================================= +// HTTP SERVER (Your original email service) +// ============================================================================= + +async fn start_http_server() { + // Build our application with your original route plus management let app = Router::new() .route("/send-emails", post(send_emails_handler)) - .route("/health", get(|| async { (StatusCode::OK, "OK") })); + .route("/health", get(|| async { (StatusCode::OK, "OK") })) + .route("/smtp-status", get(smtp_gateway_status)); // Get the port to listen on let port = std::env::var("PORT") @@ -40,7 +80,12 @@ async fn main() { .await .expect("Failed to bind address"); - info!("Listening on {}", addr.clone()); + info!("📡 HTTP API listening on {}", addr); + if std::env::var("ENABLE_SMTP_GATEWAY").unwrap_or_else(|_| "false".to_string()) == "true" { + let smtp_port = std::env::var("SMTP_PORT").unwrap_or_else(|_| "1025".to_string()); + info!("📧 SMTP Gateway also available on port {}", smtp_port); + info!("🔧 Configure blocked servers to use: smtp://THIS_SERVER:1025"); + } axum::serve( listener, @@ -50,6 +95,297 @@ async fn main() { .expect("Failed to run server"); } +// ============================================================================= +// SMTP GATEWAY SERVER +// ============================================================================= + +fn start_smtp_server() -> std::io::Result<()> { + let smtp_host = std::env::var("SMTP_HOST").unwrap_or_else(|_| "0.0.0.0".to_string()); + let smtp_port = std::env::var("SMTP_PORT") + .unwrap_or_else(|_| "1025".to_string()) + .parse::() + .expect("Invalid SMTP port"); + + let listener = TcpListener::bind(format!("{}:{}", smtp_host, smtp_port))?; + + info!("🚀 SMTP Gateway listening on {}:{}", smtp_host, smtp_port); + info!("📮 Blocked servers can connect to this server as SMTP host"); + + for stream in listener.incoming() { + match stream { + Ok(stream) => { + thread::spawn(move || { + if let Err(e) = handle_smtp_client(stream) { + error!("SMTP client error: {}", e); + } + }); + } + Err(e) => { + error!("Failed to accept SMTP connection: {}", e); + } + } + } + + Ok(()) +} + +// ============================================================================= +// SMTP PROTOCOL HANDLER +// ============================================================================= + +#[derive(Debug, Default)] +struct SmtpSession { + from: Option, + to: Vec, + data: String, + state: SmtpState, +} + +#[derive(Debug, Default)] +enum SmtpState { + #[default] + Connected, + Greeted, + Mail, + Rcpt, + Data, +} + +fn handle_smtp_client(mut stream: TcpStream) -> std::io::Result<()> { + let peer_addr = stream.peer_addr()?; + info!("📥 New SMTP connection from {}", peer_addr); + + let mut reader = BufReader::new(stream.try_clone()?); + let mut session = SmtpSession::default(); + + // Send SMTP greeting + write!(stream, "220 Email Service SMTP Gateway Ready\r\n")?; + stream.flush()?; + + loop { + let mut line = String::new(); + match reader.read_line(&mut line) { + Ok(0) => break, // Connection closed + Ok(_) => { + let command = line.trim(); + if !command.is_empty() { + info!("📨 SMTP[{}]: {}", peer_addr, command); + + match handle_smtp_command(&mut stream, command, &mut session) { + Ok(should_continue) => { + if !should_continue { + break; + } + } + Err(e) => { + error!("SMTP command error: {}", e); + write!(stream, "451 Internal server error\r\n")?; + break; + } + } + } + } + Err(e) => { + error!("Failed to read SMTP command: {}", e); + break; + } + } + } + + info!("📪 SMTP connection from {} closed", peer_addr); + Ok(()) +} + +fn handle_smtp_command( + stream: &mut TcpStream, + command: &str, + session: &mut SmtpSession, +) -> std::io::Result { + let parts: Vec<&str> = command.split_whitespace().collect(); + if parts.is_empty() { + write!(stream, "500 Command unrecognized\r\n")?; + stream.flush()?; + return Ok(true); + } + + let cmd = parts[0].to_uppercase(); + + match cmd.as_str() { + "HELO" | "EHLO" => { + let hostname = parts.get(1).unwrap_or(&"localhost"); + write!(stream, "250-Hello {}\r\n", hostname)?; + write!(stream, "250-8BITMIME\r\n")?; + write!(stream, "250 HELP\r\n")?; + session.state = SmtpState::Greeted; + } + "MAIL" => { + if let Some(from) = parse_mail_from(command) { + session.from = Some(from); + write!(stream, "250 OK\r\n")?; + session.state = SmtpState::Mail; + } else { + write!(stream, "501 Syntax error in MAIL command\r\n")?; + } + } + "RCPT" => { + if let Some(to) = parse_rcpt_to(command) { + session.to.push(to); + write!(stream, "250 OK\r\n")?; + session.state = SmtpState::Rcpt; + } else { + write!(stream, "501 Syntax error in RCPT command\r\n")?; + } + } + "DATA" => { + write!(stream, "354 Start mail input; end with .\r\n")?; + stream.flush()?; + + // Read email data + let mut reader = BufReader::new(stream.try_clone()?); + let mut data = String::new(); + + loop { + let mut line = String::new(); + reader.read_line(&mut line)?; + if line.trim() == "." { + break; + } + data.push_str(&line); + } + + session.data = data; + + // Send email using your existing functions + match send_smtp_email(session) { + Ok(_) => { + write!(stream, "250 OK: Message accepted for delivery\r\n")?; + info!("✅ Email successfully sent via SMTP gateway"); + } + Err(e) => { + write!(stream, "451 Temporary failure: {}\r\n", e)?; + error!("❌ Failed to send email via SMTP gateway: {}", e); + } + } + + // Reset session + session.from = None; + session.to.clear(); + session.data.clear(); + session.state = SmtpState::Greeted; + } + "RSET" => { + session.from = None; + session.to.clear(); + session.data.clear(); + session.state = SmtpState::Greeted; + write!(stream, "250 OK\r\n")?; + } + "QUIT" => { + write!(stream, "221 Bye\r\n")?; + stream.flush()?; + return Ok(false); + } + "NOOP" => { + write!(stream, "250 OK\r\n")?; + } + _ => { + write!(stream, "502 Command not implemented\r\n")?; + } + } + + stream.flush()?; + Ok(true) +} + +fn parse_mail_from(command: &str) -> Option { + if let Some(start) = command.find('<') { + if let Some(end) = command.find('>') { + if end > start { + return Some(command[start + 1..end].to_string()); + } + } + } + None +} + +fn parse_rcpt_to(command: &str) -> Option { + if let Some(start) = command.find('<') { + if let Some(end) = command.find('>') { + if end > start { + return Some(command[start + 1..end].to_string()); + } + } + } + None +} + +fn send_smtp_email(session: &SmtpSession) -> Result<(), String> { + // Get SMTP configuration from environment + let smtp_config = SmtpConfig { + server: std::env::var("TARGET_SMTP_SERVER") + .map_err(|_| "TARGET_SMTP_SERVER not configured")?, + username: std::env::var("TARGET_SMTP_USERNAME") + .map_err(|_| "TARGET_SMTP_USERNAME not configured")?, + password: std::env::var("TARGET_SMTP_PASSWORD") + .map_err(|_| "TARGET_SMTP_PASSWORD not configured")?, + from_email: session.from.as_ref() + .ok_or("No sender specified")? + .clone(), + from_name: std::env::var("TARGET_SMTP_FROM_NAME").ok(), + }; + + // Parse email content + let (subject, body) = parse_email_data(&session.data); + + // Create runtime for async operations + let rt = tokio::runtime::Runtime::new().map_err(|e| format!("Runtime error: {}", e))?; + + rt.block_on(async { + // Create SMTP transport using your existing function + let smtp_transport = create_smtp_transport(&smtp_config).await?; + + // Send to each recipient using your existing function + for to_email in &session.to { + let email_message = EmailMessage { + to_email: to_email.clone(), + to_name: None, + subject: subject.clone(), + text_body: body.clone(), + html_body: None, + }; + + send_email(&smtp_transport, &smtp_config, &email_message).await?; + info!("📧 Email sent to {} via {}", to_email, smtp_config.server); + } + + Ok::<(), String>(()) + }) +} + +fn parse_email_data(data: &str) -> (String, String) { + let lines: Vec<&str> = data.lines().collect(); + let mut subject = "No Subject".to_string(); + let mut body_start = 0; + + // Parse headers + for (i, line) in lines.iter().enumerate() { + if line.is_empty() { + body_start = i + 1; + break; + } + if line.to_lowercase().starts_with("subject:") { + subject = line[8..].trim().to_string(); + } + } + + let body = lines[body_start..].join("\n"); + (subject, body) +} + +// ============================================================================= +// YOUR ORIGINAL EMAIL SERVICE CODE +// ============================================================================= + // Request model #[derive(Debug, Deserialize)] struct EmailRequest { @@ -57,7 +393,7 @@ struct EmailRequest { emails: Vec, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] struct SmtpConfig { server: String, username: String, @@ -84,8 +420,11 @@ struct EmailResponse { errors: Vec, } -// Handler function +// Handler function (your original code) async fn send_emails_handler(Json(payload): Json) -> impl IntoResponse { + info!("📨 HTTP API received {} emails to send via {}", + payload.emails.len(), payload.smtp_config.server); + // Create SMTP transport let smtp_transport = match create_smtp_transport(&payload.smtp_config).await { Ok(transport) => transport, @@ -138,10 +477,11 @@ async fn send_emails_handler(Json(payload): Json) -> impl IntoResp errors, }; + info!("📊 HTTP API completed: {} sent, {} failed", sent_count, failed_count); (status, Json(response)) } -// Create SMTP transport +// Create SMTP transport (your original function) async fn create_smtp_transport( config: &SmtpConfig, ) -> Result, String> { @@ -155,7 +495,7 @@ async fn create_smtp_transport( Ok(mailer) } -// Send a single email +// Send a single email (your original function) async fn send_email( transport: &AsyncSmtpTransport, smtp_config: &SmtpConfig, @@ -209,3 +549,32 @@ async fn send_email( Ok(()) } + +// ============================================================================= +// MANAGEMENT ENDPOINTS +// ============================================================================= + +async fn smtp_gateway_status() -> impl IntoResponse { + let smtp_enabled = std::env::var("ENABLE_SMTP_GATEWAY") + .unwrap_or_else(|_| "false".to_string()) == "true"; + + let status = serde_json::json!({ + "http_api": { + "enabled": true, + "port": std::env::var("PORT").unwrap_or_else(|_| "4500".to_string()), + "endpoint": "/send-emails" + }, + "smtp_gateway": { + "enabled": smtp_enabled, + "port": std::env::var("SMTP_PORT").unwrap_or_else(|_| "1025".to_string()), + "target_smtp": std::env::var("TARGET_SMTP_SERVER").unwrap_or_else(|_| "Not configured".to_string()) + }, + "message": if smtp_enabled { + "Both HTTP API and SMTP Gateway are running" + } else { + "Only HTTP API is running. Set ENABLE_SMTP_GATEWAY=true to enable SMTP gateway" + } + }); + + (StatusCode::OK, Json(status)) +} \ No newline at end of file From 742c21188c7f1ac78d75adea9c5ab422a8ccefe2 Mon Sep 17 00:00:00 2001 From: fierylion Date: Mon, 18 Aug 2025 12:23:08 +0300 Subject: [PATCH 2/3] added: smtp configuration to enable smtp clients to connect --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 42e3310..79686b7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,7 @@ on: push: branches: - "main" + - "feature" - "feat/*" - "v*" tags: From bc8bf78c2a071a89202b17f7e138be1b55a7f4c5 Mon Sep 17 00:00:00 2001 From: fierylion Date: Mon, 18 Aug 2025 12:31:14 +0300 Subject: [PATCH 3/3] changes --- Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 54692c5..f17a874 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ RUN mkdir -p src && \ RUN cargo build --release # Remove the dummy source (updated binary name) -RUN rm ./target/release/deps/email_service* && \ +RUN rm ./target/release/deps/mail_sender* && \ rm src/main.rs # Now copy the real source code @@ -28,7 +28,7 @@ COPY src src/ # Build the real application RUN cargo build --release && \ - strip /app/target/release/email-service + strip /app/target/release/mail-sender # Runtime stage using distroless FROM gcr.io/distroless/cc-debian12 @@ -39,7 +39,7 @@ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ WORKDIR /app # Copy binary from builder (updated binary name) -COPY --from=builder /app/target/release/email-service . +COPY --from=builder /app/target/release/mail-sender . # Set default environment variables ENV RUST_LOG=info @@ -51,4 +51,4 @@ ENV ENABLE_SMTP_GATEWAY=false # Expose both HTTP API and SMTP gateway ports EXPOSE 4500 1025 -ENTRYPOINT ["./email-service"] \ No newline at end of file +ENTRYPOINT ["./mail-sender"] \ No newline at end of file