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))