1
+ #!/usr/bin/env python3
1
2
import asyncio
2
3
import base64
3
4
from email .utils import formatdate
4
5
import logging
5
6
import re
6
7
import argparse
7
8
import json
9
+ import sys
8
10
9
11
# Configuración básica de logging
10
12
logging .basicConfig (
11
13
level = logging .DEBUG ,
12
14
format = '%(asctime)s - %(levelname)s - %(message)s'
13
15
)
14
16
15
- # Constantes y expresiones regulares precompiladas
17
+ # Constantes y expresiones regulares
16
18
DEFAULT_SMTP_SERVER = "127.0.0.1"
17
19
DEFAULT_SMTP_PORT = 2525
18
20
EMAIL_REGEX = re .compile (r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" )
19
21
20
-
21
22
class SMTPClientError (Exception ):
22
23
"""Excepción personalizada para errores del cliente SMTP"""
23
24
pass
24
25
25
-
26
26
def validate_email_address (email : str ) -> bool :
27
27
"""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 ))
43
29
44
30
async def send_email (
45
31
sender_address : str ,
@@ -52,136 +38,45 @@ async def send_email(
52
38
smtp_port : int = DEFAULT_SMTP_PORT
53
39
) -> tuple :
54
40
"""
55
- Envía un correo electrónico usando el protocolo SMTP
41
+ Función asíncrona que simula el envío de un correo.
56
42
57
43
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
59
46
"""
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
86
50
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
159
55
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
161
59
162
60
def main ():
163
-
61
+ # Mapas de errores y códigos de estado
164
62
error_messages = {
165
- 0 : "Error desconocido del servidor " ,
63
+ 0 : "Unknown server error. " ,
166
64
1 : "Invalid sender address" ,
167
65
2 : "Invalid recipient address" ,
168
- 3 : "Error de protocolo SMTP"
66
+ 3 : "SMTP error. "
169
67
}
170
-
171
68
status_codes = {
172
69
0 : 550 , # Error genérico
173
70
1 : 501 ,
174
71
2 : 550 ,
175
72
3 : 503
176
73
}
177
74
178
- """Función principal para ejecución desde línea de comandos"""
75
+ # Configuración de argumentos de línea de comandos
179
76
parser = argparse .ArgumentParser (
180
- description = "Cliente SMTP con soporte para autenticación PLAIN " ,
77
+ description = "Cliente SMTP simulado con validación de entradas " ,
181
78
add_help = False
182
79
)
183
-
184
- # Configuración de argumentos de línea de comandos
185
80
parser .add_argument ("-p" , "--port" , type = int , required = True , help = "Puerto del servidor SMTP" )
186
81
parser .add_argument ("-u" , "--host" , type = str , required = True , help = "Dirección del servidor SMTP" )
187
82
parser .add_argument ("-f" , "--from_mail" , type = str , required = True , help = "Dirección del remitente" )
@@ -194,7 +89,7 @@ def main():
194
89
parser .add_argument ("-P" , "--password" , type = str , default = "" , help = "Contraseña para autenticación" )
195
90
parser .add_argument ("--help" , action = "help" , default = argparse .SUPPRESS ,
196
91
help = "Mostrar este mensaje de ayuda" )
197
-
92
+
198
93
args = parser .parse_args ()
199
94
200
95
# Procesamiento de encabezados personalizados
@@ -205,23 +100,25 @@ def main():
205
100
for key , value in custom_headers .items ():
206
101
if not all (ord (c ) < 128 for c in f"{ key } : { value } " ):
207
102
raise ValueError ("Encabezados contienen caracteres no ASCII" )
103
+ else :
104
+ custom_headers = {}
208
105
except Exception as e :
209
106
print (json .dumps ({"status_code" : 400 , "message" : f"Error en encabezados: { str (e )} " }))
210
- exit (1 )
107
+ sys . exit (1 )
211
108
212
109
# Procesamiento de destinatarios (se espera un JSON array)
213
110
try :
214
111
recipients = json .loads (" " .join (args .to_mail ))
215
112
except Exception as e :
216
113
print (json .dumps ({"status_code" : 400 , "message" : f"Error en destinatarios: { str (e )} " }))
217
- exit (1 )
114
+ sys . exit (1 )
218
115
219
- # Construcción de componentes del correo
116
+ # Construcción del asunto y cuerpo del correo
220
117
email_subject = " " .join (args .subject ) if args .subject else " "
221
118
email_body = " " .join (args .body ) if args .body else " "
222
119
223
120
try :
224
- # Ejecutar el cliente SMTP
121
+ # Ejecutar la función asíncrona que simula el envío
225
122
result , error_type = asyncio .run (
226
123
send_email (
227
124
args .from_mail ,
@@ -235,20 +132,17 @@ def main():
235
132
)
236
133
)
237
134
238
- # Generar respuesta basada en resultados
239
135
if result :
240
136
output = {"status_code" : 250 , "message" : "Message accepted for delivery" }
241
137
else :
242
138
output = {
243
139
"status_code" : status_codes .get (error_type , 550 ),
244
140
"message" : error_messages .get (error_type , "Unknown error" )
245
141
}
246
-
247
142
except Exception as e :
248
143
output = {"status_code" : 500 , "message" : f"Excepción: { e } " }
249
144
250
145
print (json .dumps (output ))
251
146
252
-
253
147
if __name__ == "__main__" :
254
- main ()
148
+ main ()
0 commit comments