Source code for sarracenia.flowcb.send.email

"""
Email Sender
============

``sarracenia.flowcb.send.email.Email`` is an sr3 sender plugin. It will send the *contents* of a 
file in the *body* of an email to the configured recipient(s).

The email subject will be the name of the file being sent.

Usage:
^^^^^^

    1. In the config file, include the following line: ::

        callback send.email

    And define the email server ::

        sendTo

    2. Define the email server (required) using the ``sendTo`` option, and the sender's email address (optional)
       in the config file: ::

        sendTo      smtp://email.relay.server.ca

        email_from  santa@canada.ca

        # or, with a "human readable" sender name:

        email_from  Santa Claus <santa@canada.ca>
    
    3. Configure recipients using accept statements. You must have at least one recipient per accept statement.
       Multiple recipients can be specified by separating each address by a comma. ::
    
        accept .*AACN27.* test@example.com
        accept .*SXCN.*   user1@example.com, user2@example.com
        accept .*CACN.* DESTFN=A_CACN_Bulletin  me@ssc-spc.gc.ca,you@ssc-spc.gc.ca,someone@ssc-spc.gc.ca

To change the filename that is sent in the subject, you can use the filename option, a renamer plugin or
DESTFN/DESTFNSCRIPT on a per-accept basis. The ``email_subject_prepend`` option can be used to add text before
the filename in the email subject. For example: ::

    email_subject_prepend  Sent by Sarracenia: 

There is also the option of sending a file as an attachment instead of embedding its contents in the email.
To do this, there are two options that can be used.
`` email_attachment `` is a boolean value to specify if you want to send files as attachments
`` email_attachment_text `` is the optional text that can be added inside of the email content, with the attached file.
    
    email_attachment True
    email_attachment_text Attached in this email is data coming from XXX


Future Improvement Ideas:
  - SMTP on different ports and with authentication
    
Original Author: Wahaj Taseer - June, 2019
"""

from email.message import EmailMessage
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
from email.mime.text import MIMEText
import mimetypes
import logging
import os.path
import re
from sarracenia.flowcb import FlowCB
import smtplib

logger = logging.getLogger(__name__)


[docs] class Email(FlowCB):
[docs] def __init__(self, options): super().__init__(options,logger) self.o.add_option('email_from', 'str', default_value='') self.o.add_option('email_subject_prepend', 'str', default_value='') self.o.add_option('email_attachment', 'flag', default_value=False) self.o.add_option('email_attachment_text', 'str', default_value='') # Parse accept/reject mask arguments into email recipient lists try: for mask in self.o.masks: # mask[4] == True if accept, False if reject, only need to parse for accept if len(mask[-1]) > 0 and mask[4]: # logger.debug(f"mask args before parse: {mask[-1]}") arg_string = ''.join(mask[-1]).replace(' ', '').strip() recipients = arg_string.split(',') mask[-1].clear() for recipient in recipients: if '@' not in recipient: logger.error(f"Invalid email recipient: {recipient} for accept {mask[0]}") else: mask[-1].append(recipient) # logger.debug(f"mask args after parse: {mask[-1]}") elif mask[4]: logger.warning(f"No email recipients defined for accept {mask[0]}") except Exception as e: logger.critical(f"Failed to parse recipients from mask: {mask}") raise e # Server must be defined if not self.o.sendTo or len(self.o.sendTo) == 0: raise Exception("No email server (sendTo) is defined in the config!") # sendTo --> email_server self.email_server = self.o.sendTo.strip('/') if '//' in self.email_server: self.email_server = self.email_server[self.email_server.find('//') + 2 :] logger.debug(f"Using email server: {self.email_server} (sendTo was: {self.o.sendTo})") # Add trailing space to email_subject_prepend if len(self.o.email_subject_prepend) > 0: self.o.email_subject_prepend += ' '
[docs] def after_work(self, worklist): """ This plugin can also be used in a sarra/subscriber, mostly for testing purposes. """ if self.o.component != 'sender': for msg in worklist.ok: actual_baseDir = self.o.baseDir actual_relPath = msg['relPath'] msg['relPath'] = os.path.join(msg['new_dir'], msg['new_file']) self.o.baseDir = msg['new_dir'] self.send(msg) self.o.baseDir = actual_baseDir msg['relPath'] = actual_relPath
[docs] def send(self, msg): """ Send an email to each recipient defined in the config file for a particular accept statement. The file contents are sent in the body of the email. The subject is the filename. """ if not msg['relPath'].startswith(self.o.baseDir): ipath = os.path.normpath(f"{self.o.baseDir}/{msg['relPath']}") else: ipath = os.path.normpath(f"{msg['relPath']}") if '_mask_index' not in msg: logger.error("Recipients unknown, can't email file {ipath}") # negative return == permanent failure, don't retry return -1 # Get list of recipients for this message, from the mask that matched the filename/path recipients = self.o.masks[msg['_mask_index']][-1] # i.e. (type/subtype, encoding) file_type = mimetypes.guess_type(ipath) # Prepare the email message try: # Build a non-text email message for the attachment if specified or if the file type can be deemed to be an image. if self.o.email_attachment or (len(file_type) > 0 and file_type[0] and 'image' in file_type[0]): emsg = MIMEMultipart() emsg_text = MIMEText(f"{self.o.email_attachment_text}") # Add the attachment text that will be paired with the attachment data emsg.attach(emsg_text) with open(ipath, 'rb') as fp: attachment_data = fp.read() attachment = MIMEApplication(attachment_data, name=os.path.basename(ipath)) # Add the attachment data to the email emsg.attach(attachment) else: emsg = EmailMessage() with open(ipath) as fp: emsg.set_content(fp.read()) except Exception as e: logger.error(f"Failed to read {ipath}, can't send to {recipients}") logger.debug('Exception details:', exc_info=True) # No retry if the file doesn't exist return -1 emsg['Subject'] = self.o.email_subject_prepend + msg['new_file'] # if not set in the config, just don't set From, the From address will usually be derived from the hostname if self.o.email_from and len(self.o.email_from) > 0: emsg['From'] = self.o.email_from # if sending to any one recipient fails, we will return False, triggering a retry. all_ok = True for recipient in recipients: if '@' not in recipient: logger.error(f"Cannot send {ipath} to recipient {recipient}. Email address is invalid!") continue try: logstr = f"file {ipath} to {recipient} with subject {emsg['Subject']}" logger.debug(f'sending {logstr} from {self.o.email_from} using server {self.email_server}') if 'To' in emsg: del emsg['To'] emsg['To'] = recipient logger.debug(emsg) s = smtplib.SMTP(self.email_server) s.send_message(emsg) s.quit() logger.info(f'Sent file {logstr}') except Exception as e: logger.error(f'failed to send {logstr} from {self.o.email_from} using server {self.email_server}' + f' because {e}') logger.debug('Exception details:', exc_info=True) all_ok = False return all_ok