Hace poco en un proyecto debíamos incluir un servicio de envío de correo y tomamos la decisión de utilizar el servicio AWS SES, sin embargo cuando me dirigí a la documentación me encontré con un código que le daba "muchas vueltas" cuando se quería enviar un correo con un simple adjunto, encontre tambien ejemplos que tenían el mismo problema, adicional muchos de los módulos de esos ejemplos actualmente se consideran legacy así que leyendo un poco me di cuenta que a partir de la versión de python 3.6 se agrego una clase llamada EmailMessage la cual esta muy cool y como veremos a continuación permite escribir un código más corto y más "legible".
Para los requisitos necesitamos tener python 3.6 o superior yo usare 3.7 (no hay que instalar librerias externas) y un servicio ya sea smtp o en cloud para enviar el correo, en mi caso usare AWS SES así que sin mas vayamos al código
from email.message import EmailMessage
from email.headerregistry import Address
def build_email(name_client):
email_message = EmailMessage()
email_message["Subject"] = "Registro de Bienvenida"
email_message["From"] = Address(username="jairo", domain="eceleris.com", display_name="Jairo Andres")
email_message["To"] = Address(username="destino", domain="gmail.com", display_name="Destino")
email_message.set_content(f"Bienvenido a nuestra pagina {name_client}")
send_email(email_message=email_message)
def send_email(email_message:EmailMessage):
client = boto3.client("sesv2")
response = client.send_email(
FromEmailAddress="jairo@eceleris.com",
Destination={
"ToAddresses": ["destino@gmail.com"],
},
Content={"Raw": {"Data": email_message.as_string()}},
)
build_email("Jairo")
Ejecutamos nuestro código
python main.py
y el resultado lo podemos ver en nuestra bandeja de correo
Vamos a explicar un poco el código , pero nos enfocaremos en el método build_email ya en otro artículo hablaremos de AWS SES
from email.message import EmailMessage
from email.headerregistry import Address
def build_email(name_client):
email_message = EmailMessage()
email_message["Subject"] = "Registro de Bienvenida"
email_message["From"] = Address(username="jairo", domain="eceleris.com", display_name="Jairo Andres")
email_message["To"] = Address(
username="jairo", domain="eceleris.com", display_name="Jairo Andres"
)
email_message.set_content(f"Bienvenido a nuestra pagina {name_client}")
send_email(email_message=email_message)
vamos a importar dos clases una que se llama EmailMessage y otra Address, para enviar un correo la estructura debe cumplir unos estándares de payloads y headers referenciados en RFC 5322 o RFC 6532 los cuales sería bastante complicados gestionarlos manualmente, estas clases utilizando orientación a objetos y diccionarios nos facilitan la gestión, la primera clase EmailMessage es la más importante en ella nos permite agregar algunos keys como vemos en el código agregamos 3:
- "Subject" como su nombre indica será el asunto del mensaje
- "From" será quien envía el mensaje, aquí se usa la clase Address que es una clase de apoyo para darle cumplimiento al formato requerido en RFC pero haciéndolo sencillo al programador convirtiendo esto en parámetros
- "To" Será el destinatario o los destinatarios, aquí igualmente se vuelve utilizar la clase de apoyo Address por la misma razón mencionada en el punto anterior (si son varios destinatarios simplemente se usa una lista de Address)
Luego el método set_content() nos permite agregar un texto plano en donde utilizamos f string para reemplazar el name_client, aquí se resalta lo importante de la clase EmailMessage por que el automaticamente por debajo agrego unos tipos de información para cumplir el RFC, en la documentación que cité legacy esto se hacía manual, en otro ejemplo abajo veremos la importancia de esto
Finalmente se llama la función send_email la cual se encarga de enviar el correo con AWS SES, el código se redujo sustancialmente a lo que antes se había podido evidenciar en el ejemplo citado de la documentación de AWS, pero veamos ahora un ejemplo más elaborado en donde tenemos un archivo de cuerpo html y se adjunta una imagen.
from email.message import EmailMessage
from email.headerregistry import Address
import boto3
def open_html(name_client):
html_data: str = ""
with open("template.html", mode="r", encoding="utf-8") as data:
html_data = data.read().replace("\n", "")
html_data = html_data.replace("{{name_client}}", name_client)
return html_data
def open_file_image():
file_image = None
with open("Wallpaper.png", mode="rb") as file:
file_image = file.read()
return file_image
def build_email(name_client):
email_message = EmailMessage()
email_message["Subject"] = "Registro de Bienvenida"
email_message["From"] = Address(
username="jairo", domain="eceleris.com", display_name="Jairo Andres"
)
email_message["To"] = Address(
username="destino", domain="destino.com", display_name="Destino"
)
html_data: str = open_html(name_client)
email_message.add_alternative(html_data, subtype="html")
email_message.add_attachment(
open_file_image(), maintype="image", subtype="png", filename="Wallpaper.png"
)
send_email(email_message=email_message)
def send_email(email_message: EmailMessage):
client = boto3.client("sesv2")
response = client.send_email(
FromEmailAddress="jairo@eceleris.com",
Destination={
"ToAddresses": ["destino@gmail.com"],
},
Content={"Raw": {"Data": email_message.as_string()}},
)
build_email("Jairo")
ejecutamos nuestro código y el resultado es el siguiente
Listo en este nuevo código hemos agregado dos nuevas funciones y dos archivos, un archivo de tipo html y otro png que pondre a continuacion
Siguiendo con el código explicaremos las dos nuevas funciones que se han agregado (open_html y open_file_image)
def open_html(name_client):
html_data: str = ""
with open("template.html", mode="r", encoding="utf-8") as data:
html_data = data.read().replace("\n", "")
html_data = html_data.replace("{{name_client}}", name_client)
return html_data
La función de open_html consiste en cargar el template.html leerlo en memoria reemplazar algunos \n por espacios vacíos y reemplazar la variable name_client en {{name_client}} después retornar esta información procesada
def open_file_image():
file_image = None
with open("Wallpaper.png", mode="rb") as file:
file_image = file.read()
return file_image
La función open_file_image básicamente carga la imagen png, la lee y retorna los bytes en memoria, seguido de este se puede observar que al código en la función build_email en el objeto email_message se llaman dos métodos nuevos add_alternative y add_attachment
def build_email(name_client):
email_message = EmailMessage()
email_message["Subject"] = "Registro de Bienvenida"
email_message["From"] = Address(
username="jairo", domain="eceleris.com", display_name="Jairo Andres"
)
email_message["To"] = Address(
username="destino", domain="destino.com", display_name="Destino"
)
html_data: str = open_html(name_client)
email_message.add_alternative(html_data, subtype="html")
email_message.add_attachment(
open_file_image(), maintype="image", subtype="png", filename="Wallpaper.png"
)
send_email(email_message=email_message)
- add_alternative permite agregarle al cuerpo del mensaje la información, en esta caso internamente la clase llama a set_content que se llamaba inicialmente, este recibe dos parámetros el html en string y el subtype que en este caso es html pero podría ser texto plano también
- add_attachment este método permite agregar adjuntos al correo, se pueden agregar tantos como se quieran y recibe cuatro parámetros el archivo en bytes, el maintype y subtype que son dos divisiones de lo que se conoce como MIME TYPES (en este caso se dividen del "/") y el nombre del archivo que queremos que se muestre en el adjunto cuando se descargue
Finalmente enviamos el correo con el método send_email, para finalizar el articulo mas que una conclusión es una invitación a usar este módulo no solo por que el módulo mime es legacy si no también por la facilidad con la que nos permite construir la estructura del correo a enviar, en mi experiencia con buenas optimizaciones el código se puede reducir y hacer muchisimo mas legible y expandible para agregar más funcionalidades, aquí pueden encontrar toda la documentación