Source code for auto_emailer.emailer

import os
import time

import smtplib
from pathlib import Path

from email import encoders
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

from .config import credentials
from .config import default_credentials


[docs]class Emailer: """Welcome to the auto-emailer to send all of your emails!""" def __init__(self, config=None, delay_login=True): """ Args: config (Optional(config.credentials.Credentials)): The constructed credentials. Can be None if environment variables are configured. delay_login (bool): If True, no login attempt will be made until send_mail is called. Otherwise, a login attempt will be made at class initialization. Raises: ValueError: If config is not in the expected format. EnvironmentError: If default_credentials is called and environment variables are not found. """ if (config is not None and not isinstance(config, credentials.Credentials)): raise ValueError('Emailer class only supports credentials from ' 'auto_emailer.config. See ' 'auto_emailer.config.credentials and ' 'auto_emailer.config.environment_vars for help on ' 'authentication with auto-emailer library.') elif config is None: try: self._config = default_credentials() except EnvironmentError: raise EnvironmentError('Emailer only supports credentials from ' 'auto_emailer.config. Either define and ' 'pass explicitly to Emailer() or set ' 'environment_vars.') else: self._config = config self._connected = False if not delay_login: self._login() @property def connected(self): """Return: bool: If SMTP client is logged in or not. """ return self._connected
[docs] def _logout(self): """Quits the connection to the smtp client.""" if self.connected: try: self._smtp.quit() except smtplib.SMTPServerDisconnected: pass self._connected = False
[docs] def _login(self): """Uses the class attribute Emailer._config to connect to SMTP client. """ self._smtp = smtplib.SMTP(host=self._config.host, port=self._config.port) # send 'hello' to SMTP server self._smtp.ehlo() # start TLS encryption self._smtp.starttls() self._smtp.login(self._config.sender_email, self._config.password) self._connected = True
[docs] def send_email(self, message, from_addr=None, to_addrs=None, delay_send=0): """Send an email message through the SMTP client. The message may either be a string containing characters in the ASCII range, or an `auto_emailer.emailer.Message` object. If the message is a string, the smtplib delivery method will use `smtplib.sendmail`. If the message is an `auto_emailer.emailer.Message` object, the smtplib delivery method will use `smtplib.send_message`. The message is then converted to a bytestring and passes it to `smtplib.sendmail`. The arguments are the same as for sendmail, except that message is an `auto_emailer.emailer.Message` object. If from_addr is None or to_addrs is None, these arguments are taken from the headers of the message as described in RFC 2822 (a ValueError is raised if there is more than one set of 'Resent-' headers). Args: message (Union[auto_emailer.emailer.Message, str]): The message may either be a string containing characters in the ASCII range, or an `auto_emailer.emailer.Message` object. from_addr (Optional[str]): The address sending the mail. to_addrs (Optional(Sequence[str])): A list of addresses to send the email to. A bare string will be treated as a list with 1 address. delay_send (Optional[int]): If you would like to delay sending the email, pass in amount of time in seconds. Raises: ValueError: If sending a string email and from_addr or to_addr is None. ValueError: If the message is not an auto_emailer.emailer.Message object or a string. """ if not isinstance(message, Message) and isinstance(message, str): smtp_meth = 'sendmail' if (from_addr is None) or (to_addrs is None): raise ValueError('If sending string email, please provide ' 'from_addr and to_addrs.') elif isinstance(message, Message): smtp_meth = 'send_message' message = message.message else: raise ValueError('The message argument must either be an ' 'auto_emailer.emailer.Message object or a string.') # delay sending by input value if delay_send: time.sleep(delay_send) # log in to email client if not already if not self._connected: self._login() # handle disconnect and connection errors by # quick login and attempt to send again try: delivery_meth = getattr(self._smtp, smtp_meth) delivery_meth(msg=message, from_addr=from_addr, to_addrs=to_addrs) except (smtplib.SMTPConnectError, smtplib.SMTPServerDisconnected): self._login() # needs to call getattr() again once it hits # here otherwise it will fail delivery_meth = getattr(self._smtp, smtp_meth) delivery_meth(msg=message, from_addr=from_addr, to_addrs=to_addrs) finally: self._logout()
[docs]class Message: """Class representing an email message.""" def __init__(self, sender, destinations, subject=None, cc=None, bcc=None): """ Args: sender (str): Email address of the sender (from). destinations (Sequence[str]): List of string email addresses to send the email message to. subject (Optional[str]): Subject of your email message. cc (Optional(Sequence[str])): List of string email addresses to CC the email message to. bcc (Optional(Sequence[str])): List of string email addresses to BCC on the email message. """ self.sender = sender self.destinations = destinations self.subject = subject self.cc = cc or [] self.bcc = bcc or [] # create multi-part message self.message = MIMEMultipart() def __str__(self): """Override __str__ method to return message as string""" return self.message.as_string()
[docs] @staticmethod def body_template(template_path): """Opens, reads, and returns the given template text file path as a string. Args: template_path (str): File path for the email template. Returns: str: Text of file. Raises: FileNotFoundError: If cannot find the file from given `template_path`. """ try: template_text = Path(template_path).read_text() except FileNotFoundError: raise FileNotFoundError('File path not found: {}' .format(template_path)) return template_text
[docs] def draft_message(self, text=None, template_path=None, template_args=None): """Create, or draft, the `self.message` instance attribute with string text or text file templates. Return self from the instance to allow method chaining of `auto_emailer.emailer.Message.attach`. Args: text (Optional[str]): The body text of your email message. template_path (Optional[str]): File path of a text template to use for the email message body. template_args (Optional[dict]): Keyword arguments to format the email message template text. Returns: auto_emailer.emailer.Message: The instance of auto_emailer.emailer.Message. """ self.message['From'] = self.sender self.message['To'] = '; '.join(self.destinations) self.message['BCC'] = '; '.join(self.bcc) self.message['CC'] = '; '.join(self.cc) self.message['Subject'] = self.subject # check if email template is used if template_path: text = self.body_template(template_path) text = text.format(**template_args) # attach text part of message self.message.attach(MIMEText(text)) # return self to encourage method chaining return self
[docs] def attach(self, attach_files=None): """Add a sequence of files as attachments to the email message. Args: attach_files (Optional(Sequence[str])): List of string file paths to attached to message. Returns: auto_emailer.emailer.Message: The instance of auto_emailer.emailer.Message. """ # iterate through files to attach for path in attach_files or []: part = MIMEBase('application', "octet-stream") with open(path, 'rb') as file: part.set_payload(file.read()) # encode file in ASCII characters to send by email encoders.encode_base64(part) # add header to attachment part part.add_header('Content-Disposition', 'attachment', filename=os.path.basename(path)) self.message.attach(part) return self