1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2012 W. Trevor King <wking@drexel.edu>
4 # This file is part of pygrader.
6 # pygrader is free software: you can redistribute it and/or modify it under the
7 # terms of the GNU General Public License as published by the Free Software
8 # Foundation, either version 3 of the License, or (at your option) any later
11 # pygrader is distributed in the hope that it will be useful, but WITHOUT ANY
12 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
13 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License along with
16 # pygrader. If not, see <http://www.gnu.org/licenses/>.
18 from __future__ import absolute_import
20 from email.header import Header as _Header
21 from email.header import decode_header as _decode_header
22 from email.mime.message import MIMEMessage as _MIMEMessage
23 from email.mime.multipart import MIMEMultipart as _MIMEMultipart
24 import email.utils as _email_utils
25 import logging as _logging
26 import smtplib as _smtplib
28 import pgp_mime as _pgp_mime
30 from . import ENCODING as _ENCODING
31 from . import LOG as _LOG
32 from .color import standard_colors as _standard_colors
33 from .color import color_string as _color_string
34 from .color import write_color as _write_color
35 from .model.person import Person as _Person
38 def test_smtp(smtp, author, targets, msg=None):
39 """Test the SMTP connection by sending a message to `target`
42 msg = _pgp_mime.encodedMIMEText('Success!')
43 msg['Date'] = _email_utils.formatdate()
45 msg['Reply-to'] = msg['From']
46 msg['To'] = ', '.join(targets)
47 msg['Subject'] = 'Testing pygrader SMTP connection'
48 _LOG.info('send test message to SMTP server')
49 smtp.send_message(msg=msg)
50 test_smtp.__test__ = False # not a test for nose
52 def send_emails(emails, smtp=None, use_color=None, debug_target=None,
54 """Iterate through `emails` and mail them off one-by-one
56 >>> from email.mime.text import MIMEText
57 >>> from sys import stdout
59 >>> for target in ['Moneypenny <mp@sis.gov.uk>', 'M <m@sis.gov.uk>']:
60 ... msg = MIMEText('howdy!', 'plain', 'us-ascii')
61 ... msg['From'] = 'John Doe <jdoe@a.gov.ru>'
62 ... msg['To'] = target
63 ... msg['Bcc'] = 'James Bond <007@sis.gov.uk>'
66 ... lambda status: stdout.write('SUCCESS: {}\\n'.format(status))))
67 >>> send_emails(emails, use_color=False, dry_run=True)
68 ... # doctest: +REPORT_UDIFF, +NORMALIZE_WHITESPACE
69 sending message to ['Moneypenny <mp@sis.gov.uk>', 'James Bond <007@sis.gov.uk>']...\tDRY-RUN
71 sending message to ['M <m@sis.gov.uk>', 'James Bond <007@sis.gov.uk>']...\tDRY-RUN
74 local_smtp = smtp is None
75 highlight,lowlight,good,bad = _standard_colors(use_color=use_color)
76 for msg,callback in emails:
78 _email_utils.formataddr(a) for a in _pgp_mime.email_sources(msg)]
81 _email_utils.formataddr(a) for a in _pgp_mime.email_targets(msg)]
82 _pgp_mime.strip_bcc(msg)
83 if _LOG.level <= _logging.DEBUG:
84 # TODO: remove convert_content_transfer_encoding?
85 #if msg.get('content-transfer-encoding', None) == 'base64':
86 # convert_content_transfer_encoding(msg, '8bit')
87 _LOG.debug(_color_string(
88 '\n{}\n'.format(msg.as_string()), color=lowlight))
89 _write_color('sending message to {}...'.format(targets),
94 smtp = _smtplib.SMTP('localhost')
96 targets = [debug_target]
97 smtp.sendmail(author, targets, msg.as_string())
101 _write_color('\tFAILED\n', bad)
106 _write_color('\tOK\n', good)
110 _write_color('\tDRY-RUN\n', good)
114 def get_address(person, header=False):
116 >>> from pygrader.model.person import Person as Person
117 >>> p = Person(name='Jack', emails=['a@b.net'])
121 Here's a simple unicode example.
127 When you encode addresses that you intend to place in an email
128 header, you should set the `header` option to `True`. This
129 encodes the name portion of the address without encoding the email
132 >>> get_address(p, header=True)
133 '=?utf-8?b?4pyJ?= <a@b.net>'
135 Note that the address is in the clear. Without the `header`
136 option you'd have to rely on something like:
138 >>> from email.header import Header
139 >>> Header(get_address(p), 'utf-8').encode()
140 '=?utf-8?b?4pyJIDxhQGIubmV0Pg==?='
142 This can cause trouble when your mailer tries to decode the name
143 following :RFC:`2822`, which limits the locations in which encoded
147 encoding = _pgp_mime.guess_encoding(person.name)
148 if encoding == 'us-ascii':
151 name = _Header(person.name, encoding).encode()
152 return _email_utils.formataddr((name, person.emails[0]))
153 return _email_utils.formataddr((person.name, person.emails[0]))
155 def _construct_email(author, targets, subject, message, cc=None):
157 signers = [author.pgp_key]
160 recipients = [p.pgp_key for p in targets if p.pgp_key]
162 for person in targets:
163 if not person.pgp_key:
164 encrypt = False # cannot encrypt to every recipient
167 recipients.extend([p.pgp_key for p in cc if p.pgp_key])
169 if not person.pgp_key:
173 encrypt = False # noone to encrypt to
174 if signers and encrypt:
175 if author.pgp_key not in recipients:
176 recipients.append(author.pgp_key)
177 message = _pgp_mime.sign_and_encrypt(
178 message=message, signers=signers, recipients=recipients,
181 message = _pgp_mime.sign(message=message, signers=signers)
183 message = _pgp_mime.encrypt(message=message, recipients=recipients)
185 message['Date'] = _email_utils.formatdate()
186 message['From'] = get_address(author, header=True)
187 message['Reply-to'] = message['From']
188 message['To'] = ', '.join(
189 get_address(target, header=True) for target in targets)
191 message['Cc'] = ', '.join(
192 get_address(target, header=True) for target in cc)
193 subject_encoding = _pgp_mime.guess_encoding(subject)
194 if subject_encoding == 'us-ascii':
195 message['Subject'] = subject
197 message['Subject'] = _Header(subject, subject_encoding)
201 def construct_email(author, targets, subject, text, cc=None):
202 r"""Build a text/plain email using `Person` instances
204 >>> from pygrader.model.person import Person as Person
205 >>> author = Person(name='Джон Доу', emails=['jdoe@a.gov.ru'])
206 >>> targets = [Person(name='Jill', emails=['c@d.net'])]
207 >>> cc = [Person(name='H.D.', emails=['hd@wall.net'])]
208 >>> msg = construct_email(author, targets, cc=cc,
209 ... subject='Once upon a time', text='Bla bla bla...')
210 >>> print(msg.as_string()) # doctest: +REPORT_UDIFF, +ELLIPSIS
211 Content-Type: text/plain; charset="us-ascii"
213 Content-Transfer-Encoding: 7bit
214 Content-Disposition: inline
216 From: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>
217 Reply-to: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>
219 Cc: "H.D." <hd@wall.net>
220 Subject: Once upon a time
226 >>> msg = construct_email(author, targets, cc=cc,
227 ... subject='Once upon a time', text='Funky ✉.')
228 >>> print(msg.as_string()) # doctest: +REPORT_UDIFF, +ELLIPSIS
229 Content-Type: text/plain; charset="utf-8"
231 Content-Transfer-Encoding: base64
232 Content-Disposition: inline
234 From: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>
235 Reply-to: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>
237 Cc: "H.D." <hd@wall.net>
238 Subject: Once upon a time
243 message = _pgp_mime.encodedMIMEText(text)
244 return _construct_email(
245 author=author, targets=targets, subject=subject, message=message,
248 def construct_response(author, targets, subject, text, original, cc=None):
249 r"""Build a multipart/mixed response email using `Person` instances
251 >>> from pygrader.model.person import Person as Person
252 >>> student = Person(name='Джон Доу', emails=['jdoe@a.gov.ru'])
253 >>> assistant = Person(name='Jill', emails=['c@d.net'])
255 >>> msg = construct_email(author=student, targets=[assistant],
256 ... subject='Assignment 1 submission', text='Bla bla bla...')
257 >>> rsp = construct_response(author=assistant, targets=[student],
258 ... subject='Received assignment 1 submission', text='3 hours late',
260 >>> print(rsp.as_string()) # doctest: +REPORT_UDIFF, +ELLIPSIS
261 Content-Type: multipart/mixed; boundary="===============...=="
265 Reply-to: Jill <c@d.net>
266 To: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>
267 Subject: Received assignment 1 submission
269 --===============...==
270 Content-Type: text/plain; charset="us-ascii"
272 Content-Transfer-Encoding: 7bit
273 Content-Disposition: inline
276 --===============...==
277 Content-Type: message/rfc822
280 Content-Type: text/plain; charset="us-ascii"
282 Content-Transfer-Encoding: 7bit
283 Content-Disposition: inline
285 From: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>
286 Reply-to: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>
288 Subject: Assignment 1 submission
291 --===============...==--
293 message = _MIMEMultipart('mixed')
294 message.attach(_pgp_mime.encodedMIMEText(text))
295 message.attach(_MIMEMessage(original))
296 return _construct_email(
297 author=author, targets=targets, subject=subject, message=message,