Package svnmailer :: Package notifier :: Module _textmail
[hide private]

Source Code for Module svnmailer.notifier._textmail

  1  # -*- coding: utf-8 -*- 
  2  # pylint: disable-msg = C0103, W0201, W0232, W0233 
  3  # 
  4  # Copyright 2004-2006 André Malo or his licensors, as applicable 
  5  # 
  6  # Licensed under the Apache License, Version 2.0 (the "License"); 
  7  # you may not use this file except in compliance with the License. 
  8  # You may obtain a copy of the License at 
  9  # 
 10  #     http://www.apache.org/licenses/LICENSE-2.0 
 11  # 
 12  # Unless required by applicable law or agreed to in writing, software 
 13  # distributed under the License is distributed on an "AS IS" BASIS, 
 14  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 15  # See the License for the specific language governing permissions and 
 16  # limitations under the License. 
 17  """ 
 18  text based email notifier 
 19  """ 
 20  __author__    = "André Malo" 
 21  __docformat__ = "epytext en" 
 22  __all__       = ['getNotifier'] 
 23   
 24  # global imports 
 25  from svnmailer.notifier import _mail 
 26   
 27   
28 -def getNotifier(cls, config, groupset):
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
51 -def decorateNotifier(cls, action, config, groupset):
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
188 -class SplittingDecorator(object):
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
251 - def _getMailWriter(self, fp):
252 """ Returns a splitting mail writer """ 253 from svnmailer import stream 254 import cStringIO 255 256 self.final_fp = stream.SplittingStream(tempdir = self.getTempDir()) 257 self.drop_fp = self.__super._getMailWriter(cStringIO.StringIO()) 258 259 return self.__super._getMailWriter(fp)
260 261
262 - def writeMetaData(self):
263 """ write meta data to drop_fp as well """ 264 old_fp, self.fp = (self.fp, self.drop_fp) 265 self.__super.writeMetaData() 266 self.fp = old_fp 267 268 self.__super.writeMetaData()
269 270
271 - def writePathList(self):
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) # don't use reset on possible codec StreamWriters... 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
283 - def _flushToFinalStream(self, split = False):
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
307 - def writePropertyDiffs(self, diff_tokens, change):
308 """ write the stuff to the real stream """ 309 self.__super.writePropertyDiffs(diff_tokens, change) 310 self._flushToFinalStream()
311 312
313 -class TruncatingDecorator(object):
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
331 - def _getMailWriter(self, fp):
332 """ Returns a truncating mail writer """ 333 from svnmailer import stream 334 335 fp = stream.TruncatingStream(fp, self.max_notification_size, True) 336 return self.__super._getMailWriter(fp)
337 338
339 -class URLDecorator(object):
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
361 - def _getMailWriter(self, fp):
362 """ Returns a "urling" mail writer """ 363 import cStringIO 364 from svnmailer import stream 365 366 fp = stream.TruncatingStream( 367 self.__super._getMailWriter(fp), 368 self.max_notification_size 369 ) 370 self.url_fp = self.__super._getMailWriter( 371 cStringIO.StringIO() 372 ) 373 374 return stream.CuckooStream(fp)
375 376
377 - def writeNotification(self):
378 """ Writes the notification body """ 379 self.__super.writeNotification() 380 381 if self.fp.getTruncatedLineCount(): 382 self.fp.replaceStream(self.url_fp)
383 384
385 - def writeMetaData(self):
386 """ Writes the commit metadata output """ 387 self.__super.writeMetaData() 388 old_fp, self.fp = (self.fp, self.url_fp) 389 self.__super.writeMetaData() 390 self.fp = old_fp
391 392
393 - def writePathList(self):
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
401 - def writeDiffList(self):
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
430 -class URLTruncatingDecorator(object):
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
448 - def _getMailWriter(self, fp):
449 """ Returns a truncating mail writer """ 450 from svnmailer import stream 451 452 fp = self.__super._getMailWriter(fp) 453 self.url_fp = stream.TruncatingStream( 454 self.url_fp, self.max_notification_size, True 455 ) 456 457 return fp
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