d1f0ef04e7ea9a273e2644745903cac3ce788ef2
[pygrader.git] / pygrader / email.py
1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2011 W. Trevor King <wking@drexel.edu>
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18 from __future__ import absolute_import
19
20 from email.header import Header as _Header
21 from email.header import decode_header as _decode_header
22 import email.utils as _email_utils
23 import logging as _logging
24 import smtplib as _smtplib
25
26 import pgp_mime as _pgp_mime
27
28 from . import ENCODING as _ENCODING
29 from . import LOG as _LOG
30 from .color import standard_colors as _standard_colors
31 from .color import color_string as _color_string
32 from .color import write_color as _write_color
33 from .model.person import Person as _Person
34
35
36 def test_smtp(smtp, author, targets, msg=None):
37     """Test the SMTP connection by sending a message to `target`
38     """
39     if msg is None:
40         msg = _pgp_mime.encodedMIMEText('Success!')
41         msg['Date'] = _email_utils.formatdate()
42         msg['From'] = author
43         msg['Reply-to'] = msg['From']
44         msg['To'] = ', '.join(targets)
45         msg['Subject'] = 'Testing pygrader SMTP connection'
46     _LOG.info('send test message to SMTP server')
47     smtp.send_message(msg=msg)
48 test_smtp.__test__ = False  # not a test for nose
49
50 def send_emails(emails, smtp=None, use_color=None, debug_target=None,
51                 dry_run=False):
52     """Iterate through `emails` and mail them off one-by-one
53
54     >>> from email.mime.text import MIMEText
55     >>> from sys import stdout
56     >>> emails = []
57     >>> for target in ['Moneypenny <mp@sis.gov.uk>', 'M <m@sis.gov.uk>']:
58     ...     msg = MIMEText('howdy!', 'plain', 'us-ascii')
59     ...     msg['From'] = 'John Doe <jdoe@a.gov.ru>'
60     ...     msg['To'] = target
61     ...     msg['Bcc'] = 'James Bond <007@sis.gov.uk>'
62     ...     emails.append(
63     ...         (msg,
64     ...          lambda status: stdout.write('SUCCESS: {}\\n'.format(status))))
65     >>> send_emails(emails, use_color=False, dry_run=True)
66     ... # doctest: +REPORT_UDIFF, +NORMALIZE_WHITESPACE
67     sending message to ['Moneypenny <mp@sis.gov.uk>', 'James Bond <007@sis.gov.uk>']...\tDRY-RUN
68     SUCCESS: None
69     sending message to ['M <m@sis.gov.uk>', 'James Bond <007@sis.gov.uk>']...\tDRY-RUN
70     SUCCESS: None
71     """
72     local_smtp = smtp is None
73     highlight,lowlight,good,bad = _standard_colors(use_color=use_color)
74     for msg,callback in emails:
75         sources = [
76             _email_utils.formataddr(a) for a in _pgp_mime.email_sources(msg)]
77         author = sources[0]
78         targets = [
79             _email_utils.formataddr(a) for a in _pgp_mime.email_targets(msg)]
80         _pgp_mime.strip_bcc(msg)
81         if _LOG.level <= _logging.DEBUG:
82             # TODO: remove convert_content_transfer_encoding?
83             #if msg.get('content-transfer-encoding', None) == 'base64':
84             #    convert_content_transfer_encoding(msg, '8bit')
85             _LOG.debug(_color_string(
86                     '\n{}\n'.format(msg.as_string()), color=lowlight))
87         _write_color('sending message to {}...'.format(targets),
88                      color=highlight)
89         if not dry_run:
90             try:
91                 if local_smtp:
92                     smtp = _smtplib.SMTP('localhost')
93                 if debug_target:
94                     targets = [debug_target]
95                 smtp.sendmail(author, targets, msg.as_string())
96                 if local_smtp:
97                     smtp.quit()
98             except:
99                 _write_color('\tFAILED\n', bad)
100                 if callback:
101                     callback(False)
102                 raise
103             else:
104                 _write_color('\tOK\n', good)
105                 if callback:
106                     callback(True)
107         else:
108             _write_color('\tDRY-RUN\n', good)
109             if callback:
110                 callback(None)
111
112 def get_address(person, header=False):
113     r"""
114     >>> from pygrader.model.person import Person as Person
115     >>> p = Person(name='Jack', emails=['a@b.net'])
116     >>> get_address(p)
117     'Jack <a@b.net>'
118
119     Here's a simple unicode example.
120
121     >>> p.name = '✉'
122     >>> get_address(p)
123     '✉ <a@b.net>'
124
125     When you encode addresses that you intend to place in an email
126     header, you should set the `header` option to `True`.  This
127     encodes the name portion of the address without encoding the email
128     portion.
129
130     >>> get_address(p, header=True)
131     '=?utf-8?b?4pyJ?= <a@b.net>'
132
133     Note that the address is in the clear.  Without the `header`
134     option you'd have to rely on something like:
135
136     >>> from email.header import Header
137     >>> Header(get_address(p), 'utf-8').encode()
138     '=?utf-8?b?4pyJIDxhQGIubmV0Pg==?='
139
140     This can cause trouble when your mailer tries to decode the name
141     following :RFC:`2822`, which limits the locations in which encoded
142     words may appear.
143     """
144     if header:
145         encoding = _pgp_mime.guess_encoding(person.name)
146         if encoding == 'us-ascii':
147             name = person.name
148         else:
149             name = _Header(person.name, encoding).encode()
150         return _email_utils.formataddr((name, person.emails[0]))
151     return _email_utils.formataddr((person.name, person.emails[0]))
152
153 def construct_email(author, targets, subject, text, cc=None, sign=True):
154     r"""Built a text/plain email using `Person` instances
155
156     >>> from pygrader.model.person import Person as Person
157     >>> author = Person(name='Джон Доу', emails=['jdoe@a.gov.ru'])
158     >>> targets = [Person(name='Jill', emails=['c@d.net'])]
159     >>> cc = [Person(name='H.D.', emails=['hd@wall.net'])]
160     >>> msg = construct_email(author, targets, cc=cc,
161     ...     subject='Once upon a time', text='Bla bla bla...')
162     >>> print(msg.as_string())  # doctest: +REPORT_UDIFF, +ELLIPSIS
163     Content-Type: text/plain; charset="us-ascii"
164     MIME-Version: 1.0
165     Content-Transfer-Encoding: 7bit
166     Content-Disposition: inline
167     Date: ...
168     From: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>
169     Reply-to: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>
170     To: Jill <c@d.net>
171     Cc: "H.D." <hd@wall.net>
172     Subject: Once upon a time
173     <BLANKLINE>
174     Bla bla bla...
175
176     With unicode text:
177
178     >>> msg = construct_email(author, targets, cc=cc,
179     ...     subject='Once upon a time', text='Funky ✉.')
180     >>> print(msg.as_string())  # doctest: +REPORT_UDIFF, +ELLIPSIS
181     Content-Type: text/plain; charset="utf-8"
182     MIME-Version: 1.0
183     Content-Transfer-Encoding: base64
184     Content-Disposition: inline
185     Date: ...
186     From: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>
187     Reply-to: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>
188     To: Jill <c@d.net>
189     Cc: "H.D." <hd@wall.net>
190     Subject: Once upon a time
191     <BLANKLINE>
192     RnVua3kg4pyJLg==
193     <BLANKLINE>
194     """
195     msg = _pgp_mime.encodedMIMEText(text)
196     if sign and author.pgp_key:
197         msg = _pgp_mime.sign(message=msg, sign_as=author.pgp_key)
198
199     msg['Date'] = _email_utils.formatdate()
200     msg['From'] = get_address(author, header=True)
201     msg['Reply-to'] = msg['From']
202     msg['To'] = ', '.join(
203         get_address(target, header=True) for target in targets)
204     if cc:
205         msg['Cc'] = ', '.join(
206             get_address(target, header=True) for target in cc)
207     subject_encoding = _pgp_mime.guess_encoding(subject)
208     if subject_encoding == 'us-ascii':
209         msg['Subject'] = subject
210     else:
211         msg['Subject'] = _Header(subject, subject_encoding)
212
213     return msg