Envío SMTP con contenido HTML o Texto plano y soporte para SSL/TLS y STARTTLS

Programa desarrollado en Python 3 para el envío de mensajes con contenido HMTL o Texto plano y con soporte para SSL/TLSSTARTTLS y archivos adjuntos.


Introducción

Programa para realizar envíos de correo con contenido en formato HTML o Texto plano, a partir de un archivo con extensión .html o .txt respectivamente.

Dispone de soporte para SSL/TLS y STARTTLS.

Posibilidad de enviar a uno o varios destinatarios, incluidos en CC, y con cambio de identidad, si el proveedor del servicio de correo lo permite, aunque no recomendamos el uso de esta práctica.

También tenemos la opción de adjuntar uno o varios archivos en el mismo envío.


Requisitos

El programa está desarrollado para ser usado con Python 3.x.


Instalación

No se requiere la instalación de módulos adicionales, ya que se encuentran incluidos por defecto en la versión del lenguaje en el que se ha desarrollado. Por lo tanto, tampoco será necesario ejecutar en un entorno virtual de Python.

Para clonar el repositorio nos ubicamos en el directorio en donde queramos instalar el programa y usamos el siguiente comando para clonarlo:

git clone https://sergiobr@bitbucket.org/sergiobr/smtp-contenido-html-o-texto-plano-desde-python.git

Y ya estaría listo para su uso en el directorio en donde lo hayamos clonado.

También podemos crear el fichero con el contenido del programa, pues fue desarrollado en un mismo archivo, aunque te recomiendo hacer el clonado por si se suben actualizaciones a BitBucket. Contenido:

import os
from os.path import basename
# Options
from optparse import OptionParser
# Email
from email.utils import formatdate
#from email.mime.image import MIMEImage
import smtplib
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText


# VARIABLES
################

# Email server.
emailHost = ''
# Int.
emailPort = 25
# True/False.
emailTLS = False
emailSSL = False
# Email credentials.
emailUser = ''
emailPwd = ''
# Email subject.
emailSubject = ''
# Email From info.
emailFrom = ''
# Email To and CC.
emailTo = ''
emailToCc = ''
# Attachment/s.
emailAttachment = ''
# Replay header.
emailReplyTo = ''


# CLASSES
#######################

# Options parser
def programParser():
      try:
            # Description and version variables.
            usage = "python %prog [options] bodyContentFile.[html|txt]"
            description = "Program to send email with HTML or Plain text content, with also attachments.\n\nConfigure the variables section to connect to your email server, or use the options to change any of these.\n\nSpecify the body content in a .html or .txt file, depending on the format you choose.\n\nNever use single quotes ('') in each option value. If you not add white spaces quotes are not neccesary, but if you use it only double quotes (\"\") are allowed."
            version = '%prog: version 1.0'

            # Parser init.
            parser = OptionParser(usage,version=version,description=description,add_help_option=True)

            # Parser options.
            parser.add_option('-H', '--host', dest = 'host', help = 'SMTP server.')
            parser.add_option('-p', '--port', dest = 'port', help = 'SMTP port.')
            parser.add_option('--tls', action = 'store_true', help = 'TLS security on.')
            parser.add_option('--ssl', action = 'store_true', help = 'SSL security on.')
            parser.add_option('--nosecurity', action = 'store_true', help = 'No TLS or SSL security.')
            parser.add_option('-u', '--username', dest = 'username', help = 'Username login.')
            parser.add_option('-P', '--password', dest = 'password', help = 'Password login.')
            parser.add_option('-s', '--subject', dest = 'subject', help = 'Email subject, between double quotes (\"\").')
            parser.add_option('-f', '--from', dest = 'emailFrom', help = 'Email from. Different than Username could be considered as Email Spoofing. If it is not defined it will be the same than the user name.')
            parser.add_option('-t', '--to', dest = 'emailTo', help = 'Email destination. More than one separated by comas (,), with no white spaces.')
            parser.add_option('-c', '--carboncopy', dest = 'emailToCc', help = 'Email destination copy. More than one separated by comas (,), with no white spaces.')
            parser.add_option('-a', '--attachment', dest = 'emailAttachment', help = 'Attach a file or files, separated by comas (,) and between double quotes (\"\").')
            parser.add_option('-r', '--replyto', dest = 'emailReplyTo', help = 'Email to reply. If it is not defined it will be the email from.')

            return parser

      except Exception as e:
            raise ValueError('Function - creating program parser: %s' % str(e))

