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