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