# Check parser to change file variables.
def checkParserOptionsArguments(options,args):
      try:
            # ARGUMENTS, only 1 is available. No arguments or more than one show an error.
            if len(args) == 0:
                  raise ValueError('No argument has been indicated.')
            elif len(args) > 1 or len(args) < 0:
                  raise ValueError('Only one argument is allowed.')
            else:
                  # Check the argument extension. Only .html or .txt are available.
                  extension = args[0].split('.')
                  fileExt = extension[1].lstrip('\'').rstrip('\'')
                  
                  # Other extensions than .html or .txt extension show an error.
                  if fileExt != 'html' and fileExt != 'txt':
                        raise ValueError('Only files with extensions .html or .txt are allowed.')
                  else:

                        # Identify email type by the file extension (HTML or Plain text).
                        global emailType
                        if fileExt == 'html':
                              emailType = 'html'
                        elif fileExt == 'txt':
                              emailType = 'plain'

            # Parser options added.
            # Email Host.
            if options.host:
                  global emailHost
                  emailHost = str(options.host.lstrip('\'').rstrip('\''))
            
            # Email Port.
            if options.port:
                  global emailPort
                  emailPort = int(options.port.lstrip('\'').rstrip('\''))
            
            # Security, only one or none allowed.
            if options.tls and options.ssl:
                  raise ValueError('Only one option is available, --ssl or --tls, or nothing.')
            elif options.nosecurity:
                  global emailSSL
                  global emailTLS
                  emailTLS = False
                  emailSSL = False                  
            else:                  
                  if options.tls:
                        emailTLS = True
                        emailSSL = False
                  if options.ssl:
                        emailSSL = True
                        emailTLS = False
            
            # Email Username
            if options.username:
                  global emailUser
                  emailUser = str(options.username.lstrip('\'').rstrip('\''))

            # Email password
            if options.password:
                  global emailPwd
                  emailPwd = str(options.password.lstrip('\'').rstrip('\''))

            # Email subject.
            if options.subject:
                  global emailSubject
                  emailSubject = str(options.subject.lstrip('\'').rstrip('\''))
            
            # Email from.
            if options.emailFrom:
                  global emailFrom
                  emailFrom = str(options.emailFrom)

            # Email/s to.
            if options.emailTo:
                  global emailTo
                  emailTo = str(options.emailTo.lstrip('\'').rstrip('\''))

            # Email/s in Carbon Copy.
            if options.emailToCc:
                  global emailToCc
                  emailToCc = str(options.emailToCc.lstrip('\'').rstrip('\''))

            # Email attachment/s.
            if options.emailAttachment:
                  global emailAttachment
                  emailAttachment = str(options.emailAttachment.lstrip('\'').rstrip('\''))

            # Email reply header.
            if options.emailReplyTo:
                  global emailReplyTo
                  emailReplyTo = str(options.emailReplyTo.lstrip('\'').rstrip('\''))

      except Exception as e:
            raise ValueError('Function - checking parser variables: %s' % str(e))
      

