1 """
2 The lamson.mail module contains nothing more than wrappers around the big work
3 done in lamson.encoding. These are the actual APIs that you'll interact with
4 when doing email, and they mostly replicate the lamson.encoding.MailBase
5 functionality.
6
7 The main design criteria is that MailRequest is mostly for reading email
8 that you've received, so it doesn't have functions for attaching files and such.
9 MailResponse is used when you are going to write an email, so it has the
10 APIs for doing attachments and such.
11 """
12
13
14 import mimetypes
15 from lamson import encoding, bounce
16 from email.utils import parseaddr
17 import os
18 import warnings
19
20
21
22 ROUTABLE_TO_HEADER='to'
25 """
26 This fixes the given address so that it is *always* a set() of
27 just email addresses suitable for routing.
28 """
29 if not addr:
30 return set()
31 elif isinstance(addr, list):
32 return set(parseaddr(a.lower())[1] for a in addr)
33 elif isinstance(addr, basestring):
34 return set([parseaddr(addr.lower())[1]])
35 else:
36 raise encoding.EncodingError("Address must be a string or a list not: %r", type(addr))
37
40 """
41 This is what's handed to your handlers for you to process. The information
42 you get out of this is *ALWAYS* in Python unicode and should be usable
43 by any API. Modifying this object will cause other handlers that deal
44 with it to get your modifications, but in general you don't want to do
45 more than maybe tag a few headers.
46 """
47 - def __init__(self, Peer, From, To, Data):
48 """
49 Peer is the remote peer making the connection (sometimes the queue
50 name). From and To are what you think they are. Data is the raw
51 full email as received by the server.
52
53 NOTE: It does not handle multiple From headers, if that's even
54 possible. It will parse the From into a list and take the first
55 one.
56 """
57
58 self.original = Data
59 self.base = encoding.from_string(Data)
60 self.Peer = Peer
61 self.From = From or self.base['from']
62 self.To = To or self.base[ROUTABLE_TO_HEADER]
63
64 if 'from' not in self.base:
65 self.base['from'] = self.From
66 if 'to' not in self.base:
67
68 self.base['to'] = self.To
69
70 self.route_to = _decode_header_randomness(self.To)
71 self.route_from = _decode_header_randomness(self.From)
72
73 if self.route_from:
74 self.route_from = self.route_from.pop()
75 else:
76 self.route_from = None
77
78 self.bounce = None
79
80
82 """Returns all multipart mime parts. This could be an empty list."""
83 return self.base.parts
84
85
87 """
88 Always returns a body if there is one. If the message
89 is multipart then it returns the first part's body, if
90 it's not then it just returns the body. If returns
91 None then this message has nothing for a body.
92 """
93 if self.base.parts:
94 return self.base.parts[0].body
95 else:
96 return self.base.body
97
98
101
104
107
110
112 """
113 Converts this to a string usable for storage into a queue or
114 transmission.
115 """
116 return encoding.to_string(self.base)
117
119 return "From: %r" % [self.Peer, self.From, self.To]
120
122 return self.base.keys()
123
125 """
126 Converts this to a Python email message you can use to
127 interact with the python mail APIs.
128 """
129 return encoding.to_message(self.base)
130
132 """Recursively walks all attached parts and their children."""
133 for x in self.base.walk():
134 yield x
135
137 """
138 Determines whether the message is a bounce message based on
139 lamson.bounce.BounceAnalzyer given threshold. 0.3 is a good
140 conservative base.
141 """
142 if not self.bounce:
143 self.bounce = bounce.detect(self)
144
145 if self.bounce.score > threshold:
146 return True
147 else:
148 return False
149
150 @property
152 warnings.warn("The .msg attribute is deprecated, use .base instead. This will be gone in Lamson 1.0",
153 category=DeprecationWarning, stacklevel=2)
154 return self.base
155
159 """
160 You are given MailResponse objects from the lamson.view methods, and
161 whenever you want to generate an email to send to someone. It has
162 the same basic functionality as MailRequest, but it is designed to
163 be written to, rather than read from (although you can do both).
164
165 You can easily set a Body or Html during creation or after by
166 passing it as __init__ parameters, or by setting those attributes.
167
168 You can initially set the From, To, and Subject, but they are headers so
169 use the dict notation to change them: msg['From'] = 'joe@test.com'.
170
171 The message is not fully crafted until right when you convert it with
172 MailResponse.to_message. This lets you change it and work with it, then
173 send it out when it's ready.
174 """
175 - def __init__(self, To=None, From=None, Subject=None, Body=None, Html=None):
176 self.Body = Body
177 self.Html = Html
178 self.base = encoding.MailBase([('To', To), ('From', From), ('Subject', Subject)])
179 self.multipart = self.Body and self.Html
180 self.attachments = []
181
184
187
190
193
194 - def attach(self, filename=None, content_type=None, data=None, disposition=None):
195 """
196 Simplifies attaching files from disk or data as files. To attach simple
197 text simple give data and a content_type. To attach a file, give the
198 data/content_type/filename/disposition combination.
199
200 For convenience, if you don't give data and only a filename, then it
201 will read that file's contents when you call to_message() later. If you
202 give data and filename then it will assume you've filled data with what
203 the file's contents are and filename is just the name to use.
204 """
205 assert filename or data, "You must give a filename or some data to attach."
206 assert data or os.path.exists(filename), "File doesn't exist, and no data given."
207
208 self.multipart = True
209
210 if filename and not content_type:
211 content_type, encoding = mimetypes.guess_type(filename)
212
213 assert content_type, "No content type given, and couldn't guess from the filename: %r" % filename
214
215 self.attachments.append({'filename': filename,
216 'content_type': content_type,
217 'data': data,
218 'disposition': disposition,})
220 """
221 Attaches a raw MailBase part from a MailRequest (or anywhere)
222 so that you can copy it over.
223 """
224 self.multipart = True
225
226 self.attachments.append({'filename': None,
227 'content_type': None,
228 'data': None,
229 'disposition': None,
230 'part': part,
231 })
232
234 """
235 Used for copying the attachment parts of a mail.MailRequest
236 object for mailing lists that need to maintain attachments.
237 """
238 for part in mail_request.all_parts():
239 self.attach_part(part)
240
241 self.base.content_encoding = mail_request.base.content_encoding.copy()
242
244 """
245 Clears out the attachments so you can redo them. Use this to keep the
246 headers for a series of different messages with different attachments.
247 """
248 del self.attachments[:]
249 del self.base.parts[:]
250 self.multipart = False
251
252
254 """
255 Used to easily set a bunch of heading from another dict
256 like object.
257 """
258 for k in message.keys():
259 self.base[k] = message[k]
260
262 """
263 Converts to a string.
264 """
265 return self.to_message().as_string()
266
267 - def _encode_attachment(self, filename=None, content_type=None, data=None, disposition=None, part=None):
268 """
269 Used internally to take the attachments mentioned in self.attachments
270 and do the actual encoding in a lazy way when you call to_message.
271 """
272 if part:
273 self.base.parts.append(part)
274 elif filename:
275 if not data:
276 data = open(filename).read()
277
278 self.base.attach_file(filename, data, content_type, disposition or 'attachment')
279 else:
280 self.base.attach_text(data, content_type)
281
282 ctype = self.base.content_encoding['Content-Type'][0]
283
284 if ctype and not ctype.startswith('multipart'):
285 self.base.content_encoding['Content-Type'] = ('multipart/mixed', {})
286
288 """
289 Figures out all the required steps to finally craft the
290 message you need and return it. The resulting message
291 is also available as a self.base attribute.
292
293 What is returned is a Python email API message you can
294 use with those APIs. The self.base attribute is the raw
295 lamson.encoding.MailBase.
296 """
297 del self.base.parts[:]
298
299 if self.Body and self.Html:
300 self.multipart = True
301 self.base.content_encoding['Content-Type'] = ('multipart/alternative', {})
302
303 if self.multipart:
304 self.base.body = None
305 if self.Body:
306 self.base.attach_text(self.Body, 'text/plain')
307
308 if self.Html:
309 self.base.attach_text(self.Html, 'text/html')
310
311 for args in self.attachments:
312 self._encode_attachment(**args)
313
314 elif self.Body:
315 self.base.body = self.Body
316 self.base.content_encoding['Content-Type'] = ('text/plain', {})
317
318 elif self.Html:
319 self.base.body = self.Html
320 self.base.content_encoding['Content-Type'] = ('text/html', {})
321
322 return encoding.to_message(self.base)
323
325 """
326 Returns all the encoded parts. Only useful for debugging
327 or inspecting after calling to_message().
328 """
329 return self.base.parts
330
332 return self.base.keys()
333
334 @property
336 warnings.warn("The .msg attribute is deprecated, use .base instead. This will be gone in Lamson 1.0",
337 category=DeprecationWarning, stacklevel=2)
338 return self.base
339