Enviar correos con Python

Esta es la continuación de un articulo anterior donde generamos un reporte en excel, en esta entrada vamos a ver como enviamos dicho reporte por correo a una lista de destinatarios.

Necesitaremos los datos del servidor smtp para poder enviar los correos.

Sin entrar mucho en detalle, el contenido del mensaje de correo puede ser de tres tipos: texto plano, html o un archivo adjunto. En nuestro caso nos interesa enviar un html, para poder tener cierto formato y un archivo adjunto que sera el reporte que deseamos enviar.

Separamos en dos partes el programa para que sea mas sencillo de analizarlo y modificar a futuro.

Mensaje.py

Comenzaremos creando un archivo llamado Mensaje.py, contendrá la parte del mensaje en html; adicionalmente del constructor, tendrá dos métodos más: uno llamado agregarContenido() que nos permite agregar contenido dinámico y el otro getMensaje() que nos devuelve el html completo listo para enviarlo.

También contiene un atributo llamado subject , donde ira el asunto del mensaje.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Mensaje(object):

    def __init__(self,inicio,fin):

        self.subject = "Reporte semanal de descansos "+inicio+" al "+fin

        self.head = "<html><h2><b>Reporte de Periodo "+inicio+" - "+fin+"</b></h2>"
        self.body = "<p>Se adjunta los datos de la semana.</p>"
           

        self.tail = '<p>Para datos especificos por favor referirse a la pagina <a href="#">web</a>.</p></html>'

    def getMensaje(self):
        return self.head+self.body+self.tail
   

    def agregarContenido(self,data):
        self.body = self.body + data

ManejoMail.py

ManejoMail se ocupa de establecer la conexión con el servidor de correo; levantar los datos y enviar a la lista de destinatarios indicados.

Toma los parametros del servidor smtp, usuario y clave desde un archivo json llamado config.json con el siguiente formato:


1
2
3
4
5
6
7
{
    "smtp":"smtp",
    "mail":"demo@demo.com",
    "nombre":"UnserName",
    "puerto":"PortNumer",
    "password":"Password"
}

Finalmente ManejoMail tiene una lista como atributo interno llamado destinatarios, donde almacena la lista de los destinatarios que recibiran el correo.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import smtplib

from os.path import basename
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import COMMASPACE, formatdate
from email.MIMEBase import MIMEBase
from email import Encoders
from Mensaje import Mensaje

import json
import io

class ManejoMail(object):

    def __init__(self):

        self.destinatarios = []
        self.parametros = json.load(open("config.json", "r"))


    def enviarCorreo(self,mensaje,archivo):

        smtp = smtplib.SMTP(self.parametros["smtp"])
        smtp.login(self.parametros["mail"], self.parametros["password"])

        correo = MIMEMultipart()
       
        correo['Subject'] = mensaje.subject
        correo['From'] = self.parametros["mail"]
       
        texto = MIMEText(mensaje.getMensaje(), 'html')
        correo.attach(texto)

        part = MIMEBase('application', "octet-stream")
        part.set_payload(open(archivo, "rb").read())
        Encoders.encode_base64(part)

        part.add_header('Content-Disposition', 'attachment; filename="'+archivo+'"')
        correo.attach(part)

        for destino in self.destinatarios:
            print ("Enviando correo a ",destino)
            correo['To'] = destino
            smtp.sendmail(self.parametros["mail"], destino, correo.as_string())

       

        smtp.quit()

Si bien el listado de destinatarios lo almacenamos en una lista, tranquilamente podriamos leerlos desde una base de datos o de un archivo txt.

Cuestión 1: En este ejemplo y para no complicar tanto, los datos del servidor smtp y credenciales del usuario están dentro de un archivo json plano; podría ser prudente tomarlos desde una base de datos.

Cuestión 2: ManejoMail asume que siempre se va a enviar un archivo como adjunto; en caso que no sea lo que deseamos, se debe modificar las lineas donde se agrega el adjunto:


1
2
3
4
5
6
        part = MIMEBase('application', "octet-stream")
        part.set_payload(open(archivo, "rb").read())
        Encoders.encode_base64(part)

        part.add_header('Content-Disposition', 'attachment; filename="'+archivo+'"')
        correo.attach(part)

¿Como lo usamos?

Suponiendo que tenemos un archivo llamado ejemplo.xls, que deseamos enviar como adjunto y una lista con tres destinatarios, el uso seria el siguiente:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from ManejoMail import ManejoMail
from Mensaje import Mensaje

msg = Mensaje('01-06-2019','07-06-2019')
#Agregamos contenido al mensaje en formato html
msg.agregarContenido("<p>Agregamos un detalle al mensaje</p>")

