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