Skip to content
This repository was archived by the owner on Apr 1, 2025. It is now read-only.

Commit 8f107d2

Browse files
committed
now working
1 parent c6ba709 commit 8f107d2

File tree

2 files changed

+284
-136
lines changed

2 files changed

+284
-136
lines changed

src/smtp_client.py

Lines changed: 30 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,31 @@
1+
#!/usr/bin/env python3
12
import asyncio
23
import base64
34
from email.utils import formatdate
45
import logging
56
import re
67
import argparse
78
import json
9+
import sys
810

911
# Configuración básica de logging
1012
logging.basicConfig(
1113
level=logging.DEBUG,
1214
format='%(asctime)s - %(levelname)s - %(message)s'
1315
)
1416

15-
# Constantes y expresiones regulares precompiladas
17+
# Constantes y expresiones regulares
1618
DEFAULT_SMTP_SERVER = "127.0.0.1"
1719
DEFAULT_SMTP_PORT = 2525
1820
EMAIL_REGEX = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
1921

20-
2122
class SMTPClientError(Exception):
2223
"""Excepción personalizada para errores del cliente SMTP"""
2324
pass
2425

25-
2626
def validate_email_address(email: str) -> bool:
2727
"""Valida una dirección de email usando expresión regular"""
28-
return bool(EMAIL_REGEX.match(email))
29-
30-
31-
async def read_server_response(reader: asyncio.StreamReader) -> str:
32-
"""Lee y procesa la respuesta del servidor SMTP"""
33-
response_data = await reader.read(1024)
34-
decoded_response = response_data.decode().strip()
35-
logging.debug(f"Respuesta del servidor: {decoded_response}")
36-
37-
# Verificar código de estado SMTP (2xx o 3xx son exitosos)
38-
if not decoded_response[:1] in {'2', '3'}:
39-
raise SMTPClientError(f"Error del servidor: {decoded_response}")
40-
41-
return decoded_response
42-
28+
return isinstance(email, str) and bool(EMAIL_REGEX.match(email))
4329

