1#!/usr/bin/env python3
  2
  3"""Utility functions for sending email."""
  4
  5from typing import List
  6from typing import Union
  7import re
  8import logging
  9import smtplib
 10from email.mime.multipart import MIMEMultipart
 11from email.mime.text import MIMEText
 12from email.mime.base import MIMEBase
 13from email.encoders import encode_base64
 14
 15from chart.project import settings
 16from chart.common.traits import is_listlike
 17
 18logger = logging.getLogger()
 19
 20reply_to = 'noreply@eumetsat.int'
 21
 22template = """From: {from_address[0]}<{from_address[1]}>
 23To: {to_addresses}
 24Reply-to: {reply_to}
 25Subject: {subject}
 26{message}
 27"""
 28
 29
 30class EmailRecipient():
 31    """Details of a target address for emails."""
 32
 33    # A Role object works as well
 34    def __init__(self, name=None, email=None):
 35        """Args:
 36            - name: Just the name, or "Name <email>"
 37            - email: Email address
 38        """
 39        self.name = name
 40        self.email = email
 41
 42
 43class Email:
 44    """Represent an email message."""
 45
 46    def __init__(self,
 47                 subject: str = None,
 48                 body: str = None,
 49                 sender: str = None):
 50        self.subject = subject
 51        self.body = body
 52        self.sender = sender
 53
 54    def __str__(self):
 55        return 'Email(from:{sender},subject:{subject},body:{body})'.format(
 56            sender=split_name_address(self.sender)[0],
 57            subject=self.subject,
 58            body=self.body[:500].replace('\n', '\\n'))
 59
 60    def send(self, to: Union[str, List[Union[object, str]]]) -> None:
 61        """Send ourselves to one or more people.
 62
 63        `to` can be strings or EmailRecipient or Role objects, or anything with a
 64        email member and optionally a name member.
 65        """
 66        if not is_listlike(to):
 67            to = [to]
 68
 69        sendmail(from_address=self.sender,
 70                 to_addresses=[(t.name, t.email) for t in to],
 71                 subject=self.subject,
 72                 message=self.body)
 73
 74
 75def make_reciepient_string(recipient):
 76    """Given a tuple of optionally a name plus an email address, create a single recipient string.
 77
 78    recipient can either look like ('a.person', 'a.person@example.com')
 79    or (None, 'a.person@example.com').
 80
 81    This function and other algorithms should probably be converted into a Recipient class
 82    to handle all this in a more organised way.
 83    """
 84    if recipient[0] is None:
 85        return recipient[1]
 86
 87    return '{name} <{email}>'.format(name=recipient[0], email=recipient[1])
 88
 89
 90def sendmail(from_address, to_addresses, subject, message):
 91    """Send an email using the built-in SMTP library.
 92
 93    Args:
 94        - from_address (tuple of str or str): Can either be a tuple of (name,address) or a string
 95            containing just the address or a string in format 'Name<Address>'.
 96        - to_addresses (tuple of str): Should be an iterable of tuples of (name,address).
 97        - subject (str): Should be a 1-line text string.
 98        - content (str): Can be a long block of text.
 99
100    """
101    if isinstance(message, dict) and message['type'] == 'html':
102        sendmail_html(from_address, to_addresses, subject, message['content'])
103
104    if isinstance(from_address, str):
105        from_address = split_name_address(from_address)
106
107    if len(to_addresses) == 0:
108        logger.info('Not sending email from {from_name}<{from_addr}> to [] re "{subject}"'.format(
109                from_name=from_address[0],
110                from_addr=from_address[1],
111                subject=subject))
112        return
113
114    logger.info('Sending email from {from_name}<{from_addr}> to {to_addr} re "{subject}"'.format(
115            from_name=from_address[0],
116            from_addr=from_address[1],
117            to_addr=','.join(x[1] for x in to_addresses),
118            subject=subject))
119
120    server = smtplib.SMTP(settings.EMAIL_HOST, settings.EMAIL_PORT)
121    # server.set_debuglevel(1)
122    server.sendmail(from_address[1],
123                    [x[1] for x in to_addresses],
124                    template.format(from_address=from_address,
125                                    to_addresses=', '.join(
126                                        make_reciepient_string(t) for t in to_addresses),
127                                    reply_to=reply_to,
128                                    subject=subject,
129                                    message=message))
130    # logger.info('Sendmail to {rec} people'.format(rec=len(to_addresses)))
131
132
133email_styles = """<style>
134body {font-family: consolas, monospace; font-size: 0.87em;}
135h1 {font-size: 1.4em; color: #0000ca;}
136h2 {font-size: 1.2em; color: #0000ca;}
137table {font-family: consolas, monospace;}
138table td {text-align:left;}
139table th {text-align:left;}
140span.PENDING {color:#0000cd;}
141span.COMPLETED {color:#006400;}
142span.FAILED {color:#b22222;}
143span.RETRY {color:#0000cd;}
144span.TIMEOUT {color:#0000cd;}
145</style>"""
146
147
148def sendmail_html(from_address, to_addresses, subject, html):
149    """Send an email using the build in Python SMTP library.
150
151    Args:
152        `from_address` (tuple of str or str):
153            Can either be a tuple of (name,address) or a string containing
154            just the address or a string in format 'Name<Address>'.
155
156        `to_addresses` (tuple of str):
157            Should be an iterable of tuples of (name,address).
158
159        `subject` (str):
160            Should be a 1-line text string.
161
162        `content` (str):
163            Can be a long block of text.
164    """
165
166    # See http://www.stevecoursen.com/674/sending-html-email-from-python-2
167    # for a method of supplying plain text also
168
169    if isinstance(from_address, str):
170        from_address = split_name_address(from_address)
171
172    message = MIMEMultipart('alternative')
173    # message = MIMEText(html)
174    message['Subject'] = subject
175    # message['From'] = from_address[1]
176    message['From'] = from_address[0] + '<' + from_address[1] + '>'
177
178    message.attach(MIMEText(email_styles + '<body>' + html + '</body>', 'html'))
179    # message.attach(MIMEText(text, 'plain'))
180
181    server = smtplib.SMTP(settings.EMAIL_HOST, settings.EMAIL_PORT)
182    # server.set_debuglevel(1)
183
184    for to_address in to_addresses:
185        logger.info('Sending email from {from_addr[0]}<{from_addr[1]}> '
186                     'to {to_addr[0]}<{to_addr[1]}> subject {subject}'.format(
187                from_addr=from_address,
188                to_addr=to_address,
189                subject=subject))
190
191        # message['To'] = to_address[1]
192        message['To'] = to_address[0] + '<' + to_address[1] + '>'
193
194        # print message.as_string()
195
196        server.sendmail(from_address[1],
197                        [to_address[1]],
198                        message.as_string())
199
200    server.quit()
201
202
203def sendmail_attachment(
204        from_address,  # :User
205        to_addresses,  # :Iterable[User]
206        subject:str,
207        attachment_name:str,
208        attachment:bytes,
209        body:str) -> None:
210    """Send mail with file attachment attachment.
211
212    This function should be combined with sendmail() and sendmail_html().
213    """
214    part = MIMEBase('application', 'octet-stream')
215
216    if isinstance(attachment, str):
217        part.set_payload(open(attachment, 'rb').read())
218        part.set_payload(part.get_payload().decode('ASCII'))  # without this line attachment is binary
219
220    else:
221        part.set_payload(attachment)
222
223    encode_base64(part)
224
225    part.add_header('Content-Disposition', 'attachment; filename="{filename}"'.format(
226        filename=attachment_name))
227
228    if isinstance(from_address, str):
229        from_address = split_name_address(from_address)
230
231    message = MIMEMultipart('alternative')
232    # message = MIMEText(html)
233    message['Subject'] = subject
234
235    # message['From'] = from_address[1]
236    message['From'] = from_address[0] + '<' + from_address[1] + '>'
237
238    # Email body text
239    part_text = MIMEText(body, 'plain')
240    message.attach(part_text)
241    message.attach(part)
242
243    server = smtplib.SMTP(settings.EMAIL_HOST, settings.EMAIL_PORT)
244    # server.set_debuglevel(1)
245
246    for to_address in to_addresses:
247        logger.info('Sending email from {from_addr[0]}<{from_addr[1]}> '
248                     'to {to_addr[0]}<{to_addr[1]}> subject {subject}'.format(
249                from_addr=from_address,
250                to_addr=to_address,
251                subject=subject))
252
253        # message['To'] = to_address[1]
254        message['To'] = to_address[0] + '<' + to_address[1] + '>'
255
256        server.sendmail(from_address[1],
257                        [to_address[1]],
258                        message.as_string())
259
260    server.quit()
261
262
263def split_name_address(instr):
264    """Split email addresses in form "name<address>" into a tuple."""
265    # TBD: Merge with better regex in roles.py and combine to single function
266    # and rename to decode_email_name and return an object with named
267    # attributes 'name' and 'email' and a bool for if they are split
268    match = re.match(r'([^<]+?)\s*<([^>]+)>', instr)
269    if match is None:
270        return (instr, instr)
271
272    return (match.groups(0)[0], match.groups(0)[1])
273
274# def sendmail(to_address, message):
275#     try:
276#         child = subprocess.Popen(['sendmail',to_address],
277#                                  stdin=subprocess.PIPE,
278#                                  stderr=subprocess.PIPE,
279#                                  stdout=subprocess.PIPE#,
280#                                  #shell=True
281#                                  )
282#     except OSError:
283#         logger.error('Could not find sendmail executable')
284#         return
285
286#     child.stdin.write(message)
287#     out,err = child.communicate()
288#     if child.returncode!=0:
289#         for line in err.split('\n'):
290#             logger.error(line)
291
292#         for line in out.split('\n'):
293#             logger.info(line)
294
295#     logger.info('sendmail to '+to_address+' returned '+str(child.returncode))