Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 405 lines (321 sloc) 14.013 kB
623d50f @mcdonc - Drop Lamson dependency by copying Lamson's MailResponse and depende…
mcdonc authored
1 # The code in this module is entirely lifted from the Lamson project
2 # (http://lamsonproject.org/). Its copyright is:
3
4 # Copyright (c) 2008, Zed A. Shaw
5 # All rights reserved.
6
7 # It is provided under this license:
8
9 # Redistribution and use in source and binary forms, with or without
10 # modification, are permitted provided that the following conditions are met:
11
12 # * Redistributions of source code must retain the above copyright notice, this
13 # list of conditions and the following disclaimer.
14
15 # * Redistributions in binary form must reproduce the above copyright notice,
16 # this list of conditions and the following disclaimer in the documentation
17 # and/or other materials provided with the distribution.
18
19 # * Neither the name of the Zed A. Shaw nor the names of its contributors may
20 # be used to endorse or promote products derived from this software without
21 # specific prior written permission.
22
23 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
24 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
25 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
26 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
27 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
28 # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
29 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
31 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
32 # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
33 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
34 # POSSIBILITY OF SUCH DAMAGE.
35
36 import os
49d86a2 @rpatterson Moved message encoding to repoze.sendmail.
rpatterson authored
37 import sys
623d50f @mcdonc - Drop Lamson dependency by copying Lamson's MailResponse and depende…
mcdonc authored
38 import mimetypes
39 import string
40 from email.mime.base import MIMEBase
41
49d86a2 @rpatterson Moved message encoding to repoze.sendmail.
rpatterson authored
42 from repoze.sendmail import encoding
43
623d50f @mcdonc - Drop Lamson dependency by copying Lamson's MailResponse and depende…
mcdonc authored
44
45 def normalize_header(header):
46 return string.capwords(header.lower(), '-')
47
48 class EncodingError(Exception):
49 """Thrown when there is an encoding error."""
50 pass
51
52 class MailBase(object):
53 """MailBase is used as the basis of lamson.mail and contains the basics of
54 encoding an email. You actually can do all your email processing with this
55 class, but it's more raw.
56 """
57 def __init__(self, items=()):
58 self.headers = dict(items)
59 self.parts = []
60 self.body = None
61 self.content_encoding = {'Content-Type': (None, {}),
62 'Content-Disposition': (None, {}),
63 'Content-Transfer-Encoding': (None, {})}
64
65 def __getitem__(self, key):
66 return self.headers.get(normalize_header(key), None)
67
68 def __len__(self):
69 return len(self.headers)
70
71 def __iter__(self):
72 return iter(self.headers)
73
74 def __contains__(self, key):
75 return normalize_header(key) in self.headers
76
77 def __setitem__(self, key, value):
78 self.headers[normalize_header(key)] = value
79
80 def __delitem__(self, key):
81 del self.headers[normalize_header(key)]
82
83 def __nonzero__(self):
84 return self.body != None or len(self.headers) > 0 or len(self.parts) > 0
9b5ac4b @rpatterson Python 2 and 3 compat, low hanging fruit. All tests pass under 2,
rpatterson authored
85 __bool__ = __nonzero__
623d50f @mcdonc - Drop Lamson dependency by copying Lamson's MailResponse and depende…
mcdonc authored
86
87 def keys(self):
88 """Returns the sorted keys."""
89 return sorted(self.headers.keys())
90
91 def attach_file(self, filename, data, ctype, disposition):
92 """
93 A file attachment is a raw attachment with a disposition that
94 indicates the file name.
95 """
96 assert filename, "You can't attach a file without a filename."
97 ctype = ctype.lower()
98
99 part = MailBase()
100 part.body = data
101 part.content_encoding['Content-Type'] = (ctype, {'name': filename})
102 part.content_encoding['Content-Disposition'] = (disposition,
103 {'filename': filename})
104 self.parts.append(part)
105
106
107 def attach_text(self, data, ctype):
108 """
109 This attaches a simpler text encoded part, which doesn't have a
110 filename.
111 """
112 ctype = ctype.lower()
113
114 part = MailBase()
115 part.body = data
116 part.content_encoding['Content-Type'] = (ctype, {})
117 self.parts.append(part)
118
119 def walk(self):
120 for p in self.parts:
121 yield p
122 for x in p.walk():
123 yield x
124
125 class MailResponse(object):
126 """
127 You are given MailResponse objects from the lamson.view methods, and
128 whenever you want to generate an email to send to someone. It has the
129 same basic functionality as MailRequest, but it is designed to be written
130 to, rather than read from (although you can do both).
131
132 You can easily set a Body or Html during creation or after by passing it
133 as __init__ parameters, or by setting those attributes.
134
135 You can initially set the From, To, and Subject, but they are headers so
136 use the dict notation to change them: msg['From'] = 'joe@test.com'.
137
138 The message is not fully crafted until right when you convert it with
139 MailResponse.to_message. This lets you change it and work with it, then
140 send it out when it's ready.
141 """
142 def __init__(self, To=None, From=None, Subject=None, Body=None, Html=None):
143 self.Body = Body
144 self.Html = Html
145 self.base = MailBase([('To', To), ('From', From), ('Subject', Subject)])
146 self.multipart = self.Body and self.Html
147 self.attachments = []
148
149 def __contains__(self, key):
150 return self.base.__contains__(key)
151
152 def __getitem__(self, key):
153 return self.base.__getitem__(key)
154
155 def __setitem__(self, key, val):
156 return self.base.__setitem__(key, val)
157
158 def __delitem__(self, name):
159 del self.base[name]
160
161 def attach(self, filename=None, content_type=None, data=None,
162 disposition=None):
163 """
164
165 Simplifies attaching files from disk or data as files. To attach
166 simple text simple give data and a content_type. To attach a file,
167 give the data/content_type/filename/disposition combination.
168
169 For convenience, if you don't give data and only a filename, then it
170 will read that file's contents when you call to_message() later. If
171 you give data and filename then it will assume you've filled data
172 with what the file's contents are and filename is just the name to
173 use.
174 """
175
176 assert filename or data, ("You must give a filename or some data to "
177 "attach.")
178 assert data or os.path.exists(filename), ("File doesn't exist, and no "
179 "data given.")
180
181 self.multipart = True
182
183 if filename and not content_type:
2055c99 @mcdonc dont shadow a global
mcdonc authored
184 content_type, enc = mimetypes.guess_type(filename)
623d50f @mcdonc - Drop Lamson dependency by copying Lamson's MailResponse and depende…
mcdonc authored
185
186 assert content_type, ("No content type given, and couldn't guess "
187 "from the filename: %r" % filename)
188
189 self.attachments.append({'filename': filename,
190 'content_type': content_type,
191 'data': data,
192 'disposition': disposition,})
193 def attach_part(self, part):
194 """
195 Attaches a raw MailBase part from a MailRequest (or anywhere)
196 so that you can copy it over.
197 """
198 self.multipart = True
199
200 self.attachments.append({'filename': None,
201 'content_type': None,
202 'data': None,
203 'disposition': None,
204 'part': part,
205 })
206
207 def attach_all_parts(self, mail_request):
208 """
209 Used for copying the attachment parts of a mail.MailRequest
210 object for mailing lists that need to maintain attachments.
211 """
212 for part in mail_request.all_parts():
213 self.attach_part(part)
214
215 self.base.content_encoding = mail_request.base.content_encoding.copy()
216
217 def clear(self):
218 """
219 Clears out the attachments so you can redo them. Use this to keep the
220 headers for a series of different messages with different attachments.
221 """
222 del self.attachments[:]
223 del self.base.parts[:]
224 self.multipart = False
225
226
227 def update(self, message):
228 """
229 Used to easily set a bunch of heading from another dict
230 like object.
231 """
232 for k in message.keys():
233 self.base[k] = message[k]
234
235 def __str__(self):
236 """
237 Converts to a string.
238 """
239 return self.to_message().as_string()
240
241 def _encode_attachment(self, filename=None, content_type=None, data=None,
242 disposition=None, part=None):
243 """
244 Used internally to take the attachments mentioned in self.attachments
245 and do the actual encoding in a lazy way when you call to_message.
246 """
247 if part:
248 self.base.parts.append(part)
249 elif filename:
250 if not data:
97eeef4 @mcdonc squash resource warning on python 3.2
mcdonc authored
251 f = open(filename)
252 data = f.read()
253 f.close()
623d50f @mcdonc - Drop Lamson dependency by copying Lamson's MailResponse and depende…
mcdonc authored
254
255 self.base.attach_file(filename, data, content_type,
256 disposition or 'attachment')
257 else:
258 self.base.attach_text(data, content_type)
259
260 ctype = self.base.content_encoding['Content-Type'][0]
261
262 if ctype and not ctype.startswith('multipart'):
263 self.base.content_encoding['Content-Type'] = ('multipart/mixed', {})
264
265 def to_message(self):
266 """
267 Figures out all the required steps to finally craft the
268 message you need and return it. The resulting message
269 is also available as a self.base attribute.
270
271 What is returned is a Python email API message you can
272 use with those APIs. The self.base attribute is the raw
273 lamson.encoding.MailBase.
274 """
275 del self.base.parts[:]
276
277 if self.Body and self.Html:
278 self.multipart = True
279 self.base.content_encoding['Content-Type'] = (
280 'multipart/alternative', {})
281
282 if self.multipart:
283 self.base.body = None
284 if self.Body:
285 self.base.attach_text(self.Body, 'text/plain')
286
287 if self.Html:
288 self.base.attach_text(self.Html, 'text/html')
289
290 for args in self.attachments:
291 self._encode_attachment(**args)
292
293 elif self.Body:
294 self.base.body = self.Body
295 self.base.content_encoding['Content-Type'] = ('text/plain', {})
296
297 elif self.Html:
298 self.base.body = self.Html
299 self.base.content_encoding['Content-Type'] = ('text/html', {})
300
301 return to_message(self.base)
302
303 def all_parts(self):
304 """
305 Returns all the encoded parts. Only useful for debugging
306 or inspecting after calling to_message().
307 """
308 return self.base.parts
309
310 def keys(self):
311 return self.base.keys()
312
313 def to_message(mail):
314 """
315 Given a MailBase message, this will construct a MIMEPart
316 that is canonicalized for use with the Python email API.
317 """
318 ctype, params = mail.content_encoding['Content-Type']
319
320 if not ctype:
321 if mail.parts:
322 ctype = 'multipart/mixed'
323 else:
324 ctype = 'text/plain'
325 else:
326 if mail.parts:
327 assert ctype.startswith(("multipart", "message")), \
328 "Content type should be multipart or message, not %r" % ctype
329
330 # adjust the content type according to what it should be now
331 mail.content_encoding['Content-Type'] = (ctype, params)
332
333 try:
334 out = MIMEPart(ctype, **params)
19861ee @rpatterson Python 2.5 compat!
rpatterson authored
335 except TypeError: # pragma: no cover
336 exc = sys.exc_info()[1]
623d50f @mcdonc - Drop Lamson dependency by copying Lamson's MailResponse and depende…
mcdonc authored
337 raise EncodingError("Content-Type malformed, not allowed: %r; "
338 "%r (Python ERROR: %s" %
339 (ctype, params, exc.message))
340
341 for k in mail.keys():
49d86a2 @rpatterson Moved message encoding to repoze.sendmail.
rpatterson authored
342 value = mail[k]
343 if k.lower() in encoding.ADDR_HEADERS:
344 if is_nonstr_iter(value): # not a string
345 value = ", ".join(value)
23e334b @rpatterson Workaround a Python 3.2.0 bug in handling emails with empty headers.
rpatterson authored
346 if value == '':
347 continue
49d86a2 @rpatterson Moved message encoding to repoze.sendmail.
rpatterson authored
348 out[k] = value
623d50f @mcdonc - Drop Lamson dependency by copying Lamson's MailResponse and depende…
mcdonc authored
349
350 out.extract_payload(mail)
351
352 # go through the children
353 for part in mail.parts:
354 out.attach(to_message(part))
355
356 return out
357
358 class MIMEPart(MIMEBase):
359 """
360 A reimplementation of nearly everything in email.mime to be more useful
361 for actually attaching things. Rather than one class for every type of
362 thing you'd encode, there's just this one, and it figures out how to
363 encode what you ask it.
364 """
365 def __init__(self, type, **params):
366 self.maintype, self.subtype = type.split('/')
367 MIMEBase.__init__(self, self.maintype, self.subtype, **params)
368
369 def extract_payload(self, mail):
370 if mail.body == None: return # only None, '' is still ok
371
372 ctype, ctype_params = mail.content_encoding['Content-Type']
373 cdisp, cdisp_params = mail.content_encoding['Content-Disposition']
374
375 assert ctype, ("Extract payload requires that mail.content_encoding "
376 "have a valid Content-Type.")
377
378 if ctype.startswith("text/"):
201899a @rpatterson Text encoding should only be handled for messages using encoders.enco…
rpatterson authored
379 self.set_payload(mail.body)
623d50f @mcdonc - Drop Lamson dependency by copying Lamson's MailResponse and depende…
mcdonc authored
380 else:
381 if cdisp:
382 # replicate the content-disposition settings
383 self.add_header('Content-Disposition', cdisp, **cdisp_params)
384
49d86a2 @rpatterson Moved message encoding to repoze.sendmail.
rpatterson authored
385 self.set_payload(mail.body)
623d50f @mcdonc - Drop Lamson dependency by copying Lamson's MailResponse and depende…
mcdonc authored
386
387 def __repr__(self):
9b5ac4b @rpatterson Python 2 and 3 compat, low hanging fruit. All tests pass under 2,
rpatterson authored
388 return "<MIMEPart '%s/%s': '%s', %r, multipart=%r>" % (
623d50f @mcdonc - Drop Lamson dependency by copying Lamson's MailResponse and depende…
mcdonc authored
389 self.subtype,
390 self.maintype,
391 self['Content-Type'],
392 self['Content-Disposition'],
393 self.is_multipart())
394
395
04dd01c @mcdonc coverage
mcdonc authored
396 def is_nonstr_iter(v): # pragma: no cover
397 if isinstance(v, str):
9b5ac4b @rpatterson Python 2 and 3 compat, low hanging fruit. All tests pass under 2,
rpatterson authored
398 return False
399 return hasattr(v, '__iter__')
400
401 # BBB Python 2 vs 3 compat
402 if sys.version < '3':
403 def is_nonstr_iter(v):
404 return hasattr(v, '__iter__')
Something went wrong with that request. Please try again.