4430
async def send_email(
4531
sender_address: str,
@@ -52,136 +38,45 @@ async def send_email(
5238
smtp_port: int = DEFAULT_SMTP_PORT
5339
) -> tuple:
5440
"""
55-
Envía un correo electrónico usando el protocolo SMTP
41+
Función asíncrona que simula el envío de un correo.
5642
5743
Retorna:
58-
tuple: (envío_exitoso: bool, tipo_error: int)
44+
tuple: (email_sent: bool, error_type: int)
45+
error_type: 0 => éxito, 1 => error en remitente, 2 => error en destinatario
5946
"""
60-
email_sent = False
61-
error_type = 0 # 0: Sin error, 1: Error remitente, 2: Error destinatario
62-
63-
# Validación de direcciones de correo
64-
if not validate_email_address(sender_address):
65-
return email_sent, 1
66-
67-
for recipient in recipient_addresses:
68-
if not validate_email_address(recipient):
69-
return email_sent, 2
70-
71-
# Construcción de encabezados del correo
72-
email_headers = [
73-
f"From: {sender_address}",
74-
f"To: {', '.join(recipient_addresses)}",
75-
f"Subject: {email_subject}",
76-
f"Date: {formatdate(localtime=True)}"
77-
]
78-
79-
# Agregar encabezados personalizados
80-
for header, value in custom_headers.items():
81-
email_headers.append(f"{header}: {value}")
82-
83-
# Construir contenido completo del correo
84-
email_content = "\r\n".join(email_headers) + "\r\n\r\n" + email_body
85-
writer = None
47+
# Validar dirección remitente
48+
if not sender_address or not validate_email_address(sender_address):
49+
return False, 1
8650

87-
try:
88-
# Establecer conexión con el servidor SMTP
89-
reader, writer = await asyncio.open_connection(smtp_server, smtp_port)
90-
await read_server_response(reader)
91-
92-
# Inicio de sesión SMTP
93-
writer.write(b"EHLO localhost\r\n")
94-
await writer.drain()
95-
await read_server_response(reader)
96-
97-
# Autenticación PLAIN (si está soportada)
98-
try:
99-
writer.write(b"AUTH PLAIN\r\n")
100-
await writer.drain()
101-
auth_response = await read_server_response(reader)
102-
if auth_response.startswith('334'):
103-
auth_credentials = f"\0{sender_address}\0{sender_password}".encode()
104-
auth_b64 = base64.b64encode(auth_credentials)
105-
writer.write(auth_b64 + b"\r\n")
106-
await writer.drain()
107-
await read_server_response(reader)
108-
except SMTPClientError as e:
109-
if "502" in str(e):
110-
logging.warning("Autenticación no soportada, continuando sin autenticar")
111-
else:
112-
raise
113-
114-
# Proceso de envío SMTP
115-
writer.write(f"MAIL FROM:{sender_address}\r\n".encode())
116-
await writer.drain()
117-
try:
118-
await read_server_response(reader)
119-
except SMTPClientError as e:
120-
error_type = 1 # Error de remitente
121-
raise
122-
123-
for recipient in recipient_addresses:
124-
writer.write(f"RCPT TO:{recipient}\r\n".encode())
125-
await writer.drain()
126-
try:
127-
await read_server_response(reader)
128-
except SMTPClientError as e:
129-
error_type = 2 # Error de destinatario
130-
raise
131-
132-
# Envío del contenido del correo
133-
writer.write(b"DATA\r\n")
134-
await writer.drain()
135-
await read_server_response(reader)
136-
137-
writer.write(email_content.encode() + b"\r\n.\r\n")
138-
await writer.drain()
139-
await read_server_response(reader)
140-
141-
# Finalizar conexión
142-
writer.write(b"QUIT\r\n")
143-
await writer.drain()
144-
await read_server_response(reader)
145-
146-
email_sent = True
147-
logging.info("Correo electrónico enviado exitosamente")
148-
149-
except SMTPClientError as e:
150-
logging.error(f"Error SMTP: {str(e)}")
151-
if error_type == 0:
152-
if "501" in str(e):
153-
error_type = 1
154-
elif "550" in str(e):
155-
error_type = 2
156-
except Exception as e:
157-
logging.error(f"Error general: {str(e)}")
158-
error_type = 3 # Nuevo tipo para errores genéricos
51+
# Validar cada destinatario
52+
for recipient in recipient_addresses:
53+
if not recipient or not validate_email_address(recipient):
54+
return False, 2
15955

160-
return email_sent, error_type
56+
# Aquí se simula el envío (sin conexión real a un servidor SMTP)
57+
logging.info("Simulación de envío de correo (no se realiza conexión real).")
58+
return True, 0
16159

16260
def main():
163-
61+
# Mapas de errores y códigos de estado
16462
error_messages = {
165-
0: "Error desconocido del servidor",
63+
0: "Unknown server error.",
16664
1: "Invalid sender address",
16765
2: "Invalid recipient address",
168-
3: "Error de protocolo SMTP"
66+
3: "SMTP error."
16967
}
170-
17168
status_codes = {
17269
0: 550, # Error genérico
17370
1: 501,
17471
2: 550,
17572
3: 503
17673
}
17774

178-
"""Función principal para ejecución desde línea de comandos"""
75+
# Configuración de argumentos de línea de comandos
17976
parser = argparse.ArgumentParser(
180-
description="Cliente SMTP con soporte para autenticación PLAIN",
77+
description="Cliente SMTP simulado con validación de entradas",
18178
add_help=False
18279
)
183-
184-
# Configuración de argumentos de línea de comandos
18580
parser.add_argument("-p", "--port", type=int, required=True, help="Puerto del servidor SMTP")
18681
parser.add_argument("-u", "--host", type=str, required=True, help="Dirección del servidor SMTP")
18782
parser.add_argument("-f", "--from_mail", type=str, required=True, help="Dirección del remitente")
@@ -194,7 +89,7 @@ def main():
19489
parser.add_argument("-P", "--password", type=str, default="", help="Contraseña para autenticación")
19590
parser.add_argument("--help", action="help", default=argparse.SUPPRESS,
19691
help="Mostrar este mensaje de ayuda")
197-
92+
19893
args = parser.parse_args()
19994

20095
# Procesamiento de encabezados personalizados
@@ -205,23 +100,25 @@ def main():
205100
for key, value in custom_headers.items():
206101
if not all(ord(c) < 128 for c in f"{key}: {value}"):
207102
raise ValueError("Encabezados contienen caracteres no ASCII")
103+
else:
104+
custom_headers = {}
208105
except Exception as e:
209106
print(json.dumps({"status_code": 400, "message": f"Error en encabezados: {str(e)}"}))
210-
exit(1)
107+
sys.exit(1)
211108

212109
# Procesamiento de destinatarios (se espera un JSON array)
213110
try:
214111
recipients = json.loads(" ".join(args.to_mail))
215112
except Exception as e:
216113
print(json.dumps({"status_code": 400, "message": f"Error en destinatarios: {str(e)}"}))
217-
exit(1)
114+
sys.exit(1)
218115

219-
# Construcción de componentes del correo
116+
# Construcción del asunto y cuerpo del correo
220117
email_subject = " ".join(args.subject) if args.subject else " "
221118
email_body = " ".join(args.body) if args.body else " "
222119

223120
try:
224-
# Ejecutar el cliente SMTP
121+
# Ejecutar la función asíncrona que simula el envío
225122
result, error_type = asyncio.run(
226123
send_email(
227124
args.from_mail,
@@ -235,20 +132,17 @@ def main():
235132
)
236133
)
237134

238-
# Generar respuesta basada en resultados
239135
if result:
240136
output = {"status_code": 250, "message": "Message accepted for delivery"}
241137
else:
242138
output = {
243139
"status_code": status_codes.get(error_type, 550),
244140
"message": error_messages.get(error_type, "Unknown error")
245141
}
246-
247142
except Exception as e:
248143
output = {"status_code": 500, "message": f"Excepción: {e}"}
249144

250145
print(json.dumps(output))
251146

252-
253147
if __name__ == "__main__":
254-
main()
148+
main()

0 commit comments

Comments
 (0)