1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 """
18 text based email notifier
19 """
20 __author__ = "André Malo"
21 __docformat__ = "epytext en"
22 __all__ = ['getNotifier']
23
24
25 from svnmailer.notifier import _mail
26
27
29 """ Returns an initialized notifier or nothing
30
31 @param cls: The notifier base class to use
32 @type cls: C{class}
33
34 @param config: The svnmailer config
35 @type config: C{svnmailer.settings.Settings}
36
37 @param groupset: The groupset to process
38 @type groupset: C{list}
39
40 @return: The list of notifiers (containing 0 or 1 member)
41 @rtype: C{list}
42 """
43 from svnmailer import util
44
45 return [decorateNotifier(
46 util.inherit(cls, TextMailNotifier),
47 groupset.groups[0].long_mail_action, config, groupset
48 )]
49
50
52 """ Decorates the notifier class (or not)
53
54 @param cls: The notifier class
55 @type cls: C{class}
56
57 @param action: The configured action
58 @type action: C{unicode}
59
60 @param config: The svnmailer config
61 @type config: C{svnmailer.settings.Settings}
62
63 @param groupset: The groupset to process
64 @type groupset: C{list}
65
66 @return: The decorated class or C{None}
67 @rtype: C{class}
68 """
69 if action:
70 from svnmailer.settings import modes
71 runtime = config.runtime
72
73 is_commit = bool(runtime.mode == modes.commit)
74 other = bool(
75 ( action.REVPROP in action.scope
76 and runtime.mode == modes.propchange)
77 or
78 ( action.LOCKS in action.scope
79 and runtime.mode in (modes.lock, modes.unlock))
80 )
81
82 if action.maxbytes and (is_commit or other):
83 from svnmailer import util
84
85 decorator = None
86 generator = cls
87
88 if action.mode == action.TRUNCATE:
89 decorator = util.inherit(TruncatingDecorator, generator)
90
91 elif action.mode == action.URLS:
92 if is_commit:
93 decorator = util.inherit(URLDecorator, generator)
94 if action.truncate:
95 decorator = util.inherit(
96 is_commit and
97 URLTruncatingDecorator or TruncatingDecorator,
98 decorator or generator
99 )
100
101 elif action.mode == action.SPLIT:
102 if action.truncate:
103 decorator = util.inherit(TruncatingDecorator, generator)
104 if is_commit:
105 decorator = util.inherit(
106 SplittingDecorator, decorator or generator
107 )
108
109 if decorator:
110 return decorator(config, groupset, action.maxbytes, action.drop)
111
112 return cls(config, groupset)
113
114
115 -class TextMailNotifier(_mail.MailNotifier):
116 """ Bases class for textual mail notifiers """
117 __implements__ = [_mail.MailNotifier]
118
119 - def composeMail(self):
120 """ Composes the mail
121
122 @return: The senders, the receivers, the mail(s)
123 @rtype: C{tuple}
124 """
125 import cStringIO
126
127 groups = self._groupset.groups
128 sender, to_addrs, headers = self.composeHeaders(groups)
129
130 fp = self.fp = self._getMailWriter(cStringIO.StringIO())
131 self.writeNotification()
132 mails = self._getTextMails('utf-8', {
133 u"quoted-printable": "Q",
134 u"qp" : "Q",
135 u"base64" : "B",
136 u"base 64" : "B",
137 u"8bit" : "8",
138 u"8 bit" : "8",
139 }.get((self.getTransferEncoding() or u'').lower(), '8'))
140
141 for mail in mails:
142 mail.update(headers)
143 yield (
144 sender.encode('utf-8'),
145 [addr.encode('utf-8') for addr in to_addrs],
146 mail
147 )
148
149 fp.close()
150
151
152 - def sendMail(self, sender, to_addr, mail):
153 """ Sends the mail (abstract method) """
154 raise NotImplementedError()
155
156
157 - def _getTextMails(self, charset, enc):
158 """ Returns the text mail(s)
159
160 @param charset: The mail charset
161 @type charset: C{str}
162
163 @param enc: transfer encoding token
164 @type enc: C{str}
165
166 @return: The mail(s)
167 @rtype: C{list} of C{_TextMail}
168 """
169 return [_TextMail(
170 self.getMailSubject(), self.fp.getvalue(), charset, enc
171 )]
172
173
174 - def _getMailWriter(self, fp):
175 """ Returns a mail writer
176
177 @param fp: The stream to wrap
178 @type fp: file like object
179
180 @return: The file object
181 @rtype: file like object
182 """
183 from svnmailer import stream
184
185 return stream.UnicodeStream(fp)
186
187
189 """ Splits the content between diffs, if it gets loo long
190
191 @ivar final_fp: Actual stream object containg all data
192 @type final_fp: file like object
193
194 @ivar max_notification_size: Maximum size of one mail content
195 @type max_notification_size: C{int}
196
197 @ivar drop: maximum number of mails
198 @type drop: C{int}
199
200 @ivar drop_fp: The alternate summary stream
201 @type drop_fp: file like object
202 """
203
204 - def __init__(self, config, groupset, maxsize, drop):
205 """ Initialization
206
207 @param maxsize: The maximum number of bytes that should be written
208 into one mail
209 @type maxsize: C{int}
210
211 @param drop: maximum number of mails
212 @type drop: C{int}
213 """
214 self.__super = super(self.__decorator_class, self)
215 self.__super.__init__(config, groupset, maxsize, drop)
216 self.max_notification_size = maxsize
217 self.drop = drop
218
219
220 - def _getTextMails(self, charset, enc):
221 """ Returns the text mail(s) """
222 self._flushToFinalStream(split = True)
223 stream = self.final_fp
224
225 nummails = stream.getPartCount()
226 if nummails == 1:
227 yield _TextMail(
228 self.getMailSubject(), stream.getPart(0), charset, enc
229 )
230 elif self.drop and nummails > self.drop:
231 self.drop_fp.write((
232 u"\n[This commit notification would consist of %d parts, "
233 u"\nwhich exceeds the limit of %d ones, so it was shortened "
234 u"to the summary.]\n" % (nummails, self.drop)
235 ).encode("utf-8"))
236
237 yield _TextMail(
238 self.getMailSubject(), self.drop_fp.getvalue(), charset, enc
239 )
240 else:
241 for idx in range(nummails):
242 yield _TextMail(
243 self.getMailSubject(u"[%d/%d]" % (idx + 1, nummails)),
244 stream.getPart(idx), charset, enc
245 )
246
247 self.drop_fp.close()
248 self.final_fp.close()
249
250
260
261
269
270
272 """ write the stuff to the real stream """
273 self.__super.writePathList()
274 self.final_fp.write(self.fp.getvalue())
275 self.fp.seek(0)
276 self.fp.truncate()
277
278 if self.final_fp.current > self.max_notification_size:
279 self.final_fp.write("\n")
280 self.final_fp.split()
281
282
284 """ Flushes the current content to the final stream
285
286 @param split: Should split regardless of the current size?
287 @type split: C{bool}
288 """
289 value = self.fp.getvalue()
290 self.fp.seek(0)
291 self.fp.truncate()
292
293 supposed = self.final_fp.current + len(value)
294 if split or supposed > self.max_notification_size:
295 self.final_fp.write("\n")
296 self.final_fp.split()
297
298 self.final_fp.write(value)
299
300
301 - def writeContentDiff(self, change):
302 """ write the stuff to the real stream """
303 self.__super.writeContentDiff(change)
304 self._flushToFinalStream()
305
306
311
312
314 """ Truncates the mail body after n bytes """
315
316 - def __init__(self, config, groupset, maxsize, drop):
317 """ Initialization
318
319 @param maxsize: The maximum number of bytes that should be written
320 into one mail
321 @type maxsize: C{int}
322
323 @param drop: maximum number of mails
324 @type drop: C{int}
325 """
326 self.__super = super(self.__decorator_class, self)
327 self.__super.__init__(config, groupset, maxsize, drop)
328 self.max_notification_size = maxsize
329
330
337
338
340 """ Shows only the urls, if the mail gets too long
341
342 @ivar url_fp: The alternative stream
343 @type url_fp: file like object
344 """
345
346 - def __init__(self, config, groupset, maxsize, drop):
347 """ Initialization
348
349 @param maxsize: The maximum number of bytes that should be written
350 into one mail
351 @type maxsize: C{int}
352
353 @param drop: maximum number of mails
354 @type drop: C{int}
355 """
356 self.__super = super(self.__decorator_class, self)
357 self.__super.__init__(config, groupset, maxsize, drop)
358 self.max_notification_size = maxsize
359
360
375
376
383
384
391
392
394 """ Writes the commit path list """
395 self.__super.writePathList()
396 old_fp, self.fp = (self.fp, self.url_fp)
397 self.__super.writePathList()
398 self.fp = old_fp
399
400
402 """ Writes the commit diffs """
403 if self.getUrl(self.config):
404 self.url_fp.write(
405 u"\n[This mail would be too long, it was shortened to "
406 u"contain the URLs only.]\n\n"
407 )
408 else:
409 self.url_fp.write(
410 u"\n[This mail would be too long, it should contain the "
411 u"URLs only, but no browser base url was configured...]\n"
412 )
413
414 self.__super.writeDiffList()
415
416
417 - def writeContentDiff(self, change):
418 """ Writes the content diff for a particular change """
419 self.__super.writeContentDiff(change)
420
421 url = self.getContentDiffUrl(self.config, change)
422 if url is not None:
423 old_fp, self.fp = (self.fp, self.url_fp)
424 self.__super.writeContentDiffAction(change)
425 self.fp = old_fp
426 self.url_fp.write("URL: %s\n" % url)
427 self.url_fp.write("\n")
428
429
431 """ Truncates the mail body after n bytes """
432
433 - def __init__(self, config, groupset, maxsize, drop):
434 """ Initialization
435
436 @param maxsize: The maximum number of bytes that should be written
437 into one mail
438 @type maxsize: C{int}
439
440 @param drop: maximum number of mails
441 @type drop: C{int}
442 """
443 self.__super = super(self.__decorator_class, self)
444 self.__super.__init__(config, groupset, maxsize, drop)
445 self.max_notification_size = maxsize
446
447
458
459
460 from email import MIMENonMultipart
461 -class _TextMail(MIMENonMultipart.MIMENonMultipart):
462 """ A text mail class (email.MIMEText produces undesired results) """
463
464 - def __init__(self, subject, body, charset, enc = 'Q'):
465 """ Initialization
466
467 @param subject: The subject to use
468 @type subject: C{str}
469
470 @param body: The mail body
471 @type body: C{str}
472
473 @param charset: The charset, the body is encoded
474 @type charset: C{str}
475
476 @param enc: transfer encoding token (C{Q}, C{B} or C{8})
477 @type enc: C{str}
478 """
479 from email import Charset, Header
480
481 _charset = Charset.Charset(charset)
482 _charset.body_encoding = {
483 'Q': Charset.QP, 'B': Charset.BASE64, '8': None
484 }.get(str(enc), Charset.QP)
485 _charset.header_encoding = Charset.QP
486 MIMENonMultipart.MIMENonMultipart.__init__(
487 self, 'text', 'plain', charset = charset
488 )
489 self.set_payload(body, _charset)
490 self['Subject'] = Header.Header(subject, 'iso-8859-1')
491
492
493 - def dump(self, fp):
494 """ Serializes the mail into a descriptor
495
496 @param fp: The file object
497 @type fp: file like object
498 """
499 from email import Generator
500
501 class MyGenerator(Generator.Generator):
502 """ Derived generator to handle the payload """
503
504 def _handle_text_plain(self, msg):
505 """ handle the payload """
506 payload = msg.get_payload()
507 cset = msg.get_charset()
508 if cset:
509 enc = cset.get_body_encoding()
510 if enc == 'quoted-printable':
511 import binascii
512 payload = binascii.b2a_qp(payload, istext = True)
513 elif enc == 'base64':
514 payload = payload.encode('base64')
515 self.write(payload)
516
517 generator = MyGenerator(fp, mangle_from_ = False)
518 generator.flatten(self, unixfrom = False)
519
520
521 - def update(self, headers):
522 """ Update the header set of the mail
523
524 @param headers: The new headers
525 @type headers: C{dict}
526 """
527 for name, value in headers.items():
528 self[name] = value
529