1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 """
18 email notifier
19 """
20 __author__ = "André Malo"
21 __docformat__ = "epytext en"
22 __all__ = ['Error', 'InvalidMailOption', 'getNotifier']
23
24
25 from svnmailer.notifier import _mail
26
27
29 """ Base exception for this module """
30 pass
31
33 """ Invalid Multipart mail option """
34 pass
35
36
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
103
104
106 """ Bases class for mail notifiers using attachments for the diffs """
107 __implements__ = [_mail.MailNotifier]
108
109
110 - def __init__(self, config, groupset, *args, **kwargs):
114
115
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
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
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
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
193 for diff in self.diff_file_list:
194 parts.append(diff.toMIMEPart())
195 del diff
196
197 return [_MultiMail(self.getMailSubject(), parts)]
198
199
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
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
259
260
266
267
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
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
313
314
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
374
375
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
399
400
407
408
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
437
438
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
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
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
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
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
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
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
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
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
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
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
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
670 """ A multimail class """
671
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
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
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
725 name = name.decode('utf-8')
726 names = self._encodeRfc2184(name, dosplit = False)
727
728
729 if names[0] == name:
730 tparam['name'] = dparam['filename'] = name.encode('utf-8')
731
732
733 elif len(names) == 1:
734 tparam['name*'] = dparam['filename*'] = names[0]
735
736
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
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:
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
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