forked from nhoad/outbox
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathoutbox.py
235 lines (177 loc) · 6.69 KB
/
outbox.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
'''
File: outbox.py
Author: Nathan Hoad
Description: Simple wrapper around smtplib for sending an email.
'''
import smtplib
import sys
from email import encoders
from email.header import Header
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formatdate
PY2 = sys.version_info[0] == 2
if PY2:
string_type = basestring
iteritems = lambda d: d.iteritems()
else:
string_type = str
iteritems = lambda d: d.items()
class Email(object):
def __init__(self, recipients, subject, body=None, html_body=None,
charset='utf8', fields=None, rfc2231=True):
"""
Object representation of an email. Contains a recipient, subject,
conditionally a body or HTML body.
Arguments:
recipients - list of strings of the email addresses of the
recipients. May also be a string containing a single
email address.
subject - Subject of the email.
body - Plain-text body.
html_body - Rich-text body.
charset - charset to use for encoding the `body` and `html_body`
attributes.
fields - any additional headers you want to add to the email message.
"""
iter(recipients)
if isinstance(recipients, string_type):
recipients = [recipients]
if not recipients:
raise ValueError("At least one recipient must be specified!")
for r in recipients:
if not isinstance(r, string_type):
raise TypeError("Recipient not a string: %s" % r)
if body is None and html_body is None:
raise ValueError("No body set")
self.recipients = recipients
self.subject = subject
self.body = body
self.html_body = html_body
self.charset = charset
self.fields = fields or {}
self.rfc2231 = rfc2231
def as_mime(self, attachments=()):
bodies = []
if self.body:
bodies.append(MIMEText(self.body, 'plain', self.charset))
if self.html_body:
bodies.append(MIMEText(self.html_body, 'html', self.charset))
with_alternative = len(bodies) == 2
if with_alternative or attachments:
if with_alternative:
txt = MIMEMultipart('alternative')
if attachments:
msg = MIMEMultipart('mixed')
msg.attach(txt)
else:
msg = txt
else:
msg = txt = MIMEMultipart('mixed')
for body in bodies:
txt.attach(body)
else:
msg = bodies[0]
msg['To'] = ', '.join(self.recipients)
msg['Date'] = formatdate(localtime=True)
msg['Subject'] = self.subject
for key, value in iteritems(self.fields):
msg[key] = value
for f in attachments:
if not isinstance(f, Attachment):
raise TypeError("attachment must be of type Attachment")
add_attachment(msg, f, self.rfc2231)
return msg
class Attachment(object):
'''Attachment for an email'''
def __init__(self, name, fileobj):
self.name = name
self.raw = fileobj.read()
if not isinstance(self.raw, bytes):
self.raw = self.raw.encode()
def read(self):
return self.raw
class Outbox(object):
'''Thin wrapper around the SMTP and SMTP_SSL classes from the smtplib module.'''
def __init__(self, username, password, server, port, mode='TLS', debug=False):
if mode not in ('SSL', 'TLS', None):
raise ValueError("Mode must be one of TLS, SSL, or None")
self.username = username
self.password = password
self.connection_details = (server, port, mode, debug)
self._conn = None
def __enter__(self):
self.connect()
return self
def __exit__(self, type, value, traceback):
self.disconnect()
def _login(self):
'''Login to the SMTP server specified at instantiation
Returns an authenticated SMTP instance.
'''
server, port, mode, debug = self.connection_details
if mode == 'SSL':
smtp_class = smtplib.SMTP_SSL
else:
smtp_class = smtplib.SMTP
smtp = smtp_class(server, port)
smtp.set_debuglevel(debug)
if mode == 'TLS':
smtp.starttls()
self.authenticate(smtp)
return smtp
def connect(self):
self._conn = self._login()
def authenticate(self, smtp):
"""Perform login with the given smtplib.SMTP instance."""
smtp.login(self.username, self.password)
def disconnect(self):
self._conn.quit()
def send(self, email, attachments=()):
'''Send an email. Connect/Disconnect if not already connected
Arguments:
email: Email instance to send.
attachments: iterable containing Attachment instances
'''
msg = email.as_mime(attachments)
if 'From' not in msg:
msg['From'] = self.sender_address()
if self._conn:
self._conn.sendmail(self.username, email.recipients,
msg.as_string())
else:
with self:
self._conn.sendmail(self.username, email.recipients,
msg.as_string())
def sender_address(self):
'''Return the sender address.
The default implementation is to use the username that is used for
signing in.
If you want pretty names, e.g. <Captain Awesome> foo@example.com,
override this method to do what you want.
'''
return self.username
class AnonymousOutbox(Outbox):
"""Outbox subclass suitable for SMTP servers that do not (or will not)
perform authentication.
"""
def __init__(self, *args, **kwargs):
super(AnonymousOutbox, self).__init__('', '', *args, **kwargs)
def authenticate(self, smtp):
"""Perform no authentication as the server does not require it."""
pass
def add_attachment(message, attachment, rfc2231=True):
'''Attach an attachment to a message as a side effect.
Arguments:
message: MIMEMultipart instance.
attachment: Attachment instance.
'''
data = attachment.read()
part = MIMEBase('application', 'octet-stream')
part.set_payload(data)
encoders.encode_base64(part)
filename = attachment.name if rfc2231 else Header(attachment.name).encode()
part.add_header('Content-Disposition', 'attachment',
filename=filename)
message.attach(part)