Package svnmailer :: Package notifier :: Module _mail

[hide private]

# Source Code for Module svnmailer.notifier._mail

      1  # -*- coding: utf-8 -*-
      2  # pylint: disable-msg = 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 base module
     19  """
     20  __author__    = "André Malo"
     21  __docformat__ = "epytext en"
     22  __all__       = ["MailNotifier"]
     23
     24  # global imports
     25  from svnmailer.notifier import _text
     26
     27  # Exceptions
     28 -class Error(Exception):
     29      """ Base exception for this module """
     30      pass
     31
     32 -class NoRecipientsError(Error):
     33      """ No recipients found """
     34      pass
     35
     36
     37 -class MailNotifier(_text.TextNotifier):
     38      """ Bases class for mail (like) notifiers
     39
     40          @ivar _header_re: Pattern for header name checking
     41          @type _header_re: sre pattern or C{None}
     42      """
     43      __implements__ = [_text.TextNotifier]
     44
     45      COMMIT_SUBJECT  = u"%(prefix)s r%(revision)s %(part)s - %(files/dirs)s"
     46      REVPROP_SUBJECT = u"%(prefix)s r%(revision)s - %(property)s"
     47      LOCK_SUBJECT    = u"%(prefix)s %(files/dirs)s"
     48      UNLOCK_SUBJECT  = u"%(prefix)s %(files/dirs)s"
     49
     50      # need this (variable args) for deco classes
     51 -    def __init__(self, config, groupset, *args, **kwargs):
     52          """ Initialization """
     53          _text.TextNotifier.__init__(self, config, groupset)
     54          self._header_re = None
     55
     56
     57 -    def run(self):
     58          """ Send notification as mail """
     59          import sys
     60
     61          try:
     62              for mail in self.getMails():
     63                  if self._settings.runtime.debug:
     64                      mail[2].dump(sys.stdout)
     65                  else:
     66                      self.sendMail(mail[0], mail[1], mail[2])
     67          except NoRecipientsError:
     68              if self._settings.runtime.debug:
     69                  sys.stdout.write("No recipients found for %s\n" %
     70                      ', '.join([
     71                          "[%s]" % group._name.encode('utf-8')
     72                          for group in self._groupset.groups
     73                      ])
     74                  )
     75
     76
     77 -    def getMails(self):
     78          """ Returns the composed mail(s)
     79
     80              @return: The mails
     81              @rtype: generator
     82          """
     83          for mail in self.composeMail():
     84              yield mail
     85
     86
     87 -    def writeNotification(self):
     88          """ Writes the whole diff notification body """
     89          from svnmailer.settings import modes
     90
     91          mode = self._settings.runtime.mode
     92
     93          if mode == modes.commit:
     94              self.writeMetaData()
     95              self.writePathList()
     96              self.writeDiffList()
     97          elif mode == modes.propchange:
     98              self.writeRevPropData()
     99          elif mode in (modes.lock, modes.unlock):
    100              self.writeLockData()
    101          else:
    102              raise AssertionError("Unknown runtime.mode %r" % (mode,))
    103
    104
    105 -    def getTransferEncoding(self):
    106          """ Returns the transfer encoding to use
    107
    108              @return: The configured value
    109              @rtype: C{unicode}
    110          """
    111          return self.config.mail_transfer_encoding
    112
    113
    114 -    def sendMail(self, sender, to_addr, mail):
    115          """ Sends the mail
    116
    117              @param sender: The mail sender (envelope from)
    118              @type sender: C{str}
    119
    120              @param to_addr: The receivers
    121              @type to_addr: C{list}
    122
    123              @param mail: The mail object
    124              @type mail: C{_TextMail}
    125          """
    126          raise NotImplementedError()
    127
    128
    129 -    def composeMail(self):
    130          """ Composes the mail
    131
    132              @return: The senders, the receivers, the mail(s)
    133              @rtype: C{tuple}
    134          """
    135          raise NotImplementedError()
    136
    137
    138 -    def getBasicHeaders(self):
    139          """ Returns the basic headers
    140
    141              @return: The headers
    142              @rtype: C{dict}
    143          """
    144          from email import Utils, Header
    145          from svnmailer import version
    146
    147          return {
    148              'X-Mailer': Header.Header(
    149                  ("svnmailer-%s" % version).decode('utf-8'), 'iso-8859-1'
    150              ),
    151              'Date': Utils.formatdate(),
    152          }
    153
    154
    155 -    def composeHeaders(self, groups):
    156          """ Compose the informational headers of the mail
    157
    158              @param groups: The groups to process
    159              @type groups: C{list}
    160
    161              @return: sender (C{unicode}), recipients (C{list}), headers
    162                  (C{dict})
    163              @rtype: C{tuple}
    164          """
    165          from email import Header
    166
    167          sender, from_addrs, reply_to, to_addrs, to_fakes, bcc_addrs = \
    168              self.getMailAddresses(groups)
    169          recipients = to_addrs + bcc_addrs
    170          if not recipients:
    171              raise NoRecipientsError()
    172
    173          headers = self.getBasicHeaders()
    174          headers['From'] = ', '.join(from_addrs)
    175
    176          if self._settings.general.debug_all_mails_to:
    177              headers['X-Supposed-Recipients'] = \
    178                  Header.Header(', '.join(recipients))
    179              recipients = self._settings.general.debug_all_mails_to
    180          if to_addrs:
    181              headers['To'] = Header.Header(', '.join(to_addrs))
    182          elif to_fakes:
    183              headers['To'] = Header.Header(', '.join(to_fakes))
    184
    185          if reply_to:
    186              headers['Reply-To'] = Header.Header(', '.join(reply_to))
    187          if len(from_addrs) > 1:
    188              # TODO: make Sender configurable?
    189              headers['Sender'] = Header.Header(from_addrs[0])
    190
    191          headers.update(self.getCustomHeaders(groups))
    192
    193          # TODO: generate message-id (using configured format string)?
    194
    195          if self._settings.runtime.debug:
    196              headers['X-Config-Groups'] = Header.Header(', '.join([
    197                  "[%s]" % group._name for group in groups
    198              ]), 'iso-8859-1')
    199
    200          return (sender, recipients, headers)
    201
    202
    203 -    def getCustomHeaders(self, groups):
    204          """ Returns the custom headers
    205
    206              @param groups: The groups to process
    207              @type groups: C{list}
    208
    209              @return: The custom headers
    210              @rtype: C{dict}
    211          """
    212          import re
    213          from email import Header
    214
    215          header_re = self._header_re
    216          if not header_re:
    217              header_re = self._header_re = re.compile("[^%s]" %
    218                  re.escape(u''.join([
    219                      # RFC 2822, 3.6.8.
    220                      chr(num) for num in range(33,127) if num != 58
    221                  ]))
    222              )
    223
    224          custom = {}
    225          for group in [group for group in groups if group.custom_header]:
    226              tup = group.custom_header.split(None, 1)
    227              custom.setdefault(
    228                  'X-%s' % header_re.sub("", tup[0]), []
    229              ).extend(tup[1:])
    230
    231          return dict([(name,
    232              Header.Header(', '.join(values), 'iso-8859-1')
    233          ) for name, values in custom.items()])
    234
    235
    236 -    def getMailAddresses(self, groups):
    237          """ Returns the substituted mail addresses (from/to/reply-to)
    238
    239              @param groups: The groups to process
    240              @type groups: C{list}
    241
    242              @return: The address lists (sender, from, reply-to, to, to-fake,
    243                       bcc)
    244              @rtype: C{tuple}
    245          """
    246          from_addrs = []
    247          to_addrs = []
    248          to_fakes = []
    249          bcc_addrs = []
    250          reply_to = []
    251
    252          sender = None
    253          for group in groups:
    254              from_addrs.extend(group.from_addr or [])
    255              to_addrs.extend(group.to_addr or [])
    256              bcc_addrs.extend(group.bcc_addr or [])
    257              to_fakes.extend(group.to_fake and [group.to_fake] or [])
    258              reply_to.extend(
    259                  group.reply_to_addr and [group.reply_to_addr] or []
    260              )
    261
    262          from_addrs = dict.fromkeys(from_addrs).keys() or [
    263              (self.getAuthor() and self.getAuthor().decode('utf-8', 'replace'))
    264              or u'no_author'
    265          ]
    266          to_addrs = dict.fromkeys(to_addrs).keys()
    267          bcc_addrs = dict.fromkeys(bcc_addrs).keys()
    268          reply_to = dict.fromkeys(reply_to).keys()
    269          if to_addrs:
    270              to_fakes = []
    271          else:
    272              to_fakes = dict.fromkeys(to_fakes).keys()
    273
    274          return (
    275              sender or from_addrs[0], from_addrs, reply_to,
    276              to_addrs, to_fakes, bcc_addrs
    277          )
    278
    279
    280 -    def getMailSubject(self, countprefix = None):
    281          """ Returns the subject
    282
    283              @param countprefix: Optional countprefix (inserted after the rev
    284                  number)
    285              @type countprefix: C{unicode}
    286
    287              @return: The subject line
    288              @rtype: C{unicode}
    289          """
    290          from svnmailer import util
    291          from svnmailer.settings import modes
    292
    293          runtime = self._settings.runtime
    294          groups, changeset = (self._groupset.groups, self._groupset.changes[:])
    295          xset = self._groupset.xchanges
    296          if xset:
    297              changeset.extend(xset)
    298
    299          max_length   = max(0, groups[0].max_subject_length)
    300          short_length = max_length or 255 # for files/dirs
    301
    302          template, mode = {
    303              modes.commit:     (self.COMMIT_SUBJECT,  'commit',   ),
    304              modes.propchange: (self.REVPROP_SUBJECT, 'propchange'),
    305              modes.lock:       (self.LOCK_SUBJECT,    'lock',     ),
    306              modes.unlock:     (self.UNLOCK_SUBJECT,  'unlock',   ),
    307          }[runtime.mode]
    308
    309          template = getattr(groups[0], "%s_subject_template" % mode) \
    310              or template
    311
    312          params = {
    313              'prefix'  : getattr(groups[0], "%s_subject_prefix" % mode),
    314              'part'    : countprefix,
    315              'files'   : self._getPrefixedFiles(changeset),
    316              'dirs'    : self._getPrefixedDirectories(changeset),
    317          }
    318
    319          # We may try twice, first with files/dirs = files
    320          # If the result is too long, we do again with files/dirs = dirs
    321          def dosubject(param):
    322              """ Returns the subject """
    323              # set files/dirs, substitute, normalize WS
    324              params['files/dirs'] = params[param]
    325              cparams = params.copy()
    326              cparams.update(groups[0]._sub_.dict())
    327              return " ".join(util.substitute(template, cparams).split())
    328
    329          subject = dosubject('files')
    330          if len(subject) > short_length:
    331              subject = dosubject('dirs')
    332
    333          # reduce to the max ...
    334          if max_length and len(subject) > max_length:
    335              subject = "%s..." % subject[:max_length - 3]
    336
    337          return subject
    338
    339
    340 -    def _getPrefixedDirectories(self, changeset):
    341          """ Returns the longest common directory prefix
    342
    343              @param changeset: The change set
    344              @type changeset: list
    345
    346              @return: The common dir and the path list, human readable
    347              @rtype: C{unicode}
    348          """
    349          import posixpath
    350          from svnmailer import util
    351
    352          dirs = dict.fromkeys([
    353              "%s/" % (change.isDirectory() and
    354                  [change.path] or [posixpath.dirname(change.path)])[0]
    355              for change in changeset
    356          ]).keys()
    357
    358          common, dirs = util.commonPaths(dirs)
    359          dirs.sort()
    360
    361          return self._getPathString(common, dirs)
    362
    363
    364 -    def _getPrefixedFiles(self, changeset):
    365          """ Returns the longest common path prefix
    366
    367              @param changeset: The change set
    368              @type changeset: list
    369
    370              @return: The common dir and the path list, human readable
    371              @rtype: C{unicode}
    372          """
    373          from svnmailer import util
    374
    375          paths = dict.fromkeys([
    376              change.isDirectory() and "%s/" % change.path or change.path
    377              for change in changeset
    378          ]).keys()
    379
    380          common, paths = util.commonPaths(paths)
    381          paths.sort()
    382
    383          return self._getPathString(common, paths)
    384
    385
    386 -    def _getPathString(self, prefix, paths):
    387          """ Returns the (possibly) prefixed paths as string
    388
    389              All parameters are expected to be UTF-8 encoded
    390
    391              @param prefix: The prefix (may be empty)
    392              @type prefix: C{str}
    393
    394              @param paths: List of paths (C{[str, str, ...]})
    395              @type paths: C{list}
    396
    397              @return: The prefixed paths as unicode
    398              @rtype: C{unicode}
    399          """
    400          slash = [u"/", u""][bool(prefix)]
    401          paths = u" ".join([
    402              u"%s%s" % (slash, path.decode("utf-8"))
    403              for path in paths
    404          ])
    405
    406          return (prefix and
    407              u"in /%s: %s" % (prefix.decode("utf-8"), paths) or paths
    408          )
    409