359 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			359 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import mimetypes
 | 
						|
from email import encoders
 | 
						|
from email import message_from_bytes
 | 
						|
from email.header import Header
 | 
						|
from email.mime.base import MIMEBase
 | 
						|
from email.mime.multipart import MIMEMultipart
 | 
						|
from email.mime.text import MIMEText
 | 
						|
from email.utils import formataddr, formatdate
 | 
						|
from smtplib import SMTP, SMTP_SSL, SMTPServerDisconnected
 | 
						|
 | 
						|
import mistune
 | 
						|
from bs4 import BeautifulSoup
 | 
						|
from os.path import basename
 | 
						|
 | 
						|
class SMTPMailer:
 | 
						|
    """
 | 
						|
    Wrapper around `smtplib.SMTP` class, for managing a SMTP client.
 | 
						|
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    host : str
 | 
						|
        The hostname of the SMTP server to connect to.
 | 
						|
    port : int, optional
 | 
						|
        The port number of the SMTP server to connect to.
 | 
						|
    user : str, optional
 | 
						|
        The username to be used for authentication to the SMTP server.
 | 
						|
    password : str, optional
 | 
						|
        The password to be used for authentication to the SMTP server.
 | 
						|
    ssl : bool, optional
 | 
						|
        Whether to use SSL for the SMTP connection.
 | 
						|
    tls : bool, optional
 | 
						|
        Whether to use TLS // `starttls` for the SMTP connection.
 | 
						|
    keyfile : str, optional
 | 
						|
        File containing the SSL private key.
 | 
						|
    certfile : str, optional
 | 
						|
        File containing the SSL certificate in PEM format.
 | 
						|
    context: `ssl.SSLContext` object
 | 
						|
        The SSL context to be used in the SSL connection.
 | 
						|
 | 
						|
    Attributes
 | 
						|
    ----------
 | 
						|
    client: `smtplib.SMTP` object
 | 
						|
        The SMTP client that'd be used to send emails.
 | 
						|
    connected: bool
 | 
						|
        Whether there is an active SMTP connection.
 | 
						|
    host : str
 | 
						|
        The hostname of the SMTP server to connect to.
 | 
						|
    port : int
 | 
						|
        The port number of the SMTP server to connect to.
 | 
						|
    user : str
 | 
						|
        The username to be used for authentication to the SMTP server.
 | 
						|
    password : str
 | 
						|
        The password to be used for authentication to the SMTP server.
 | 
						|
    ssl : bool
 | 
						|
        Whether to use SSL for the SMTP connection.
 | 
						|
    tls : bool
 | 
						|
        Whether to use TLS // `starttls` for the SMTP connection.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, host, port=None, user=None, password=None, ssl=False, tls=False, **kwargs):
 | 
						|
        self.host = host
 | 
						|
        self.ssl = ssl
 | 
						|
        self.tls = tls
 | 
						|
        if ssl:
 | 
						|
            self.port = port or 465
 | 
						|
        elif tls:
 | 
						|
            self.port = port or 587
 | 
						|
        else:
 | 
						|
            self.port = port or 25
 | 
						|
        if kwargs is not None:
 | 
						|
            self.__ssloptions = dict()
 | 
						|
            for key in ['keyfile', 'certfile', 'context']:
 | 
						|
                self.__ssloptions[key] = kwargs.get(key, None)
 | 
						|
        self.user = user
 | 
						|
        self.password = password
 | 
						|
        self.connected = False
 | 
						|
        self.client = None
 | 
						|
 | 
						|
    def connect(self):
 | 
						|
        """
 | 
						|
        Create the SMTP connection.
 | 
						|
        """
 | 
						|
        self.client = SMTP(self.host, self.port) if not self.ssl else SMTP_SSL(self.host, self.port,
 | 
						|
                                                                               **self.__ssloptions)
 | 
						|
        self.client.ehlo()
 | 
						|
        if self.tls:
 | 
						|
            self.client.starttls(**self.__ssloptions)
 | 
						|
            self.client.ehlo()
 | 
						|
        if self.user and self.password:
 | 
						|
            self.client.login(self.user, self.password)
 | 
						|
        self.connected = True
 | 
						|
 | 
						|
    def __enter__(self):
 | 
						|
        return self
 | 
						|
 | 
						|
    def close(self):
 | 
						|
        """
 | 
						|
        Close the SMTP connection and `quit` the `self.client` object.
 | 
						|
        """
 | 
						|
        if self.connected:
 | 
						|
            try:
 | 
						|
                self.client.quit()
 | 
						|
            except SMTPServerDisconnected:
 | 
						|
                pass
 | 
						|
        self.connected = False
 | 
						|
 | 
						|
    def __exit__(self, exc_type, exc_val, exc_tb):
 | 
						|
        self.close()
 | 
						|
        return False
 | 
						|
 | 
						|
    def __del__(self):
 | 
						|
        self.close()
 | 
						|
 | 
						|
    def send(self, message, sender=None, receivers=None):
 | 
						|
        """
 | 
						|
        Send an email through this SMTP client.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        message : `drymail.Message` object
 | 
						|
            The message to be sent.
 | 
						|
        sender : str, optional
 | 
						|
            The email address of the sender.
 | 
						|
        receivers : list of str, optional
 | 
						|
            The email addresses of the receivers // recipients.
 | 
						|
        """
 | 
						|
        if not message.prepared:
 | 
						|
            message.prepare()
 | 
						|
        if not self.connected:
 | 
						|
            self.connect()
 | 
						|
        self.client.send_message(message.message, from_addr=sender, to_addrs=receivers)
 | 
						|
 | 
						|
 | 
						|
def stringify_address(address):
 | 
						|
    """
 | 
						|
    Converts an address into a string in the `"John Doe" <john@example.com>"` format, which can be directly used in the
 | 
						|
    headers of an email.
 | 
						|
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    address : str or (str, str)
 | 
						|
        An address. Can be either the email address or a tuple of the name
 | 
						|
        and the email address.
 | 
						|
 | 
						|
    Returns
 | 
						|
    -------
 | 
						|
    str
 | 
						|
        Address as a single string, in the `"John Doe" <john@example.com>"` format. Returns
 | 
						|
        `address` unchanged if it's a single string.
 | 
						|
    """
 | 
						|
    address = ('', address) if isinstance(address, str) else address
 | 
						|
    return formataddr((str(Header(address[0], 'utf-8')), address[1]))
 | 
						|
 | 
						|
 | 
						|
def stringify_addresses(addresses):
 | 
						|
    """
 | 
						|
    Converts a list of addresses into a string in the
 | 
						|
    `"John Doe" <john@example.com>, "Jane" <jane@example.com>"` format,
 | 
						|
    which can be directly used in the headers of an email.
 | 
						|
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    addresses : (str or (str, str)) or list of (str or (str, str))
 | 
						|
        A single address or a list of addresses which is to be converted into a single string. Each element can be
 | 
						|
        either an email address or a tuple of a name and an email address.
 | 
						|
 | 
						|
    Returns
 | 
						|
    -------
 | 
						|
    str
 | 
						|
        The address(es) as a single string which can be directly used in the headers of an email.
 | 
						|
    """
 | 
						|
    if isinstance(addresses, list):
 | 
						|
        addresses = [stringify_address(address) for address in addresses]
 | 
						|
        return ', '.join(addresses)
 | 
						|
    else:
 | 
						|
        return stringify_address(addresses)
 | 
						|
 | 
						|
 | 
						|
class Message:
 | 
						|
    """
 | 
						|
    Class representing an email message.
 | 
						|
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    sender : str or (str, str)
 | 
						|
        The address of the sender. Can be either the email address or a tuple of the name and the email address.
 | 
						|
    receivers : list of (str or (str, str))
 | 
						|
        The list of receivers // recipients. Each element can be either an email address or a tuple of a name and an
 | 
						|
        email address.
 | 
						|
    subject : str, optional
 | 
						|
        The subject of the email
 | 
						|
    authors : list of (str or (str, str)), optional
 | 
						|
        The list of authors, to be mentioned in the `Authors` header. Each
 | 
						|
        element can be either an email address or a tuple of a name and an email address.
 | 
						|
    cc : list of (str or (str, str)), optional
 | 
						|
        The list of addresses to CC to. Each element can be either an email
 | 
						|
        address or a tuple of a name and an email address.
 | 
						|
    bcc : list of (str or (str, str)), optional
 | 
						|
        The list of addresses to BCC to. Each element can be either an email address or a tuple of a name and an email
 | 
						|
        address.
 | 
						|
    reply_to : list of (str or (str, str)), optional
 | 
						|
        The list of addresses to mention in the `Reply-To` header. Each element can be either an email address or a
 | 
						|
        tuple of a name and an email address.
 | 
						|
    headers : dict, optional
 | 
						|
        Custom headers as key-value pairs, to be injected into the email.
 | 
						|
    text: str, optional
 | 
						|
        The body of the message, as plaintext. At least one among `text` and `html`
 | 
						|
        must be provided.
 | 
						|
    html: str, optional
 | 
						|
        The body of the message, as HTML. At least one among `text` and `html`
 | 
						|
        must be provided.
 | 
						|
    prepared_message: bytes, optional
 | 
						|
        A prepared email as bytes. If this is provided, all the other optional parameters will be ignored.
 | 
						|
 | 
						|
    Attributes
 | 
						|
    ----------
 | 
						|
    message: `email.message.Message` object or `email.mime.multipart.MIMEMultipart` object
 | 
						|
        The prepared message object.
 | 
						|
    prepared: bool
 | 
						|
        Whether the message is prepared, in other words whether `self.message` is available and proper.
 | 
						|
    sender : str or (str, str)
 | 
						|
        The address of the sender. Can be either the email address or a tuple of the name and the email address.
 | 
						|
    receivers : list of (str or (str, str))
 | 
						|
        The list of receivers // recipients. Each element can be either an email address or a tuple of a name and an
 | 
						|
        email address.
 | 
						|
    subject : str
 | 
						|
        The subject of the email
 | 
						|
    authors : list of (str or (str, str))
 | 
						|
        The list of authors, to be mentioned in the `Authors` header. Each element can be either an email address or a
 | 
						|
        tuple of a name and an email address.
 | 
						|
    cc : list of (str or (str, str))
 | 
						|
        The list of addresses to CC to. Each element can be either an email address or a tuple of a name and an email
 | 
						|
        address.
 | 
						|
    bcc : list of (str or (str, str))
 | 
						|
        The list of addresses to BCC to. Each element can be either an email address or a tuple of a name and an email
 | 
						|
        address.
 | 
						|
    reply_to : list of (str or (str, str))
 | 
						|
        The list of addresses to mention in the `Reply-To` header. Each element can be either an email address or a
 | 
						|
        tuple of a name and an email address.
 | 
						|
    headers : dict
 | 
						|
        Custom headers as key-value pairs, to be injected into the email.
 | 
						|
    text: str
 | 
						|
        The body of the message, as plaintext.
 | 
						|
    html: str
 | 
						|
        The body of the message, as HTML.
 | 
						|
    prepared_message: bytes
 | 
						|
        A prepared email as bytes.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, sender, receivers, subject=None, authors=None, cc=None, bcc=None, reply_to=None, headers=None,
 | 
						|
                 text=None, html=None, prepared_message=None):
 | 
						|
        self.subject = subject or ''
 | 
						|
        self.sender = sender
 | 
						|
        self.receivers = receivers
 | 
						|
        self.authors = authors
 | 
						|
        self.cc = cc
 | 
						|
        self.bcc = bcc
 | 
						|
        self.headers = headers
 | 
						|
        self.reply_to = reply_to
 | 
						|
        self.text = text or ''
 | 
						|
        self.html = html or ''
 | 
						|
        self.__attachments = []
 | 
						|
        self.__attachments_data = []
 | 
						|
        self.prepared_message = prepared_message
 | 
						|
        self.prepared = False
 | 
						|
        self.message = MIMEMultipart('mixed')
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        if not self.prepared:
 | 
						|
            self.prepare()
 | 
						|
        return self.message.as_string()
 | 
						|
 | 
						|
    @property
 | 
						|
    def attachments(self):
 | 
						|
        """
 | 
						|
        All the attachments attached to the message.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        list of str
 | 
						|
            The filenames of the attachments attached.
 | 
						|
        """
 | 
						|
        return self.__attachments
 | 
						|
 | 
						|
    def attach(self, filename, data=None, mimetype=None):
 | 
						|
        """
 | 
						|
        Add a file as attachment to the email.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        filename : str
 | 
						|
            If data is provoded:
 | 
						|
                The filename of the file to be attached.
 | 
						|
            If data not provoded:
 | 
						|
                The full name (path + filename) of the
 | 
						|
                file to be attached.
 | 
						|
        data: bytes, optional
 | 
						|
            The raw content of the file to be attached.
 | 
						|
        mimetype : str, optional
 | 
						|
            The MIMEType of the file to be attached.
 | 
						|
        """
 | 
						|
        if self.prepared_message:
 | 
						|
            return
 | 
						|
 | 
						|
        filename_only = basename(filename)
 | 
						|
        if not mimetype:
 | 
						|
            mimetype, encoding = mimetypes.guess_type(filename)
 | 
						|
            if mimetype is None or encoding is not None:
 | 
						|
                mimetype = 'application/octet-stream'
 | 
						|
        if data:
 | 
						|
            maintype, subtype = mimetype.split('/', 1)
 | 
						|
            attachment = MIMEBase(maintype, subtype)
 | 
						|
            attachment.set_payload(data)
 | 
						|
            encoders.encode_base64(attachment)
 | 
						|
            attachment.add_header('Content-Disposition', 'attachment', filename=filename_only)
 | 
						|
            self.message.attach(attachment)
 | 
						|
        else:
 | 
						|
            self.__attachments_data.append([filename, mimetype])
 | 
						|
        if filename_only not in self.__attachments:
 | 
						|
            self.__attachments.append(filename_only)
 | 
						|
 | 
						|
    def prepare(self):
 | 
						|
        """
 | 
						|
        Prepare the `self.message` object.
 | 
						|
        """
 | 
						|
        if self.prepared_message:
 | 
						|
            self.message = message_from_bytes(self.prepared_message)
 | 
						|
            self.prepared = True
 | 
						|
            return
 | 
						|
 | 
						|
        self.text = self.text or BeautifulSoup(self.html, 'html.parser').get_text(strip=True)
 | 
						|
        self.html = self.html or mistune.markdown(self.text)
 | 
						|
 | 
						|
        self.message['Sender'] = stringify_address(self.sender)
 | 
						|
        self.message['From'] = stringify_addresses(self.authors) if self.authors else stringify_address(self.sender)
 | 
						|
        self.message['To'] = stringify_addresses(self.receivers)
 | 
						|
        self.message['Subject'] = self.subject
 | 
						|
        self.message["Date"] = formatdate(localtime=True)
 | 
						|
        if self.cc:
 | 
						|
            self.message['CC'] = stringify_addresses(self.cc)
 | 
						|
        if self.bcc:
 | 
						|
            self.message['BCC'] = stringify_addresses(self.bcc)
 | 
						|
        if self.reply_to:
 | 
						|
            self.message['Reply-To'] = stringify_addresses(self.reply_to)
 | 
						|
        if self.headers:
 | 
						|
            for key, value in self.headers.items():
 | 
						|
                self.message[key] = value
 | 
						|
 | 
						|
        body = MIMEMultipart('alternative')
 | 
						|
        plaintext_part = MIMEText(self.text, 'plain')
 | 
						|
        html_part = MIMEText(self.html, 'html')
 | 
						|
        body.attach(plaintext_part)
 | 
						|
        body.attach(html_part)
 | 
						|
        self.message.attach(body)
 | 
						|
        if self.__attachments_data:
 | 
						|
            for attachment_data in self.__attachments_data:
 | 
						|
                with open(attachment_data[0], 'rb') as a_file:
 | 
						|
                    self.attach(filename=basename(attachment_data[0]), data=a_file.read(), mimetype=attachment_data[1])
 | 
						|
        self.prepared = True
 |