Source code for pyautomail.emailsender

import smtplib
import os
import jinja2
import json
from email import encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from configparser import ConfigParser
from pyautomail.config import configurable
from pyautomail.utils import init_logger


__all__ = ['EmailSender']


[docs]class EmailSender: """ This class is for sending emails to multiple users. This class uses a template engine to render the email body. Currently, it supports two template types: '.txt' and '.html'. You can use the template type that suits your needs. The template engine that is used in this class is Jinja2. You can find more information about Jinja2 in the following link: https://jinja.palletsprojects.com/en/2.11.x/ Examples -------- >>> from pyautomail import EmailSender >>> sender = EmailSender(cfg="config.cfg") >>> sender.set_template('body.txt') >>> data = {'name': 'Jon', 'age': 30} >>> sender.send('contact1@gmail.com', 'sub1', data) a simple example of a template file: body.txt: .. code-block:: text Hello {{name}}, your age is {{age}}. a simple example of a config.cfg file: .. code-block:: ini [smtp] host = smtp.gmail.com port = 465 is_test = True [account] user = "" password = "" Or you can use the class without a config file:: from automail import EmailSender sender = EmailSender(user="your-email-address", password="your-password") sender.set_template('body.txt') data = {'name': 'Jon', 'age': 30} sender.send('contact1@gmail.com', 'sub1', data) .. _NumPy Documentation HOWTO: https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt """
[docs] @configurable def __init__(self, user, password, host="smtp.gmail.com", port=465, is_test=False, log_file=None, log_level=10): """ Parameters ---------- user : str The email address of the sender. password : str The password of the sender. host : str The host of the SMTP server. port : int The port of the SMTP server. is_test : bool If this flag is set, no email will be sent. This flag is useful for testing purposes. log_file : str The path to the log file. log_level : int The level of the logger. The default value is 100. You can use the following values: - 10: DEBUG - 20: INFO - 30: WARNING - 40: ERROR - 50: CRITICAL """ self.__logger = init_logger('EmailSender', filename=log_file, level=log_level) self.__logger.info("Initializing EmailSender...") self.template_type = None self.template = None self.__test_flg = is_test self.user = user self.host = host self.port = port self.password = None if not self.__test_flg: self.__logger.info("Connecting to SMTP server...") self.password = password self.server = smtplib.SMTP_SSL(self.host, self.port) self.__logger.info("Connected to SMTP server.") self.__logger.info(f"Logging in to user account: {self.user}...") try: self.server.login(self.user, self.password) self.__logger.info("Logged in.") except smtplib.SMTPException as e: self.__logger.error('Authentication ERROR: Username and Password not accepted. ' 'Please check username and password and try again') self.__logger.debug(self.password) exit(1) else: self.__logger.warning("Test Mode is enabled. In this mode no email will be sent.") self.__logger.warning("To disable test mode, set is_test=False when initializing this class.") self.__logger.warning(f"user account: {self.user}...")
def __del__(self): """This function is called when the class is destroyed. """ if not self.__test_flg: self.__logger.info("Closing connection to SMTP server...") self.server.close() self.__logger.info("Connection closed.")
[docs] @classmethod def from_config(cls, cfg=None): """This method is used to initialize this class from a config file. """ if cfg is None: return {} if os.path.exists(cfg): config = ConfigParser() config.read(cfg) else: return {} host = config.get('smtp', 'host') port = config.getint('smtp', 'port') is_test = config.getboolean('smtp', 'is_test') user = config.get('account', 'user') password = config.get('account', 'password') log_file = config.get('log', 'file-path') log_level = config.getint('log', 'level') return {'host': host, 'port': port, 'user': user, 'password': password, 'is_test': is_test, 'log_file': log_file, 'log_level': log_level}
[docs] def set_template(self, template_path=None, plain_temp=None): """ :param template_path: Path to template file. the template file should be either HTML or TXT. :param plain_temp: A string template. :raises: NotImplemented if template type is not supported. """ assert template_path is not None or plain_temp is not None,\ "one of 'template_path' or 'plain_temp' should be set" if template_path is not None: with open(template_path) as f: self.template = f.read() if template_path.endswith('.html'): self.template_type = 'html' elif template_path.endswith('.txt'): self.template_type = 'txt' else: self.__logger.error("Template type not supported! Exiting! (please use HTML or TXT templates.)") raise (NotImplemented, "Template type not supported! Exiting! (please use HTML or TXT templates.)") elif plain_temp is not None: self.template = plain_temp self.template_type = 'plain'
[docs] def render_template(self, data): """This method is used by send method. :param data: dictionary of data to be replaced in the template. :return: body: the string of the rendered template. """ if isinstance(data, str): data = json.loads(data) if self.template_type == 'html': self.__logger.debug("Rendering HTML template...") return jinja2.Template(self.template).render(data) elif self.template_type == 'txt': self.__logger.debug("Rendering TXT template...") return jinja2.Template(self.template).render(data) elif self.template_type == 'plain': self.__logger.debug("Rendering plain template...") return jinja2.Template(self.template).render(data) else: self.__logger.error("Template type not supported! Exiting! (please use HTML or TXT templates.)") raise (NotImplemented, "Template type not supported! Exiting! (please use HTML or TXT templates.)")
[docs] def send(self, receiver_email_address: str, subject: str, data: dict, attachment_path=None, from_address=None) -> dict: """ :param receiver_email_address: Email address of the receiver. :param subject: Subject of the email. :param data: dictionary of data to be replaced in the template. :param attachment_path: Path to attachment file. :param from_address: Email address of the sender. :return: """ message = MIMEMultipart("alternative") message["Subject"] = subject message["To"] = receiver_email_address if from_address is None: message["From"] = self.user else: message["From"] = from_address if self.template_type is not None: body = self.render_template(data) if self.template_type == 'html': message.attach(MIMEText(body, "html")) elif self.template_type == 'txt': message.attach(MIMEText(body, "plain")) elif self.template_type == 'plain': message.attach(MIMEText(body, "plain")) else: self.__logger.warning("No template is set. Sending email with no body.") body = '' if attachment_path: filename = os.path.basename(attachment_path) with open(attachment_path, "rb") as attachment: part = MIMEBase("application", "octet-stream") part.set_payload(attachment.read()) encoders.encode_base64(part) part.add_header( "Content-Disposition", f"attachment; filename= {filename}", ) message.attach(part) try: self.__logger.info("Sending email to: " + receiver_email_address + " message:\n" + body[0:1000] + "...\n") if self.__test_flg: output = {'test': True} else: output = self.server.sendmail(self.user, receiver_email_address, message.as_string()) output['test'] = False output['err'] = 'none' except Exception as e: self.__logger.error("Error sending email to: " + receiver_email_address + " error: " + str(e)) return {'test': self.__test_flg, 'err': str(e)} return output