diff --git a/services.conf b/services.conf index 66d7c5f..deee128 100644 --- a/services.conf +++ b/services.conf @@ -12,3 +12,7 @@ tcp,Forgejo (SSH),git.reugenio.com,2222 # Telegram telegram,8158165444:AAFxUjLChsuusgFD5B1gt2svt8NflvAm1M8,1481345275 + +# Email SMTP (Mailbox.org con STARTTLS) +email_smtp,smtp.mailbox.org,587,reugenio@mailbox.org,#Szy(fFaA3DIY_2-iC8lHYQS1R3u(ur2kbfah(1q,reugenio@mailbox.org +email,reugenio@mailbox.org diff --git a/src/smtp.zig b/src/smtp.zig index bca4ba7..f1cc924 100644 --- a/src/smtp.zig +++ b/src/smtp.zig @@ -1,16 +1,16 @@ //! Cliente SMTP para envío de emails //! -//! Implementación del protocolo SMTP (RFC 5321) con soporte para -//! autenticación AUTH LOGIN y STARTTLS. +//! Implementación usando curl para SMTP con STARTTLS. +//! Curl maneja TLS de forma transparente, lo que simplifica mucho el código. //! //! Ejemplo de uso: //! ```zig //! try smtp.sendEmail(allocator, .{ -//! .host = "smtp.gmail.com", +//! .host = "smtp.mailbox.org", //! .port = 587, -//! .username = "usuario@gmail.com", +//! .username = "usuario@mailbox.org", //! .password = "app_password", -//! .from = "usuario@gmail.com", +//! .from = "usuario@mailbox.org", //! .to = &[_][]const u8{"destino@ejemplo.com"}, //! .subject = "Alerta", //! .body = "Servicio caído", @@ -41,6 +41,12 @@ pub const SmtpError = error{ NetworkError, /// Buffer demasiado pequeño. BufferTooSmall, + /// Error iniciando TLS. + TlsInitFailed, + /// El servidor no soporta STARTTLS. + StarttlsNotSupported, + /// Curl no disponible o falló. + CurlFailed, }; /// Opciones para enviar un email. @@ -61,201 +67,140 @@ pub const EmailOptions = struct { subject: []const u8, /// Cuerpo del email (texto plano). body: []const u8, + /// Usar STARTTLS (requerido para puerto 587). + use_starttls: bool = true, }; -/// Envía un email usando SMTP. +/// Envía un email usando curl con SMTP/STARTTLS. /// -/// Soporta autenticación AUTH LOGIN. Para servidores que requieren -/// TLS (como Gmail en puerto 587), se necesita STARTTLS que aún -/// no está implementado - usar puerto 465 (SMTPS) como alternativa. +/// Usa curl para manejar la conexión SMTP con TLS de forma transparente. +/// Funciona con servidores como Gmail (smtp.gmail.com:587) o +/// Mailbox (smtp.mailbox.org:587). pub fn sendEmail(allocator: std.mem.Allocator, options: EmailOptions) SmtpError!void { - // Conectar al servidor - const stream = std.net.tcpConnectToHost(allocator, options.host, options.port) catch { - return SmtpError.ConnectionFailed; - }; - defer stream.close(); - - var read_buffer: [1024]u8 = undefined; - - // Recibir saludo (220) - try expectResponse(stream, &read_buffer, "220"); - - // EHLO - try sendCommand(stream, "EHLO localhost\r\n"); - try expectResponse(stream, &read_buffer, "250"); - - // Autenticación si hay credenciales - if (options.username.len > 0 and options.password.len > 0) { - try sendCommand(stream, "AUTH LOGIN\r\n"); - try expectResponse(stream, &read_buffer, "334"); - - // Usuario en Base64 - var user_b64: [256]u8 = undefined; - const user_encoded = base64Encode(options.username, &user_b64) catch { - return SmtpError.BufferTooSmall; - }; - try sendLine(stream, user_encoded); - try expectResponse(stream, &read_buffer, "334"); - - // Password en Base64 - var pass_b64: [256]u8 = undefined; - const pass_encoded = base64Encode(options.password, &pass_b64) catch { - return SmtpError.BufferTooSmall; - }; - try sendLine(stream, pass_encoded); - - const auth_response = try readResponse(stream, &read_buffer); - if (!std.mem.startsWith(u8, auth_response, "235")) { - return SmtpError.AuthenticationFailed; - } - } - - // MAIL FROM - var from_cmd: [512]u8 = undefined; - const from_len = std.fmt.bufPrint(&from_cmd, "MAIL FROM:<{s}>\r\n", .{options.from}) catch { + // Construir URL SMTP + var url_buf: [256]u8 = undefined; + const url = std.fmt.bufPrint(&url_buf, "smtp://{s}:{d}", .{ options.host, options.port }) catch { return SmtpError.BufferTooSmall; }; - try sendCommand(stream, from_len); - const from_response = try readResponse(stream, &read_buffer); - if (!std.mem.startsWith(u8, from_response, "250")) { - return SmtpError.SenderRejected; - } + // Construir credenciales user:pass + var creds_buf: [256]u8 = undefined; + const creds = std.fmt.bufPrint(&creds_buf, "{s}:{s}", .{ options.username, options.password }) catch { + return SmtpError.BufferTooSmall; + }; - // RCPT TO (para cada destinatario) - for (options.to) |recipient| { - var rcpt_cmd: [512]u8 = undefined; - const rcpt_len = std.fmt.bufPrint(&rcpt_cmd, "RCPT TO:<{s}>\r\n", .{recipient}) catch { - return SmtpError.BufferTooSmall; - }; - try sendCommand(stream, rcpt_len); + // Construir mail-from + var from_buf: [256]u8 = undefined; + const mail_from = std.fmt.bufPrint(&from_buf, "<{s}>", .{options.from}) catch { + return SmtpError.BufferTooSmall; + }; - const rcpt_response = try readResponse(stream, &read_buffer); - if (!std.mem.startsWith(u8, rcpt_response, "250")) { - return SmtpError.RecipientRejected; - } - } + // Construir mail-rcpt (primer destinatario) + var rcpt_buf: [256]u8 = undefined; + const mail_rcpt = std.fmt.bufPrint(&rcpt_buf, "<{s}>", .{options.to[0]}) catch { + return SmtpError.BufferTooSmall; + }; - // DATA - try sendCommand(stream, "DATA\r\n"); - try expectResponse(stream, &read_buffer, "354"); - - // Headers + Body - var msg_buffer: [4096]u8 = undefined; - const msg = std.fmt.bufPrint(&msg_buffer, + // Construir mensaje completo + var msg_buf: [4096]u8 = undefined; + const message = std.fmt.bufPrint(&msg_buf, \\From: {s} \\To: {s} \\Subject: {s} \\Content-Type: text/plain; charset=UTF-8 \\ \\{s} - \\. - \\ , .{ options.from, - options.to[0], // Primer destinatario en header + options.to[0], options.subject, options.body, }) catch { return SmtpError.BufferTooSmall; }; - // Convertir \n a \r\n para SMTP - try sendSmtpData(stream, msg); + // Construir argumentos de curl + var args = std.array_list.Managed([]const u8).init(allocator); + defer args.deinit(); - const data_response = try readResponse(stream, &read_buffer); - if (!std.mem.startsWith(u8, data_response, "250")) { - return SmtpError.MessageRejected; + args.append("curl") catch return SmtpError.CurlFailed; + args.append("-s") catch return SmtpError.CurlFailed; // Silent + args.append("--url") catch return SmtpError.CurlFailed; + args.append(url) catch return SmtpError.CurlFailed; + + // STARTTLS + if (options.use_starttls) { + args.append("--ssl-reqd") catch return SmtpError.CurlFailed; } - // QUIT - try sendCommand(stream, "QUIT\r\n"); - // No esperamos respuesta de QUIT, solo cerramos -} - -/// Lee una respuesta del servidor SMTP. -fn readResponse(stream: std.net.Stream, buffer: []u8) SmtpError![]const u8 { - const bytes_read = stream.read(buffer) catch { - return SmtpError.NetworkError; - }; - - if (bytes_read == 0) { - return SmtpError.ConnectionClosed; + // Autenticación + if (options.username.len > 0) { + args.append("-u") catch return SmtpError.CurlFailed; + args.append(creds) catch return SmtpError.CurlFailed; } - return buffer[0..bytes_read]; -} + // Remitente y destinatario + args.append("--mail-from") catch return SmtpError.CurlFailed; + args.append(mail_from) catch return SmtpError.CurlFailed; + args.append("--mail-rcpt") catch return SmtpError.CurlFailed; + args.append(mail_rcpt) catch return SmtpError.CurlFailed; -/// Espera una respuesta que empiece con el código dado. -fn expectResponse(stream: std.net.Stream, buffer: []u8, expected_code: []const u8) SmtpError!void { - const response = try readResponse(stream, buffer); + // Mensaje via stdin + args.append("-T") catch return SmtpError.CurlFailed; + args.append("-") catch return SmtpError.CurlFailed; // Read from stdin - if (!std.mem.startsWith(u8, response, expected_code)) { - return SmtpError.UnexpectedResponse; - } -} + // Ejecutar curl + var child = std.process.Child.init(args.items, allocator); + child.stdin_behavior = .Pipe; + child.stdout_behavior = .Pipe; + child.stderr_behavior = .Pipe; -/// Envía un comando al servidor. -fn sendCommand(stream: std.net.Stream, command: []const u8) SmtpError!void { - _ = stream.write(command) catch { - return SmtpError.NetworkError; - }; -} + child.spawn() catch return SmtpError.CurlFailed; -/// Envía una línea con CRLF al final. -fn sendLine(stream: std.net.Stream, line: []const u8) SmtpError!void { - _ = stream.write(line) catch { - return SmtpError.NetworkError; - }; - _ = stream.write("\r\n") catch { - return SmtpError.NetworkError; - }; -} - -/// Envía datos SMTP convirtiendo \n a \r\n. -fn sendSmtpData(stream: std.net.Stream, data: []const u8) SmtpError!void { - var i: usize = 0; - while (i < data.len) { - const start = i; - // Buscar siguiente \n - while (i < data.len and data[i] != '\n') : (i += 1) {} - - // Enviar hasta aquí - if (i > start) { - _ = stream.write(data[start..i]) catch { - return SmtpError.NetworkError; - }; - } - - // Si encontramos \n, enviar \r\n - if (i < data.len and data[i] == '\n') { - _ = stream.write("\r\n") catch { - return SmtpError.NetworkError; - }; - i += 1; - } - } -} - -/// Codifica datos en Base64. -fn base64Encode(input: []const u8, output: []u8) ![]const u8 { - const encoder = std.base64.standard.Encoder; - const encoded_len = encoder.calcSize(input.len); - - if (encoded_len > output.len) { - return error.BufferTooSmall; + // Enviar mensaje por stdin + if (child.stdin) |stdin| { + stdin.writeAll(message) catch return SmtpError.CurlFailed; + stdin.close(); + child.stdin = null; } - const result = encoder.encode(output, input); - return result; + // Esperar a que termine + const result = child.wait() catch return SmtpError.CurlFailed; + + // Limpiar stdout/stderr + if (child.stdout) |stdout| { + stdout.close(); + } + if (child.stderr) |stderr| { + stderr.close(); + } + + // Verificar resultado + if (result.Exited != 0) { + // Códigos de error comunes de curl para SMTP: + // 67 = login denied (AuthenticationFailed) + // 7 = couldn't connect (ConnectionFailed) + // 56 = recv error (NetworkError) + return switch (result.Exited) { + 67 => SmtpError.AuthenticationFailed, + 7 => SmtpError.ConnectionFailed, + else => SmtpError.CurlFailed, + }; + } } // ============================================================================ // Tests // ============================================================================ -test "base64 encode" { - var buffer: [64]u8 = undefined; - const result = try base64Encode("test", &buffer); - try std.testing.expectEqualStrings("dGVzdA==", result); +test "smtp module compiles" { + // Test básico para verificar que el módulo compila + const opts = EmailOptions{ + .host = "smtp.example.com", + .from = "test@example.com", + .to = &[_][]const u8{"dest@example.com"}, + .subject = "Test", + .body = "Test body", + }; + _ = opts; }