1
# -*- test-case-name: twisted.mail.test.test_mail -*-
3
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
4
# See LICENSE for details.
7
"""Support for aliases(5) configuration files
9
API Stability: Unstable
11
@author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>}
14
Monitor files for reparsing
15
Handle non-local alias targets
16
Handle maildir alias targets
22
from twisted.mail import smtp
23
from twisted.internet import protocol
24
from twisted.internet import defer
25
from twisted.internet import error
26
from twisted.python import failure
27
from twisted.python import log
28
from zope.interface import implements, Interface
31
def handle(result, line, filename, lineNo):
32
parts = [p.strip() for p in line.split(':', 1)]
34
fmt = "Invalid format on line %d of alias file %s."
35
arg = (lineNo, filename)
39
result.setdefault(user.strip(), []).extend(map(str.strip, alias.split(',')))
41
def loadAliasFile(domains, filename=None, fp=None):
42
"""Load a file containing email aliases.
44
Lines in the file should be formatted like so::
46
username: alias1,alias2,...,aliasN
48
Aliases beginning with a | will be treated as programs, will be run, and
49
the message will be written to their stdin.
51
Aliases without a host part will be assumed to be addresses on localhost.
53
If a username is specified multiple times, the aliases for each are joined
54
together as if they had all been on one line.
56
@type domains: C{dict} of implementor of C{IDomain}
57
@param domains: The domains to which these aliases will belong.
59
@type filename: C{str}
60
@param filename: The filename from which to load aliases.
62
@type fp: Any file-like object.
63
@param fp: If specified, overrides C{filename}, and aliases are read from
67
@return: A dictionary mapping usernames to C{AliasGroup} objects.
73
filename = getattr(fp, 'name', '<unknown>')
79
if line.lstrip().startswith('#'):
81
elif line.startswith(' ') or line.startswith('\t'):
85
handle(result, prev, filename, i)
88
handle(result, prev, filename, i)
89
for (u, a) in result.items():
90
addr = smtp.Address(u)
91
result[u] = AliasGroup(a, domains, u)
94
class IAlias(Interface):
95
def createMessageReceiver():
99
def __init__(self, domains, original):
100
self.domains = domains
101
self.original = smtp.Address(original)
104
return self.domains[self.original.domain]
106
def resolve(self, aliasmap, memo=None):
109
if str(self) in memo:
111
memo[str(self)] = None
112
return self.createMessageReceiver()
114
class AddressAlias(AliasBase):
115
"""The simplest alias, translating one email address into another."""
119
def __init__(self, alias, *args):
120
AliasBase.__init__(self, *args)
121
self.alias = smtp.Address(alias)
124
return '<Address %s>' % (self.alias,)
126
def createMessageReceiver(self):
127
return self.domain().startMessage(str(self.alias))
129
def resolve(self, aliasmap, memo=None):
132
if str(self) in memo:
134
memo[str(self)] = None
136
return self.domain().exists(smtp.User(self.alias, None, None, None), memo)()
137
except smtp.SMTPBadRcpt:
139
if self.alias.local in aliasmap:
140
return aliasmap[self.alias.local].resolve(aliasmap, memo)
144
implements(smtp.IMessage)
146
def __init__(self, filename):
147
self.fp = tempfile.TemporaryFile()
148
self.finalname = filename
150
def lineReceived(self, line):
151
self.fp.write(line + '\n')
153
def eomReceived(self):
156
f = file(self.finalname, 'a')
158
return defer.fail(failure.Failure())
160
f.write(self.fp.read())
164
return defer.succeed(self.finalname)
166
def connectionLost(self):
171
return '<FileWrapper %s>' % (self.finalname,)
174
class FileAlias(AliasBase):
178
def __init__(self, filename, *args):
179
AliasBase.__init__(self, *args)
180
self.filename = filename
183
return '<File %s>' % (self.filename,)
185
def createMessageReceiver(self):
186
return FileWrapper(self.filename)
188
class MessageWrapper:
189
implements(smtp.IMessage)
193
def __init__(self, protocol, process=None):
194
self.processName = process
195
self.protocol = protocol
196
self.completion = defer.Deferred()
197
self.protocol.onEnd = self.completion
198
self.completion.addCallback(self._processEnded)
200
def _processEnded(self, result, err=0):
205
def lineReceived(self, line):
208
self.protocol.transport.write(line + '\n')
210
def eomReceived(self):
212
self.protocol.transport.loseConnection()
213
self.completion.setTimeout(60)
214
return self.completion
216
def connectionLost(self):
221
return '<ProcessWrapper %s>' % (self.processName,)
223
class ProcessAliasProtocol(protocol.ProcessProtocol):
224
def processEnded(self, reason):
225
if reason.check(error.ProcessDone):
226
self.onEnd.callback("Complete")
228
self.onEnd.errback(reason)
230
class ProcessAlias(AliasBase):
231
"""An alias for a program."""
235
def __init__(self, path, *args):
236
AliasBase.__init__(self, *args)
237
self.path = path.split()
238
self.program = self.path[0]
241
return '<Process %s>' % (self.path,)
243
def createMessageReceiver(self):
244
from twisted.internet import reactor
245
p = ProcessAliasProtocol()
246
m = MessageWrapper(p, self.program)
247
fd = reactor.spawnProcess(p, self.program, self.path)
251
"""Wrapper to deliver a single message to multiple recipients"""
253
implements(smtp.IMessage)
255
def __init__(self, objs):
258
def lineReceived(self, line):
262
def eomReceived(self):
263
return defer.DeferredList([
264
o.eomReceived() for o in self.objs
267
def connectionLost(self):
272
return '<GroupWrapper %r>' % (map(str, self.objs),)
274
class AliasGroup(AliasBase):
275
"""An alias which points to more than one recipient"""
279
def __init__(self, items, *args):
280
AliasBase.__init__(self, *args)
283
addr = items.pop().strip()
284
if addr.startswith(':'):
288
log.err("Invalid filename in alias file %r" % (addr[1:],))
290
addr = ' '.join([l.strip() for l in f])
291
items.extend(addr.split(','))
292
elif addr.startswith('|'):
293
self.aliases.append(ProcessAlias(addr[1:], *args))
294
elif addr.startswith('/'):
295
if os.path.isdir(addr):
296
log.err("Directory delivery not supported")
298
self.aliases.append(FileAlias(addr, *args))
300
self.aliases.append(AddressAlias(addr, *args))
303
return len(self.aliases)
306
return '<AliasGroup [%s]>' % (', '.join(map(str, self.aliases)))
308
def createMessageReceiver(self):
309
return MultiWrapper([a.createMessageReceiver() for a in self.aliases])
311
def resolve(self, aliasmap, memo=None):
315
for a in self.aliases:
316
r.append(a.resolve(aliasmap, memo))
317
return MultiWrapper(filter(None, r))