# Email Class
class emailClass():

      # Constructor with email config variables.
      def __init__(self,emailHost,emailPort,emailTLS,emailSSL,emailUser,emailPwd,emailSubject,emailFrom,emailTo,emailToCc,emailReplyTo):
            self.emailHost = emailHost
            self.emailPort = emailPort
            self.emailTLS = emailTLS
            self.emailSSL = emailSSL
            self.emailUser = emailUser
            self.emailPwd = emailPwd
            self.emailSubject = emailSubject

            self.emailFrom = emailFrom
            if not self.emailFrom:
                  self.emailFrom = self.emailUser
            elif not '@' in self.emailFrom:
                  self.emailFrom = self.emailFrom + " <" + self.emailUser + ">"
            
            self.emailTo = emailTo
            self.emailToCc = emailToCc
            
            self.emailReplyTo = emailReplyTo
            if not self.emailReplyTo:
                  self.emailReplyTo = self.emailFrom

            self.mailRcpt = self.emailTo.split(',') + self.emailToCc.split(',')

            if self.emailTLS and self.emailSSL:
                  raise ValueError('Check variables for SSL and TLS in the program, only one or nothing are availables.')

      # Send the email from config variables (from file or command options).
      def emailSend(self,type,bodyContent,attach=None):
            try:
                  # Type (HTML or Plain text).
                  if type == 'html':
                        msg = MIMEMultipart('related')
                  elif type == 'plain':
                        msg = MIMEMultipart()
                  else:
                        raise ValueError('Class - Error declaring email type (html or plain).')
                  
                  # Message info.
                  msg['Subject'] = self.emailSubject
                  msg['From'] = self.emailFrom
                  msg['To'] = self.emailTo
                  msg['Cc'] = self.emailToCc
                  msg["Date"] = formatdate(localtime=True)
                  msg["Reply-To"] = self.emailReplyTo

                  # Body content.
                  with open(bodyContent) as f:
                        body = f.read()
                  if type == 'html':
                        part2 = MIMEText(body,'html')
                  elif type == 'plain':
                        part2 = MIMEText(body,'plain')
                  else:
                        raise ValueError('Class - Error declaring email type (html or plain).')
                  msg.attach(part2)

                  # Attach file/s, it it has been indicated.
                  if attach:
                        for f in attach.split(','):                                                   
                              with open(f, "rb") as file:
                                    part = MIMEApplication(file.read(), Name=basename(f))
                              part['Content-Disposition'] = 'attachment; filemane=%s' % basename(f)
                              msg.attach(part)

                  # Create message.
                  textBody = msg.as_string()


                  # Configure security, if it has been configured.
                  if self.emailSSL:
                        connection = smtplib.SMTP_SSL(self.emailHost,self.emailPort)
                        connection.ehlo()
                  else:
                        connection = smtplib.SMTP(self.emailHost,self.emailPort)
                        connection.ehlo()
                        if self.emailTLS:
                              connection.starttls()

                  # Email credentials, if it has been configured.
                  if self.emailUser:
                        connection.login(self.emailUser,self.emailPwd)

                  # Send the email.
                  connection.sendmail(self.emailFrom,self.mailRcpt,textBody)

                  # Close connection.
                  connection.quit()

            except Exception as e:
                  raise ValueError('Class error: %s' % (str(e)))


# PROGRAM
###########

try:

      # Parse argument and options, if exists.
      parser = programParser()
      (options, args) = parser.parse_args()

      # Check parser options and argument.
      checkParserOptionsArguments(options,args)

      email = emailClass(emailHost,emailPort,emailTLS,emailSSL,emailUser,emailPwd,emailSubject,emailFrom,emailTo,emailToCc,emailReplyTo)

      # html or plain / html body file or plain text body, and the attachment if is required.
      email.emailSend(emailType,args[0].lstrip('\'').rstrip('\''),emailAttachment)

      # If no error is shown, print ok.
      print('Email sent!!!')

except Exception as e:
      print('ERROR - %s' % (str(e)))
finally:
      print('\nPowered by https://blog.tiraquelibras.com')

Configuración

Dentro del ejecutable

Dentro del archivo envio.py tenemos al comienzo la sección VARIABLES en donde podemos indicar los datos de acceso al buzón de correo para realizar el envío, incluido/s el/los destinatario/s del mensaje.

# Email server.
emailHost = ''
# Int.
emailPort = 25
# True/False.
emailTLS = False
emailSSL = False
# Email credentials.
emailUser = ''
emailPwd = ''
# Email subject.
emailSubject = ''
# Email From info.
emailFrom = ''
# Email To and CC.
emailTo = ''
emailToCc = ''
# Attachment/s.
emailAttachment = ''
# Replay header.
emailReplyTo = ''

En esta sección configuramos:

  • emailHost Servidor de email.
  • emailPort Puerto del servidor de email.
  • emailSSL/emailTLS Seguridad SSL/TLS o STARTTLS, si tuviera alguno.
  • emailUser/emailPwd Credenciales del buzón para realizar el envío.
  • emailSubject Asunto del mensaje.
  • emailFrom Información del origen o From.
  • emailTo/emailCc Destinatarios y destinatarios en copia.
  • emailAttachment Adjuntos, si los tuviera.
  • emailReplyTo Cabecera de respuesta.

Usando las opciones del programa (prioritario)

También podemos indicar estos parámetros en las opciones del programa, las cuales tienen prioridad sobre lo que se configure en el archivo, comentado justo encima de estas líneas, en la sección Dentro del ejecutable.

Estas serían las opciones:

Options:
--version             show program's version number and exit
-h, --help           show this help message and exit
-H HOST, --host=HOST SMTP server.
-p PORT, --port=PORT SMTP port.
--tls                 TLS security on.
--ssl                 SSL security on.
--nosecurity         No TLS or SSL security.
-u USERNAME, --username=USERNAME
                      Username login.
