| 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 |