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

Source Code for Module svnmailer.notifier._multimail

  1  # -*- coding: utf-8 -*- 
  2  # pylint: disable-msg = W0201, W0233, W0613 
  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  email notifier 
 19  """ 
 20  __author__    = "André Malo" 
 21  __docformat__ = "epytext en" 
 22  __all__       = ['Error', 'InvalidMailOption', 'getNotifier'] 
 23   
 24  # global imports 
 25  from svnmailer.notifier import _mail 
 26   
 27  # exceptions 
28 -class Error(Exception):
29 """ Base exception for this module """ 30 pass
31
32 -class InvalidMailOption(Error):
33 """ Invalid Multipart mail option """ 34 pass
35 36
37 -def getNotifier(cls, config, groupset):
38 """ Returns an initialized notifier or nothing 39 40 @param cls: The notifier base class to use 41 @type cls: C{class} 42 43 @param config: The svnmailer config 44 @type config: C{svnmailer.settings.Settings} 45 46 @param groupset: The groupset to process 47 @type groupset: C{list} 48 49 @return: The list of notifiers (containing 0 or 1 member) 50 @rtype: C{list} 51 """ 52 from svnmailer import util 53 54 return [decorateNotifier( 55 util.inherit(cls, MultiMailNotifier), 56 groupset.groups[0].long_mail_action, config, groupset 57 )]
58 59
60 -def decorateNotifier(cls, action, config, groupset):
61 """ Decorates the notifier class (or not) 62 63 @param cls: The notifier class 64 @type cls: C{class} 65 66 @param action: The configured action 67 @type action: C{unicode} 68 69 @param config: The svnmailer config 70 @type config: C{svnmailer.settings.Settings} 71 72 @param groupset: The groupset to process 73 @type groupset: C{list} 74 75 @return: The decorated class or C{None} 76 @rtype: C{class} 77 """ 78 if action and action.maxbytes: 79 from svnmailer import util 80 81 decorator = None 82 generator = cls 83 84 if action.mode == action.TRUNCATE: 85 decorator = util.inherit(TruncatingDecorator, generator) 86 87 elif action.mode == action.URLS: 88 decorator = util.inherit(URLDecorator, generator) 89 if action.truncate: 90 decorator = util.inherit(URLTruncatingDecorator, decorator) 91 92 elif action.mode == action.SPLIT: 93 decorator = util.inherit(SplittingDecorator, generator) 94 if action.truncate: 95 decorator = util.inherit( 96 SplittingTruncatingDecorator, decorator 97 ) 98 99 if decorator: 100 return decorator(config, groupset, action.maxbytes, action.drop) 101 102 return cls(config, groupset)
103 104
105 -class MultiMailNotifier(_mail.MailNotifier):
106 """ Bases class for mail notifiers using attachments for the diffs """ 107 __implements__ = [_mail.MailNotifier] 108 109 # need this (variable args) for deco classes
110 - def __init__(self, config, groupset, *args, **kwargs):
111 """ Initialization """ 112 _mail.MailNotifier.__init__(self, config, groupset) 113 self.mctype, self.mdispo = self._parseMailType()
114 115
116 - def _parseMailType(self):
117 """ Returns the multimail options 118 119 @return: The diff content type and disposition 120 @rtype: C{tuple} 121 """ 122 from svnmailer import util 123 124 ctype = 'text/plain' 125 dispo = 'inline' 126 for option in (self.config.mail_type or '').split()[1:]: 127 try: 128 name, value = [val.lower() for val in option.split('=')] 129 if name == u'type': 130 ctype = util.parseContentType(value) 131 if not ctype: 132 raise ValueError( 133 "invalid multimail type specification %r" % value 134 ) 135 ctype = ctype[0] 136 elif name == u'disposition': 137 if value not in (u'inline', 'attachment'): 138 raise ValueError( 139 "invalid disposition specification %r" % value 140 ) 141 dispo = value 142 else: 143 raise ValueError("unknown multimail option %r" % option) 144 except ValueError, exc: 145 raise InvalidMailOption(str(exc)) 146 147 return (ctype, dispo)
148 149
150 - def composeMail(self):
151 """ Composes the mail 152 153 @return: The senders, the receivers, the mail(s) 154 @rtype: C{tuple} 155 """ 156 import cStringIO 157 158 groups = self._groupset.groups 159 sender, to_addrs, headers = self.composeHeaders(groups) 160 161 self.diff_file_list = [] 162 self.fp = self._getMailWriter(cStringIO.StringIO()) 163 self.writeNotification() 164 165 # create the mails 166 mails = self._getMultiMails() 167 168 for mail in mails: 169 mail.update(headers) 170 yield ( 171 sender.encode('utf-8'), 172 [addr.encode('utf-8') for addr in to_addrs], 173 mail 174 ) 175 176 self.diff_file_list = [] 177 self.fp.close()
178 179
180 - def sendMail(self, sender, to_addr, mail):
181 """ Sends the mail (abstract method) """ 182 raise NotImplementedError()
183 184
185 - def _getMultiMails(self):
186 """ Returns the multipart mail(s) 187 188 @return: The mail(s) 189 @rtype: C{list} of C{_MultiMail} 190 """ 191 parts = [_SinglePart(self.fp.getvalue(), encoding = 'utf-8')] 192 diff = None # avoid UnboundLocalError if the loop is not run 193 for diff in self.diff_file_list: 194 parts.append(diff.toMIMEPart()) 195 del diff # cleanup 196 197 return [_MultiMail(self.getMailSubject(), parts)]
198 199
200 - def _getMailWriter(self, fp):
201 """ Returns a mail writer 202 203 @param fp: The stream to wrap 204 @type fp: file like object 205 206 @return: The file object 207 @rtype: file like object 208 """ 209 from svnmailer import stream 210 211 return stream.UnicodeStream(fp)
212 213
214 - def writeDiffList(self):
215 """ Writes the commit diffs into temp files """ 216 from svnmailer import stream 217 218 # drop all stuff between diffs 219 old_fp, self.fp = (self.fp, stream.DevNullStream()) 220 221 super(MultiMailNotifier, self).writeDiffList() 222 223 # restore the descriptor 224 self.fp.close() 225 self.fp = old_fp
226 227
228 - def writeContentDiff(self, change, raw = False):
229 """ Dump the diff into a separate file """ 230 if change.isDirectory(): 231 return 232 233 tmpfile = self.getTempFile() 234 old_fp, self.fp = (self.fp, self._getDiffStream(tmpfile.fp)) 235 236 raw = True 237 super(MultiMailNotifier, self).writeContentDiff(change, raw) 238 239 self.fp.close() 240 self.diff_file_list.append(DiffDescriptor(self, tmpfile, change)) 241 242 self.fp = old_fp
243 244
245 - def writePropertyDiffs(self, diff_tokens, change, raw = False):
246 """ Dump the property diff into a separate file """ 247 tmpfile = self.getTempFile() 248 old_fp, self.fp = (self.fp, self._getDiffStream(tmpfile.fp)) 249 250 raw = True 251 super(MultiMailNotifier, self).writePropertyDiffs( 252 diff_tokens, change, raw 253 ) 254 255 self.fp.close() 256 self.diff_file_list.append(DiffDescriptor(self, tmpfile, change, True)) 257 258 self.fp = old_fp
259 260
261 - def _getDiffStream(self, fp):
262 """ Returns the (possibly decorated) diff stream """ 263 from svnmailer import stream 264 265 return stream.BinaryOrUnicodeStream(fp)
266 267
268 -class SplittingDecorator(object):
269 """ Splits the content between diffs if it gets too long 270 271 @ivar max_notification_size: Maximum size of one mail content 272 @type max_notification_size: C{int} 273 274 @ivar drop: maximum number of mails 275 @type drop: C{int} 276 277 @ivar drop_fp: The alternate summary stream 278 @type drop_fp: file like object 279 """ 280
281 - def __init__(self, settings, groupset, maxsize, drop):
282 """ Initialization 283 284 @param maxsize: The maximum number of bytes that should be written 285 into one mail 286 @type maxsize: C{int} 287 288 @param drop: maximum number of mails 289 @type drop: C{int} 290 """ 291 self.__super = super(self.__decorator_class, self) 292 self.__super.__init__(settings, groupset, maxsize, drop) 293 self.max_notification_size = maxsize 294 self.drop = drop
295 296
297 - def _getMailWriter(self, fp):
298 """ inits the drop_fp """ 299 import cStringIO 300 301 self.drop_fp = self.__super._getMailWriter(cStringIO.StringIO()) 302 303 return self.__super._getMailWriter(fp)
304 305
306 - def writeMetaData(self):
307 """ write meta data to drop_fp as well """ 308 old_fp, self.fp = (self.fp, self.drop_fp) 309 self.__super.writeMetaData() 310 self.fp = old_fp 311 312 self.__super.writeMetaData()
313 314
315 - def _getMultiMails(self):
316 """ Returns the multipart mail(s) 317 318 @return: The mail(s) 319 @rtype: C{list} of C{_MultiMail} 320 """ 321 parts = [_SinglePart(self.fp.getvalue(), encoding = 'utf-8')] 322 323 diff = None 324 asize = parts[0].getSize() 325 diffs = [(diff, diff.toMIMEPart().getSize()) 326 for diff in self.diff_file_list 327 ] 328 diff_sizes = [diff[1] for diff in diffs] 329 maxsize = sum(diff_sizes, asize) 330 331 if maxsize <= self.max_notification_size or not diff_sizes: 332 parts.extend([diff.toMIMEPart() for diff in self.diff_file_list]) 333 yield _MultiMail(self.getMailSubject(), parts) 334 else: 335 nummails = 1 336 tsize = asize 337 for size in diff_sizes: 338 tsize += size 339 if tsize > self.max_notification_size: 340 nummails += 1 341 tsize = size 342 343 if self.drop and nummails > self.drop: 344 self.drop_fp.write(( 345 u"\n[This commit notification would consist of %d parts, " 346 u"\nwhich exceeds the limit of %d ones, so it was " 347 u"shortened to the summary.]\n" % (nummails, self.drop) 348 ).encode("utf-8")) 349 350 yield _MultiMail(self.getMailSubject(), [_SinglePart( 351 self.drop_fp.getvalue(), encoding = 'utf-8' 352 )]) 353 else: 354 mcount = 1 355 for diff in diffs: 356 asize += diff[1] 357 if asize > self.max_notification_size: 358 yield _MultiMail( 359 self.getMailSubject(u"[%d/%d]" % (mcount, nummails) 360 ), parts) 361 mcount += 1 362 asize = diff[1] 363 parts = [_SinglePart(( 364 u"[Part %d/%d]\n" % (mcount, nummails) 365 ).encode('utf-8'), encoding = 'utf-8')] 366 parts.append(diff[0].toMIMEPart()) 367 368 if parts: 369 yield _MultiMail( 370 self.getMailSubject(u"[%d/%d]" % (mcount, nummails) 371 ), parts) 372 373 del diff # cleanup
374 375
376 -class SplittingTruncatingDecorator(object):
377 """ split/truncate decorator """ 378
379 - def __init__(self, settings, groupset, maxsize, drop):
380 """ Initialization 381 382 @param maxsize: The maximum number of bytes that should be written 383 into one mail 384 @type maxsize: C{int} 385 386 @param drop: maximum number of mails 387 @type drop: C{int} 388 """ 389 self.__super = super(self.__decorator_class, self) 390 self.__super.__init__(settings, groupset, maxsize, drop)
391 392
393 - def _getMailWriter(self, fp):
394 """ Returns a truncating mail writer """ 395 from svnmailer import stream 396 397 fp = stream.TruncatingStream(fp, self.max_notification_size, True) 398 return self.__super._getMailWriter(fp)
399 400
401 - def _getDiffStream(self, fp):
402 """ Returns the truncating diff stream """ 403 from svnmailer import stream 404 405 fp = stream.TruncatingFileStream(fp, self.max_notification_size, True) 406 return self.__super._getDiffStream(fp)
407 408
409 -class TruncatingDecorator(object):
410 """ Truncates the mail body after n bytes 411 412 @ivar max_notification_size: Maximum size of one mail content 413 @type max_notification_size: C{int} 414 """ 415
416 - def __init__(self, settings, groupset, maxsize, drop):
417 """ Initialization 418 419 @param maxsize: The maximum number of bytes that should be written 420 into one mail 421 @type maxsize: C{int} 422 423 @param drop: maximum number of mails 424 @type drop: C{int} 425 """ 426 self.__super = super(self.__decorator_class, self) 427 self.__super.__init__(settings, groupset, maxsize, drop) 428 self.max_notification_size = maxsize
429 430
431 - def _getMailWriter(self, fp):
432 """ Returns a truncating mail writer """ 433 from svnmailer import stream 434 435 fp = stream.TruncatingStream(fp, self.max_notification_size, True) 436 return self.__super._getMailWriter(fp)
437 438
439 - def _getMultiMails(self):
440 """ Returns the multipart mail(s) 441 442 @return: The mail(s) 443 @rtype: C{list} of C{_MultiMail} 444 """ 445 parts = [_SinglePart(self.fp.getvalue(), encoding = 'utf-8')] 446 447 if self.fp.getTruncatedLineCount(): 448 dlen = len(self.diff_file_list) 449 if dlen: 450 parts.append(_SinglePart(( 451 u"[... %d diffs stripped ...]\n" % dlen 452 ).encode('utf-8'), encoding = 'utf-8')) 453 else: 454 diff = None # avoid UnboundLocalError if the loop is not run 455 asize = parts[0].getSize() 456 mcount = len(self.diff_file_list) 457 for diff in self.diff_file_list: 458 thispart = diff.toMIMEPart() 459 asize += thispart.getSize() 460 if asize > self.max_notification_size: 461 break 462 parts.append(thispart) 463 mcount -= 1 464 del diff # cleanup 465 if mcount: 466 parts.append(_SinglePart(( 467 u"[... %d diffs stripped ...]\n" % mcount 468 ).encode('utf-8'), encoding = 'utf-8')) 469 470 return [_MultiMail(self.getMailSubject(), parts)]
471 472
473 -class URLDecorator(object):
474 """ Shows only the urls, if the mail gets too long 475 476 @ivar url_fp: The alternative stream 477 @type url_fp: file like object 478 479 @ivar do_truncate: truncating mode? 480 @type do_truncate: C{bool} 481 482 @ivar max_notification_size: Maximum size of one mail content 483 @type max_notification_size: C{int} 484 """ 485
486 - def __init__(self, settings, groupset, maxsize, drop):
487 """ Initialization 488 489 @param maxsize: The maximum number of bytes that should be written 490 into one mail 491 @type maxsize: C{int} 492 493 @param drop: maximum number of mails 494 @type drop: C{int} 495 """ 496 self.__super = super(self.__decorator_class, self) 497 self.__super.__init__(settings, groupset, maxsize, drop) 498 self.max_notification_size = maxsize 499 self.do_truncate = False
500 501
502 - def _getMailWriter(self, fp):
503 """ Returns a "urling" mail writer """ 504 import cStringIO 505 from svnmailer import stream 506 507 if self.do_truncate: 508 fp = stream.TruncatingStream( 509 self.__super._getMailWriter(fp), 510 self.max_notification_size, 511 True 512 ) 513 514 self.url_fp = self.__super._getMailWriter(cStringIO.StringIO()) 515 return fp
516 517
518 - def writeContentDiff(self, change):
519 """ Writes the content diff for a particular change """ 520 self.__super.writeContentDiff(change) 521 522 url = self.getContentDiffUrl(self.config, change) 523 if url is not None: 524 old_fp, self.fp = (self.fp, self.url_fp) 525 self.__super.writeContentDiffAction(change) 526 self.fp = old_fp 527 self.url_fp.write("URL: %s\n" % url) 528 self.url_fp.write("\n")
529 530
531 - def _getMultiMails(self):
532 """ Returns the multipart mail(s) 533 534 @return: The mail(s) 535 @rtype: C{list} of C{_MultiMail} 536 """ 537 part0 = _SinglePart(self.fp.getvalue(), encoding = 'utf-8') 538 if self.do_truncate and self.fp.getTruncatedLineCount(): 539 return [_MultiMail(self.getMailSubject(), [part0])] 540 541 diff = None 542 asize = part0.getSize() 543 diff_sizes = [diff.toMIMEPart().getSize() 544 for diff in self.diff_file_list 545 ] 546 maxsize = sum(diff_sizes, asize) 547 del diff 548 549 if maxsize <= self.max_notification_size or not diff_sizes: 550 # fine. 551 self.url_fp.close() 552 return self.__super._getMultiMails() 553 554 if not self.getUrl(self.config): 555 self.fp.write( 556 u"\n[This mail would be too long, it should contain the " 557 u"URLs only, but no browser base url was configured...]\n" 558 ) 559 parts = [_SinglePart(self.fp.getvalue(), encoding = 'utf-8')] 560 else: 561 self.fp.write( 562 u"\n[This mail would be too long, it was shortened to " 563 u"contain the URLs only.]\n" 564 ) 565 parts = [_SinglePart(self.fp.getvalue(), encoding = 'utf-8')] 566 567 if self.do_truncate: 568 import cStringIO 569 from svnmailer import stream 570 tfp = stream.TruncatingStream( 571 cStringIO.StringIO(), 572 self.max_notification_size - parts[0].getSize(), 573 True 574 ) 575 tfp.write(self.url_fp.getvalue()) 576 self.url_fp.close() 577 self.url_fp = tfp 578 579 parts.append( 580 _SinglePart(self.url_fp.getvalue(), encoding = 'utf-8') 581 ) 582 583 self.url_fp.close() 584 return [_MultiMail(self.getMailSubject(), parts)]
585 586
587 -class URLTruncatingDecorator(object):
588 """ Truncates url-only mails """ 589
590 - def __init__(self, settings, groupset, maxsize, drop):
591 """ Initialization 592 593 @param maxsize: The maximum number of bytes that should be written 594 into one mail 595 @type maxsize: C{int} 596 597 @param drop: maximum number of mails 598 @type drop: C{int} 599 """ 600 super(self.__decorator_class, self).__init__( 601 settings, groupset, maxsize, drop 602 ) 603 self.do_truncate = True
604 605
606 -class DiffDescriptor(object):
607 """ Container class to describe a dumped diff """ 608
609 - def __init__(self, notifier, tmpfile, change, propdiff = False):
610 """ Initialization 611 612 @param tmpfile: The tempfile the diff was dumped to 613 @type tmpfile: C{svnmailer.util.TempFile} 614 615 @param change: The change in question 616 @type change: C{svnmailer.subversion.VersionedPathDescriptor} 617 618 @param propdiff: is a property diff? 619 @type propdiff: C{bool} 620 """ 621 self.tmpfile = tmpfile 622 self.change = change 623 self.propdiff = propdiff 624 self.encoding = None 625 self.ctype = notifier.mctype 626 self.dispo = notifier.mdispo 627 628 if not self.propdiff: 629 enc1, enc2 = notifier.getContentEncodings(change, None) 630 if enc1 and enc1 == enc2: 631 self.encoding = enc1
632 633
634 - def getValue(self):
635 """ Returns the diff value 636 637 @return: The value 638 @rtype: C{str} 639 """ 640 return file(self.tmpfile.name, 'rb').read()
641 642
643 - def getSize(self):
644 """ Returns the size of the diff part 645 646 @return: The size 647 @rtype: C{int} 648 """ 649 import os 650 return os.stat(self.tmpfile.name).st_size
651 652
653 - def toMIMEPart(self):
654 """ Returns the diff as MIME part """ 655 import posixpath 656 ext = self.propdiff and 'propchange' or 'diff' 657 name = "%s.%s" % (posixpath.basename(self.change.path), ext) 658 enc = (self.propdiff and [None] or [self.encoding])[0] 659 660 part = _SinglePart(self.getValue(), 661 name = name, encoding = enc, binary = True, ctype = self.ctype, 662 dispo = self.dispo 663 ) 664 665 return part
666 667 668 from email import MIMEMultipart, MIMENonMultipart
669 -class _MultiMail(MIMEMultipart.MIMEMultipart):
670 """ A multimail class """ 671
672 - def __init__(self, subject, parts):
673 """ Initialization 674 675 @param subject: The subject to use 676 @type subject: C{str} 677 678 @param parts: The body parts 679 @type parts: C{list} 680 """ 681 from email import Header 682 683 MIMEMultipart.MIMEMultipart.__init__(self) 684 self['Subject'] = Header.Header(subject, 'iso-8859-1') 685 for part in parts: 686 self.attach(part)
687 688
689 - def dump(self, fp):
690 """ Serializes the mail into a descriptor 691 692 @param fp: The file object 693 @type fp: file like object 694 """ 695 from email import Generator 696 697 generator = Generator.Generator(fp, mangle_from_ = False) 698 generator.flatten(self, unixfrom = False)
699 700
701 - def update(self, headers):
702 """ Update the header set of the mail 703 704 @param headers: The new headers 705 @type headers: C{dict} 706 """ 707 for name, value in headers.items(): 708 self[name] = value
709 710
711 -class _SinglePart(MIMENonMultipart.MIMENonMultipart):
712 """ A single part of a multipart mail """ 713
714 - def __init__(self, body, name = None, encoding = None, binary = False, 715 ctype = 'text/plain', dispo = 'inline'):
716 """ Initialization 717 718 @param body: The body 719 @type body: C{str} 720 """ 721 tparam = {} 722 dparam = {} 723 if name is not None: 724 # Deal with RFC 2184 encoding 725 name = name.decode('utf-8') 726 names = self._encodeRfc2184(name, dosplit = False) 727 728 # safe name 729 if names[0] == name: 730 tparam['name'] = dparam['filename'] = name.encode('utf-8') 731 732 # encoded simple 733 elif len(names) == 1: 734 tparam['name*'] = dparam['filename*'] = names[0] 735 736 # encoded and splitted (turned off above [dosplit = False]) 737 else: 738 for idx, name in enumerate(names): 739 tparam['name*%d*' % idx] = name 740 dparam['filename*%d*' % idx] = name 741 742 if encoding is not None: 743 tparam['charset'] = encoding 744 745 maintype, subtype = ctype.encode('us-ascii').split('/') 746 MIMENonMultipart.MIMENonMultipart.__init__( 747 self, maintype, subtype, **tparam 748 ) 749 self.set_payload(body) 750 self.add_header('Content-Disposition', 751 dispo.encode('us-ascii'), **dparam) 752 if binary: 753 cte = 'binary' 754 else: 755 cte = '7bit' 756 try: 757 body.encode('us-ascii') 758 except UnicodeError: 759 cte = '8bit' 760 self['Content-Transfer-Encoding'] = cte 761 del self['MIME-Version']
762 763
764 - def _encodeRfc2184(self, value, dosplit = False):
765 """ Encode a string (parameter value) according to RFC 2184 766 767 @param value: The value to encode 768 @type value: C{unicode} 769 770 @param dosplit: Allow long parameter splitting? (Note that is not 771 widely supported...) 772 @type dosplit: C{bool} 773 774 @return: The list of encoded values 775 @rtype: C{list} 776 """ 777 encode = False 778 try: 779 value.encode('us-ascii') 780 except UnicodeError: 781 encode = True 782 else: 783 if u'"' in value or (len(value) > 78 and dosplit): 784 encode = True 785 786 values = [] 787 if encode: 788 import urllib 789 value = "utf-8''%s" % urllib.quote(value.encode('utf-8')) 790 if dosplit: # doesn't seem to be supported that much ... 791 while len(value) > 78: 792 slen = value[77] == '%' and 80 or ( 793 value[76] == '%' and 79 or 78 794 ) 795 values.append(value[:slen]) 796 value = value[slen + 1:] 797 values.append(value) 798 799 return values
800 801
802 - def getSize(self):
803 """ Serializes the mail into a descriptor 804 805 @return: The size of the serialized object 806 @rtype: C{int} 807 """ 808 from email import Generator 809 from svnmailer import stream 810 811 fp = stream.CountStream() 812 generator = Generator.Generator(fp, mangle_from_ = False) 813 generator.flatten(self, unixfrom = False) 814 815 return fp.size
816