| OLD | NEW | 
|---|
|  | (Empty) | 
| 1 # -*- test-case-name: twisted.mail.test.test_mail -*- |  | 
| 2 # |  | 
| 3 # Copyright (c) 2001-2007 Twisted Matrix Laboratories. |  | 
| 4 # See LICENSE for details. |  | 
| 5 |  | 
| 6 |  | 
| 7 """ |  | 
| 8 Support for aliases(5) configuration files |  | 
| 9 |  | 
| 10 @author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>} |  | 
| 11 |  | 
| 12 TODO:: |  | 
| 13     Monitor files for reparsing |  | 
| 14     Handle non-local alias targets |  | 
| 15     Handle maildir alias targets |  | 
| 16 """ |  | 
| 17 |  | 
| 18 import os |  | 
| 19 import tempfile |  | 
| 20 |  | 
| 21 from twisted.mail import smtp |  | 
| 22 from twisted.internet import reactor |  | 
| 23 from twisted.internet import protocol |  | 
| 24 from twisted.internet import defer |  | 
| 25 from twisted.python import failure |  | 
| 26 from twisted.python import log |  | 
| 27 from zope.interface import implements, Interface |  | 
| 28 |  | 
| 29 |  | 
| 30 def handle(result, line, filename, lineNo): |  | 
| 31     parts = [p.strip() for p in line.split(':', 1)] |  | 
| 32     if len(parts) != 2: |  | 
| 33         fmt = "Invalid format on line %d of alias file %s." |  | 
| 34         arg = (lineNo, filename) |  | 
| 35         log.err(fmt % arg) |  | 
| 36     else: |  | 
| 37         user, alias = parts |  | 
| 38         result.setdefault(user.strip(), []).extend(map(str.strip, alias.split(',
     '))) |  | 
| 39 |  | 
| 40 def loadAliasFile(domains, filename=None, fp=None): |  | 
| 41     """Load a file containing email aliases. |  | 
| 42 |  | 
| 43     Lines in the file should be formatted like so:: |  | 
| 44 |  | 
| 45         username: alias1,alias2,...,aliasN |  | 
| 46 |  | 
| 47     Aliases beginning with a | will be treated as programs, will be run, and |  | 
| 48     the message will be written to their stdin. |  | 
| 49 |  | 
| 50     Aliases without a host part will be assumed to be addresses on localhost. |  | 
| 51 |  | 
| 52     If a username is specified multiple times, the aliases for each are joined |  | 
| 53     together as if they had all been on one line. |  | 
| 54 |  | 
| 55     @type domains: C{dict} of implementor of C{IDomain} |  | 
| 56     @param domains: The domains to which these aliases will belong. |  | 
| 57 |  | 
| 58     @type filename: C{str} |  | 
| 59     @param filename: The filename from which to load aliases. |  | 
| 60 |  | 
| 61     @type fp: Any file-like object. |  | 
| 62     @param fp: If specified, overrides C{filename}, and aliases are read from |  | 
| 63     it. |  | 
| 64 |  | 
| 65     @rtype: C{dict} |  | 
| 66     @return: A dictionary mapping usernames to C{AliasGroup} objects. |  | 
| 67     """ |  | 
| 68     result = {} |  | 
| 69     if fp is None: |  | 
| 70         fp = file(filename) |  | 
| 71     else: |  | 
| 72         filename = getattr(fp, 'name', '<unknown>') |  | 
| 73     i = 0 |  | 
| 74     prev = '' |  | 
| 75     for line in fp: |  | 
| 76         i += 1 |  | 
| 77         line = line.rstrip() |  | 
| 78         if line.lstrip().startswith('#'): |  | 
| 79             continue |  | 
| 80         elif line.startswith(' ') or line.startswith('\t'): |  | 
| 81             prev = prev + line |  | 
| 82         else: |  | 
| 83             if prev: |  | 
| 84                 handle(result, prev, filename, i) |  | 
| 85             prev = line |  | 
| 86     if prev: |  | 
| 87         handle(result, prev, filename, i) |  | 
| 88     for (u, a) in result.items(): |  | 
| 89         addr = smtp.Address(u) |  | 
| 90         result[u] = AliasGroup(a, domains, u) |  | 
| 91     return result |  | 
| 92 |  | 
| 93 class IAlias(Interface): |  | 
| 94     def createMessageReceiver(): |  | 
| 95         pass |  | 
| 96 |  | 
| 97 class AliasBase: |  | 
| 98     def __init__(self, domains, original): |  | 
| 99         self.domains = domains |  | 
| 100         self.original = smtp.Address(original) |  | 
| 101 |  | 
| 102     def domain(self): |  | 
| 103         return self.domains[self.original.domain] |  | 
| 104 |  | 
| 105     def resolve(self, aliasmap, memo=None): |  | 
| 106         if memo is None: |  | 
| 107             memo = {} |  | 
| 108         if str(self) in memo: |  | 
| 109             return None |  | 
| 110         memo[str(self)] = None |  | 
| 111         return self.createMessageReceiver() |  | 
| 112 |  | 
| 113 class AddressAlias(AliasBase): |  | 
| 114     """The simplest alias, translating one email address into another.""" |  | 
| 115 |  | 
| 116     implements(IAlias) |  | 
| 117 |  | 
| 118     def __init__(self, alias, *args): |  | 
| 119         AliasBase.__init__(self, *args) |  | 
| 120         self.alias = smtp.Address(alias) |  | 
| 121 |  | 
| 122     def __str__(self): |  | 
| 123         return '<Address %s>' % (self.alias,) |  | 
| 124 |  | 
| 125     def createMessageReceiver(self): |  | 
| 126         return self.domain().startMessage(str(self.alias)) |  | 
| 127 |  | 
| 128     def resolve(self, aliasmap, memo=None): |  | 
| 129         if memo is None: |  | 
| 130             memo = {} |  | 
| 131         if str(self) in memo: |  | 
| 132             return None |  | 
| 133         memo[str(self)] = None |  | 
| 134         try: |  | 
| 135             return self.domain().exists(smtp.User(self.alias, None, None, None),
      memo)() |  | 
| 136         except smtp.SMTPBadRcpt: |  | 
| 137             pass |  | 
| 138         if self.alias.local in aliasmap: |  | 
| 139             return aliasmap[self.alias.local].resolve(aliasmap, memo) |  | 
| 140         return None |  | 
| 141 |  | 
| 142 class FileWrapper: |  | 
| 143     implements(smtp.IMessage) |  | 
| 144 |  | 
| 145     def __init__(self, filename): |  | 
| 146         self.fp = tempfile.TemporaryFile() |  | 
| 147         self.finalname = filename |  | 
| 148 |  | 
| 149     def lineReceived(self, line): |  | 
| 150         self.fp.write(line + '\n') |  | 
| 151 |  | 
| 152     def eomReceived(self): |  | 
| 153         self.fp.seek(0, 0) |  | 
| 154         try: |  | 
| 155             f = file(self.finalname, 'a') |  | 
| 156         except: |  | 
| 157             return defer.fail(failure.Failure()) |  | 
| 158 |  | 
| 159         f.write(self.fp.read()) |  | 
| 160         self.fp.close() |  | 
| 161         f.close() |  | 
| 162 |  | 
| 163         return defer.succeed(self.finalname) |  | 
| 164 |  | 
| 165     def connectionLost(self): |  | 
| 166         self.fp.close() |  | 
| 167         self.fp = None |  | 
| 168 |  | 
| 169     def __str__(self): |  | 
| 170         return '<FileWrapper %s>' % (self.finalname,) |  | 
| 171 |  | 
| 172 |  | 
| 173 class FileAlias(AliasBase): |  | 
| 174 |  | 
| 175     implements(IAlias) |  | 
| 176 |  | 
| 177     def __init__(self, filename, *args): |  | 
| 178         AliasBase.__init__(self, *args) |  | 
| 179         self.filename = filename |  | 
| 180 |  | 
| 181     def __str__(self): |  | 
| 182         return '<File %s>' % (self.filename,) |  | 
| 183 |  | 
| 184     def createMessageReceiver(self): |  | 
| 185         return FileWrapper(self.filename) |  | 
| 186 |  | 
| 187 |  | 
| 188 |  | 
| 189 class ProcessAliasTimeout(Exception): |  | 
| 190     """ |  | 
| 191     A timeout occurred while processing aliases. |  | 
| 192     """ |  | 
| 193 |  | 
| 194 |  | 
| 195 |  | 
| 196 class MessageWrapper: |  | 
| 197     """ |  | 
| 198     A message receiver which delivers content to a child process. |  | 
| 199 |  | 
| 200     @type completionTimeout: C{int} or C{float} |  | 
| 201     @ivar completionTimeout: The number of seconds to wait for the child |  | 
| 202         process to exit before reporting the delivery as a failure. |  | 
| 203 |  | 
| 204     @type _timeoutCallID: C{NoneType} or L{IDelayedCall} |  | 
| 205     @ivar _timeoutCallID: The call used to time out delivery, started when the |  | 
| 206         connection to the child process is closed. |  | 
| 207 |  | 
| 208     @type done: C{bool} |  | 
| 209     @ivar done: Flag indicating whether the child process has exited or not. |  | 
| 210 |  | 
| 211     @ivar reactor: An L{IReactorTime} provider which will be used to schedule |  | 
| 212         timeouts. |  | 
| 213     """ |  | 
| 214     implements(smtp.IMessage) |  | 
| 215 |  | 
| 216     done = False |  | 
| 217 |  | 
| 218     completionTimeout = 60 |  | 
| 219     _timeoutCallID = None |  | 
| 220 |  | 
| 221     reactor = reactor |  | 
| 222 |  | 
| 223     def __init__(self, protocol, process=None, reactor=None): |  | 
| 224         self.processName = process |  | 
| 225         self.protocol = protocol |  | 
| 226         self.completion = defer.Deferred() |  | 
| 227         self.protocol.onEnd = self.completion |  | 
| 228         self.completion.addBoth(self._processEnded) |  | 
| 229 |  | 
| 230         if reactor is not None: |  | 
| 231             self.reactor = reactor |  | 
| 232 |  | 
| 233 |  | 
| 234     def _processEnded(self, result): |  | 
| 235         """ |  | 
| 236         Record process termination and cancel the timeout call if it is active. |  | 
| 237         """ |  | 
| 238         self.done = True |  | 
| 239         if self._timeoutCallID is not None: |  | 
| 240             # eomReceived was called, we're actually waiting for the process to |  | 
| 241             # exit. |  | 
| 242             self._timeoutCallID.cancel() |  | 
| 243             self._timeoutCallID = None |  | 
| 244         else: |  | 
| 245             # eomReceived was not called, this is unexpected, propagate the |  | 
| 246             # error. |  | 
| 247             return result |  | 
| 248 |  | 
| 249 |  | 
| 250     def lineReceived(self, line): |  | 
| 251         if self.done: |  | 
| 252             return |  | 
| 253         self.protocol.transport.write(line + '\n') |  | 
| 254 |  | 
| 255 |  | 
| 256     def eomReceived(self): |  | 
| 257         """ |  | 
| 258         Disconnect from the child process, set up a timeout to wait for it to |  | 
| 259         exit, and return a Deferred which will be called back when the child |  | 
| 260         process exits. |  | 
| 261         """ |  | 
| 262         if not self.done: |  | 
| 263             self.protocol.transport.loseConnection() |  | 
| 264             self._timeoutCallID = self.reactor.callLater( |  | 
| 265                 self.completionTimeout, self._completionCancel) |  | 
| 266         return self.completion |  | 
| 267 |  | 
| 268 |  | 
| 269     def _completionCancel(self): |  | 
| 270         """ |  | 
| 271         Handle the expiration of the timeout for the child process to exit by |  | 
| 272         terminating the child process forcefully and issuing a failure to the |  | 
| 273         completion deferred returned by L{eomReceived}. |  | 
| 274         """ |  | 
| 275         self._timeoutCallID = None |  | 
| 276         self.protocol.transport.signalProcess('KILL') |  | 
| 277         exc = ProcessAliasTimeout( |  | 
| 278             "No answer after %s seconds" % (self.completionTimeout,)) |  | 
| 279         self.protocol.onEnd = None |  | 
| 280         self.completion.errback(failure.Failure(exc)) |  | 
| 281 |  | 
| 282 |  | 
| 283     def connectionLost(self): |  | 
| 284         # Heh heh |  | 
| 285         pass |  | 
| 286 |  | 
| 287 |  | 
| 288     def __str__(self): |  | 
| 289         return '<ProcessWrapper %s>' % (self.processName,) |  | 
| 290 |  | 
| 291 |  | 
| 292 |  | 
| 293 class ProcessAliasProtocol(protocol.ProcessProtocol): |  | 
| 294     """ |  | 
| 295     Trivial process protocol which will callback a Deferred when the associated |  | 
| 296     process ends. |  | 
| 297 |  | 
| 298     @ivar onEnd: If not C{None}, a L{Deferred} which will be called back with |  | 
| 299         the failure passed to C{processEnded}, when C{processEnded} is called. |  | 
| 300     """ |  | 
| 301 |  | 
| 302     onEnd = None |  | 
| 303 |  | 
| 304     def processEnded(self, reason): |  | 
| 305         """ |  | 
| 306         Call back C{onEnd} if it is set. |  | 
| 307         """ |  | 
| 308         if self.onEnd is not None: |  | 
| 309             self.onEnd.errback(reason) |  | 
| 310 |  | 
| 311 |  | 
| 312 |  | 
| 313 class ProcessAlias(AliasBase): |  | 
| 314     """ |  | 
| 315     An alias which is handled by the execution of a particular program. |  | 
| 316 |  | 
| 317     @ivar reactor: An L{IReactorProcess} and L{IReactorTime} provider which |  | 
| 318         will be used to create and timeout the alias child process. |  | 
| 319     """ |  | 
| 320     implements(IAlias) |  | 
| 321 |  | 
| 322     reactor = reactor |  | 
| 323 |  | 
| 324     def __init__(self, path, *args): |  | 
| 325         AliasBase.__init__(self, *args) |  | 
| 326         self.path = path.split() |  | 
| 327         self.program = self.path[0] |  | 
| 328 |  | 
| 329 |  | 
| 330     def __str__(self): |  | 
| 331         """ |  | 
| 332         Build a string representation containing the path. |  | 
| 333         """ |  | 
| 334         return '<Process %s>' % (self.path,) |  | 
| 335 |  | 
| 336 |  | 
| 337     def spawnProcess(self, proto, program, path): |  | 
| 338         """ |  | 
| 339         Wrapper around C{reactor.spawnProcess}, to be customized for tests |  | 
| 340         purpose. |  | 
| 341         """ |  | 
| 342         return self.reactor.spawnProcess(proto, program, path) |  | 
| 343 |  | 
| 344 |  | 
| 345     def createMessageReceiver(self): |  | 
| 346         """ |  | 
| 347         Create a message receiver by launching a process. |  | 
| 348         """ |  | 
| 349         p = ProcessAliasProtocol() |  | 
| 350         m = MessageWrapper(p, self.program, self.reactor) |  | 
| 351         fd = self.spawnProcess(p, self.program, self.path) |  | 
| 352         return m |  | 
| 353 |  | 
| 354 |  | 
| 355 |  | 
| 356 class MultiWrapper: |  | 
| 357     """ |  | 
| 358     Wrapper to deliver a single message to multiple recipients. |  | 
| 359     """ |  | 
| 360 |  | 
| 361     implements(smtp.IMessage) |  | 
| 362 |  | 
| 363     def __init__(self, objs): |  | 
| 364         self.objs = objs |  | 
| 365 |  | 
| 366     def lineReceived(self, line): |  | 
| 367         for o in self.objs: |  | 
| 368             o.lineReceived(line) |  | 
| 369 |  | 
| 370     def eomReceived(self): |  | 
| 371         return defer.DeferredList([ |  | 
| 372             o.eomReceived() for o in self.objs |  | 
| 373         ]) |  | 
| 374 |  | 
| 375     def connectionLost(self): |  | 
| 376         for o in self.objs: |  | 
| 377             o.connectionLost() |  | 
| 378 |  | 
| 379     def __str__(self): |  | 
| 380         return '<GroupWrapper %r>' % (map(str, self.objs),) |  | 
| 381 |  | 
| 382 |  | 
| 383 |  | 
| 384 class AliasGroup(AliasBase): |  | 
| 385     """ |  | 
| 386     An alias which points to more than one recipient. |  | 
| 387 |  | 
| 388     @ivar processAliasFactory: a factory for resolving process aliases. |  | 
| 389     @type processAliasFactory: C{class} |  | 
| 390     """ |  | 
| 391 |  | 
| 392     implements(IAlias) |  | 
| 393 |  | 
| 394     processAliasFactory = ProcessAlias |  | 
| 395 |  | 
| 396     def __init__(self, items, *args): |  | 
| 397         AliasBase.__init__(self, *args) |  | 
| 398         self.aliases = [] |  | 
| 399         while items: |  | 
| 400             addr = items.pop().strip() |  | 
| 401             if addr.startswith(':'): |  | 
| 402                 try: |  | 
| 403                     f = file(addr[1:]) |  | 
| 404                 except: |  | 
| 405                     log.err("Invalid filename in alias file %r" % (addr[1:],)) |  | 
| 406                 else: |  | 
| 407                     addr = ' '.join([l.strip() for l in f]) |  | 
| 408                     items.extend(addr.split(',')) |  | 
| 409             elif addr.startswith('|'): |  | 
| 410                 self.aliases.append(self.processAliasFactory(addr[1:], *args)) |  | 
| 411             elif addr.startswith('/'): |  | 
| 412                 if os.path.isdir(addr): |  | 
| 413                     log.err("Directory delivery not supported") |  | 
| 414                 else: |  | 
| 415                     self.aliases.append(FileAlias(addr, *args)) |  | 
| 416             else: |  | 
| 417                 self.aliases.append(AddressAlias(addr, *args)) |  | 
| 418 |  | 
| 419     def __len__(self): |  | 
| 420         return len(self.aliases) |  | 
| 421 |  | 
| 422     def __str__(self): |  | 
| 423         return '<AliasGroup [%s]>' % (', '.join(map(str, self.aliases))) |  | 
| 424 |  | 
| 425     def createMessageReceiver(self): |  | 
| 426         return MultiWrapper([a.createMessageReceiver() for a in self.aliases]) |  | 
| 427 |  | 
| 428     def resolve(self, aliasmap, memo=None): |  | 
| 429         if memo is None: |  | 
| 430             memo = {} |  | 
| 431         r = [] |  | 
| 432         for a in self.aliases: |  | 
| 433             r.append(a.resolve(aliasmap, memo)) |  | 
| 434         return MultiWrapper(filter(None, r)) |  | 
| 435 |  | 
| OLD | NEW | 
|---|