1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2012 W. Trevor King <wking@tremily.us>
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 .model.person import Person as _Person
35 def test_smtp(smtp, author, targets, msg=None):
36 """Test the SMTP connection by sending a message to `target`
39 msg = _pgp_mime.encodedMIMEText('Success!')
40 msg['Date'] = _email_utils.formatdate()
42 msg['Reply-to'] = msg['From']
43 msg['To'] = ', '.join(targets)
44 msg['Subject'] = 'Testing pygrader SMTP connection'
45 _LOG.info('send test message to SMTP server')
46 smtp.send_message(msg=msg)
47 test_smtp.__test__ = False # not a test for nose
49 def send_emails(emails, smtp=None, debug_target=None, dry_run=False):
50 """Iterate through `emails` and mail them off one-by-one
52 >>> from email.mime.text import MIMEText
53 >>> from sys import stdout
55 >>> for target in ['Moneypenny <mp@sis.gov.uk>', 'M <m@sis.gov.uk>']:
56 ... msg = MIMEText('howdy!', 'plain', 'us-ascii')
57 ... msg['From'] = 'John Doe <jdoe@a.gov.ru>'
58 ... msg['To'] = target
59 ... msg['Bcc'] = 'James Bond <007@sis.gov.uk>'
62 ... lambda status: stdout.write('SUCCESS: {}\\n'.format(status))))
63 >>> send_emails(emails, dry_run=True)
64 ... # doctest: +REPORT_UDIFF, +NORMALIZE_WHITESPACE
68 local_smtp = smtp is None
69 for msg,callback in emails:
71 _email_utils.formataddr(a) for a in _pgp_mime.email_sources(msg)]
74 _email_utils.formataddr(a) for a in _pgp_mime.email_targets(msg)]
75 _pgp_mime.strip_bcc(msg)
76 if _LOG.level <= _logging.DEBUG:
77 # TODO: remove convert_content_transfer_encoding?
78 #if msg.get('content-transfer-encoding', None) == 'base64':
79 # convert_content_transfer_encoding(msg, '8bit')
80 _LOG.debug('\n{}\n'.format(msg.as_string()))
81 _LOG.info('sending message to {}...'.format(targets))
85 smtp = _smtplib.SMTP('localhost')
87 targets = [debug_target]
88 smtp.sendmail(author, targets, msg.as_string())
92 _LOG.warning('failed to send message to {}'.format(targets))
97 _LOG.info('sent message to {}'.format(targets))
101 _LOG.info('dry run, so no message sent to {}'.format(targets))
106 class Responder (object):
107 def __init__(self, *args, **kwargs):
113 def __call__(self, message):
114 send_emails([(message, None)], *self.args, **self.kwargs)
117 def get_address(person):
119 >>> from pygrader.model.person import Person as Person
120 >>> p = Person(name='Jack', emails=['a@b.net'])
124 Here's a simple unicode example. The name portion of the address
125 is encoded following RFC 2047.
129 '=?utf-8?b?4pyJ?= <a@b.net>'
131 Note that the address is in the clear. Otherwise you can have
132 trouble when your mailer tries to decode the name following
133 :RFC:`2822`, which limits the locations in which encoded words may
136 encoding = _pgp_mime.guess_encoding(person.name)
137 return _email_utils.formataddr(
138 (person.name, person.emails[0]), charset=encoding)
140 def construct_email(author, targets, subject, message, cc=None):
142 signers = [author.pgp_key]
145 recipients = [p.pgp_key for p in targets if p.pgp_key]
147 for person in targets:
148 if not person.pgp_key:
149 encrypt = False # cannot encrypt to every recipient
152 recipients.extend([p.pgp_key for p in cc if p.pgp_key])
154 if not person.pgp_key:
158 encrypt = False # noone to encrypt to
159 if signers and encrypt:
160 if author.pgp_key not in recipients:
161 recipients.append(author.pgp_key)
162 message = _pgp_mime.sign_and_encrypt(
163 message=message, signers=signers, recipients=recipients,
166 message = _pgp_mime.sign(message=message, signers=signers)
168 message = _pgp_mime.encrypt(message=message, recipients=recipients)
170 message['Date'] = _email_utils.formatdate()
171 message['From'] = get_address(author)
172 message['Reply-to'] = message['From']
173 message['To'] = ', '.join(
174 get_address(target) for target in targets)
176 message['Cc'] = ', '.join(
177 get_address(target) for target in cc)
178 subject_encoding = _pgp_mime.guess_encoding(subject)
179 if subject_encoding == 'us-ascii':
180 message['Subject'] = subject
182 message['Subject'] = _Header(subject, subject_encoding)
186 def construct_text_email(author, targets, subject, text, cc=None):
187 r"""Build a text/plain email using `Person` instances
189 >>> from pygrader.model.person import Person as Person
190 >>> author = Person(name='Джон Доу', emails=['jdoe@a.gov.ru'])
191 >>> targets = [Person(name='Jill', emails=['c@d.net'])]
192 >>> cc = [Person(name='H.D.', emails=['hd@wall.net'])]
193 >>> msg = construct_text_email(author, targets, cc=cc,
194 ... subject='Once upon a time', text='Bla bla bla...')
195 >>> print(msg.as_string()) # doctest: +REPORT_UDIFF, +ELLIPSIS
196 Content-Type: text/plain; charset="us-ascii"
198 Content-Transfer-Encoding: 7bit
199 Content-Disposition: inline
201 From: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>
202 Reply-to: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>
204 Cc: "H.D." <hd@wall.net>
205 Subject: Once upon a time
211 >>> msg = construct_text_email(author, targets, cc=cc,
212 ... subject='Once upon a time', text='Funky ✉.')
213 >>> print(msg.as_string()) # doctest: +REPORT_UDIFF, +ELLIPSIS
214 Content-Type: text/plain; charset="utf-8"
216 Content-Transfer-Encoding: base64
217 Content-Disposition: inline
219 From: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>
220 Reply-to: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>
222 Cc: "H.D." <hd@wall.net>
223 Subject: Once upon a time
228 message = _pgp_mime.encodedMIMEText(text)
229 return construct_email(
230 author=author, targets=targets, subject=subject, message=message,
233 def construct_response(author, targets, subject, text, original, cc=None):
234 r"""Build a multipart/mixed response email using `Person` instances
236 >>> from pygrader.model.person import Person as Person
237 >>> student = Person(name='Джон Доу', emails=['jdoe@a.gov.ru'])
238 >>> assistant = Person(name='Jill', emails=['c@d.net'])
240 >>> msg = construct_text_email(author=student, targets=[assistant],
241 ... subject='Assignment 1 submission', text='Bla bla bla...')
242 >>> rsp = construct_response(author=assistant, targets=[student],
243 ... subject='Received assignment 1 submission', text='3 hours late',
245 >>> print(rsp.as_string()) # doctest: +REPORT_UDIFF, +ELLIPSIS
246 Content-Type: multipart/mixed; boundary="===============...=="
250 Reply-to: Jill <c@d.net>
251 To: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>
252 Subject: Received assignment 1 submission
254 --===============...==
255 Content-Type: text/plain; charset="us-ascii"
257 Content-Transfer-Encoding: 7bit
258 Content-Disposition: inline
261 --===============...==
262 Content-Type: message/rfc822
265 Content-Type: text/plain; charset="us-ascii"
267 Content-Transfer-Encoding: 7bit
268 Content-Disposition: inline
270 From: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>
271 Reply-to: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>
273 Subject: Assignment 1 submission
276 --===============...==--
278 message = _MIMEMultipart('mixed')
279 message.attach(_pgp_mime.encodedMIMEText(text))
280 message.attach(_MIMEMessage(original))
281 return construct_email(
282 author=author, targets=targets, subject=subject, message=message,