#Agregamos una imagen al mensaje
msg.agregarContenido("<p><img src="http://www.gsampallo.com/blog/wp-content/uploads/2019/07/logo-gs_logo-fondo-negro.jpg" height="15%"    width="15%"></p>")

mMail = ManejoMail()

#Agregamos a la lista los destinatarios que deseamos que reciban el correo
mMail.destinatarios.append("destinatario1@demo.com")
mMail.destinatarios.append("destinatario2@demo.com")
mMail.destinatarios.append("destinatario3@demo.com")

mMail.enviarCorreo(msg,"ejemplo.xls")

Pueden encontrar el codigo completo del ejemplo en el repositorio de github:

https://github.com/gsampallo/automated_report_python

Un video de como funciona lo pueden ver acá:

Generar un reporte en Excel con Python

Estos días estuve que trabajando en preparar algunos reportes semanales; la idea es automatizarlos para que todos los lunes genere el reporte (partiendo de una consulta a la base de datos) cree un excel y lo envié por correo a las personas interesadas. Básicamente ahorrarme trabajo.

Utilice dos librerias de Python:

  • mysql.connector Para conectarnos con la base de datos, mysql en este caso. Se puede instalar por medio de pip , realizando:
    pip install mysql-connector-python
  • xlwt Para generar el archivo en excel. También se puede instalar con pip: pip install xlwt

El programa lo arme con cinco archivos, cuatro clases para manejar los datos y las acciones y uno que maneja el hilo principal; la idea es que lo llame cron de forma semana.

Para poder mostrar como funciona, vamos a adaptarlo a un ejemplo, supongamos que tenemos una base de datos donde se registran los movimientos del stock de productos:

El reporte consiste en todos los movimientos que se realizaron en esta semana (pueden aplicarlo a cualquier periodo, con un simple cambio).

Para ello utilizaremos un sencilla consulta sql a la base:


1
2
3
4
5
6
7
8
9
10
SELECT
  movimientos.fecha,
  productos.producto_id,
  productos.descripcion,
  movimientos.tipo_movimiento,
  movimientos.cantidad
FROM movimientos
INNER JOIN productos ON movimientos.producto_id = productos.producto_id
WHERE WEEKOFYEAR(movimientos.fecha) = (WEEKOFYEAR(curdate())-1)
ORDER BY productos.descripcion,movimientos.fecha

El truco en la consulta para obtener todos los registros de la semana pasada es la condición que utilizamos:


1
WEEKOFYEAR(movimientos.fecha) = (WEEKOFYEAR(curdate())-1)

Devuelve el nro. de la semana del año y le pedimos que sea igual a la semana anterior. De esta forma obtenemos todos los registros necesarios para el reporte.

Movimiento.py

Movimiento.py sera el primer archivo que crearemos, su función sera la de almacenar un registro (recuerdan el patrón MVC?). Será una clase cuyo constructor tendrá como argumento un array con los datos del movimiento y lo almacenara dentro de atributos propios. Tendrá lo siguiente:


1
2
3
4
5
6
7
8
9
10
11
12
class Movimiento(object):
   
    def __init__(self,lista):
        self.fecha = lista[0]
        self.producto_id = lista[1]
        self.producto = lista[2]
        self.tipo_movimiento = lista[3]
        self.cantidad = str(lista[4])


    def listar(self):
        print (self.fecha,self.producto_id,self.producto,self.tipo_movimiento,self.cantidad)

MovimientosToExcel.py

Como dije anteriormente, deseamos generar el reporte en un archivo excel, esta sera la función de MovimientosToExcel.py; además del constructor tendrá dos métodos, uno para agregar registros/filas y otro para guardar la planilla en un archivo.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import xlwt
from datetime import datetime

class MovimientosToExcel:

    def __init__(self):
        self.wb = xlwt.Workbook()
        self.ws = self.wb.add_sheet('Planilla de Movimientos ',cell_overwrite_ok=True)

        self.ws.write(0, 0, 'Listado de Movimientos')

        columnas = ["Fecha",
                    "Producto ID",
                    "Producto",
                    "Tipo Movimiento",
                    "Cantidad"]

        c = 0
        for columna in columnas:
            self.ws.write(1, c, columna)
            c = c + 1

        self.fila = 2

    def agregarItem(self,item):
        self.ws.write(self.fila, 0, item.fecha)
        self.ws.write(self.fila, 1, item.producto_id)
        self.ws.write(self.fila, 2, item.producto)
        self.ws.write(self.fila, 3, item.tipo_movimiento)
        self.ws.write(self.fila, 4, item.cantidad)

        self.fila = self.fila + 1

    def guardarPlanilla(self,nombreArchivo):
        self.wb.save(nombreArchivo)
        print ("Generado")

Describo brevemente que hace cada método:

  • Constructor: instancia Workbook, de manera que podemos comenzar a crear la planilla, crea la cabecera de la tabla, para ello utiliza la vble. columnas donde están los datos de las columnas.
  • agregarItem(self,item): recibe un argumento; ese argumento sera una instancia de Movimientos.py, y traslada cada atributo del mismo a una fila de la tabla, incremente en 1 la cantidad de filas.
  • guardarPlanilla(nombreArchivo) Valga la rebundancia: guarda el workbook en un archivo, el nombre del archivo esta dado por el argumento (si, ‘
    nombreArchivo’)

En este punto podemos por algunas lineas, escribir un pequeño programa de prueba para validar que todo funcione:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from Movimiento import Movimiento
from MovimientosToExcel import MovimientosToExcel

#Creamos un registro de prueba
item = []
item.append("2019-04-04")
item.append(1)
item.append("Producto A")
item.append("A")
item.append(5)

#Creamos una instancia de Movimiento con el registro de prueba
movimiento1 = Movimiento(item)
movimiento1.listar()

#Instanciamos MovimientoToExcel y le agregamos el mov1
m2e = MovimientosToExcel()
m2e.agregarItem(movimiento1)

#Le pedimos a m2e que cree el archivo planilla.xls con los datos que tenemos.
m2e.guardarPlanilla("planilla.xls")

Luego de ejecutarlo, van a encontrar un nuevo archivo en la carpeta llamado planilla.xls, si lo abrimos con excel tenemos:

De esta manera tenemos un mecanismo sencillo para crear la planilla; nos queda alimentarlo con datos de la base.

reporteSemanal.py

Acá es donde llevaremos el hilo principal, consultaremos a la base de datos, obtendremos el listado de registros y crearemos el archivo excel. Para esto utilizaremos la consulta sql comentada previamente. Lo realizamos de la siguiente manera:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
from datetime import datetime, timedelta
import mysql.connector
from mysql.connector import errorcode
from Movimiento import Movimiento
from MovimientosToExcel import MovimientosToExcel

config = {
        'user': 'root',
        'password': '',
        'host': 'localhost',
        'database': 'stock',
        'raise_on_warnings': True,

      }

cursor = 0
try:
    cnx = mysql.connector.connect(**config)
    cursor = cnx.cursor()

    query = "SELECT \
                DATE_FORMAT(movimientos.fecha,'%d-%m-%Y') as fecha, \
                productos.producto_id, \
                productos.descripcion, \
                movimientos.tipo_movimiento, \
                movimientos.cantidad \
                FROM \
                movimientos \
                INNER JOIN productos ON movimientos.producto_id = productos.producto_id \
                WHERE WEEKOFYEAR(movimientos.fecha) = (WEEKOFYEAR(curdate())-1) \
                ORDER BY productos.descripcion,movimientos.fecha"

    cursor.execute(query)
    m2e = MovimientosToExcel()

    for fila in cursor:
        print(fila)
        movimiento = Movimiento(fila)
        movimiento.listar()

        m2e.agregarItem(movimiento)

    m2e.guardarPlanilla("reporte_semanal.xls")

except mysql.connector.Error as err:
    if err.errno == errorcode.ER_ACCESS_DENIED_ERROR:
        print("Something is wrong with your user name or password")
    elif err.errno == errorcode.ER_BAD_DB_ERROR:
        print("Database does not exist")
    else:
        print(err)
else:
  cnx.close()

Como pueden ver es relativamente sencillo una vez que lo tenemos separados en partes:

  1. Creamos m2e, una instancia de Movimientos2Excel, por medio del cual creamos nuestro archivo excel.
  2. Consultamos a la base y recorremos los registros.
  3. Dentro del bloque de código de la iteración, creamos una instancia de Movimiento que recibe como argumento el registro completo; luego esa instancia la usamos para alimentar al metodo m2e.agregarItem().
  4. Luego que recorremos todos los registros, guardamos el archivo excel.

En este punto si ejecutamos reporteSemanal.py vamos a tener un archivo excel con todos los movimientos pertenecientes a la semana anterior.

El envio por correo lo vamos a ver en el siguiente articulo para que sea tan largo.

Pueden encontrar todo el ejemplo de este articulo en github.

https://github.com/gsampallo/automated_report_python

A continuación dejo un link al video de youtube donde se muestra como funciona:

Cuestión 1: A fin de que el ejemplo no sea tan complicado, la credencial que se utiliza para conectarse a mysql están dentro del programa, esto no es buena practica.

Cuestión 2: Si van a probar el ejemplo, tengan a bien de modificar la fecha de los registros de ejemplo o agregar nuevos registros dentro del periodo de tiempo que necesitan.