-P PASSWORD, --password=PASSWORD
                      Password login.
-s SUBJECT, --subject=SUBJECT
                      Email subject, between double quotes ("").
-f EMAILFROM, --from=EMAILFROM
                      Email from. Different than Username could be
                      considered as Email Spoofing. If it is not defined it
                      will be the same than the user name.
-t EMAILTO, --to=EMAILTO
                      Email destination. More than one separated by comas
                      (,), with no white spaces.
-c EMAILTOCC, --carboncopy=EMAILTOCC
                      Email destination copy. More than one separated by
                      comas (,), with no white spaces.
-a EMAILATTACHMENT, --attachment=EMAILATTACHMENT
                      Attach a file or files, separated by comas (,) and
                      between double quotes ("").
-r EMAILREPLYTO, --replyto=EMAILREPLYTO
                      Email to reply. If it is not defined it will be the
                      email from.

El listado de opciones mostradas son las siguientes:

OpciónVarianteDescripción
versionMuestra la versión del programa.
-hhelpMuestra la ayuda del programa.
-H <host>host=<host>Servidor de correo para realizar el envío.
-p <port>port=<port>Puerto del servidor de correo.
tlsSeguridad STARTTLS.
sslSeguridad SSL.
nosecurityAnular cualquier tipo de seguridad.
-u
 <username>
username=<username>Login de usuario.
-P <passwd>password=<passwd>Contraseña de usuario.
-s <subject>subject=<subject>Asunto del mensaje.
-f <from>from=<from>Información del From del mensaje.
-t <to>to=<to>Destinatario/s, separados por comas (,).
-c <cc>carboncopy=<cc>Destinatario/s en copia, separados por comas(,).
-a <file/s>attachment=<file/s>Archivo/s adjunto/s, separados por comas (,).
-r <reply-to>replyto=<reply-to>Cabecera de respuesta. Si no se indica coincidirá con la dirección de From.
Opciones de ejecución preferentes

Ejecución

Para ejecutar el programa utilizamos el siguiente comando:

python3 envio.py <argumento> <opciones>

Siendo en nuestro caso:

  • python3 la versión de Python instalada, en nuestro caso python3.
  • envio.py es el archivo que contiene nuestro código del programa.
  • <argumento> archivo.html o archivo.txt con el contenido del email.
  • <opciones> las opciones que veremos más adelante en la ayuda.

Por ejemplo:

python3 envio.py archivo.html

Para consultar las opciones del programa disponemos de una ayuda contextual agregando la opción -h o help. Recordar que estas tienen prioridad sobre las variables configuradas en el mismo ejecutable, comentado en la sección anterior de Configuración:

python3 envio.py archivo.html --help

La explicación de cada opción la puedes ver en la sección anterior.

Indicar que las opciones en formato string no deben de contener espacios en blanco, o si los contienen como por ejemplo el asunto se deben de utilizar comillas dobles (» «), nunca usar comillas simples (‘ ‘). El programa está diseñado para sustituirlas, pero puede que algún caso no se hubiera contemplado, dando un error como resultado final. Por ejemplo, si queremos enviar a más de un destinatario podemos indicar las direcciones sin comillas dobles y sin espacios en blanco:

email1@example.org,email2@example.org

O con comilla dobles, por ejemplo si usamos espacios como en el asunto:

«email1@example.org,email2@example.org«

Para mostrar la versión del programa ejecutamos el comando:

python envio.py --version

Ejemplo

Creamos un archivo prueba.html con el siguiente contenido:

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <meta http-equiv="X-UA-Compatible" content="ie=edge">
   <title>Document</title>
</head>
<body>
   <h1>Email de prueba</h1>
   <hr>
   <p>Esto es un email de prueba.</p>
   <br>
   <p>Saludos</p>
</body>
</html>

Y lo enviamos ejecutando el comando de la siguiente forma, sin usar opciones, únicamente con las variables configuradas en el ejecutable:

python envio.py prueba.html

Siendo el resultado el siguiente:

$ python envio.py prueba.html
Email sent!!!
​
Powered by https://blog.tiraquelibras.com

Y en el destino se vería de la siguiente forma:

Ejemplo de envío

Lo mismo con un archivo de texto con extensión .txt y texto plano en su interior.


Enlaces de interés

Acceso al repositorio en BitBucket.