~landscape/zope3/newer-from-ztk

« back to all changes in this revision

Viewing changes to src/twisted/mail/alias.py

  • Committer: Thomas Hervé
  • Date: 2009-07-08 13:52:04 UTC
  • Revision ID: thomas@canonical.com-20090708135204-df5eesrthifpylf8
Remove twisted copy

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- test-case-name: twisted.mail.test.test_mail -*-
2
 
#
3
 
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
4
 
# See LICENSE for details.
5
 
 
6
 
 
7
 
"""Support for aliases(5) configuration files
8
 
 
9
 
API Stability: Unstable
10
 
 
11
 
@author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>}
12
 
 
13
 
TODO::
14
 
    Monitor files for reparsing
15
 
    Handle non-local alias targets
16
 
    Handle maildir alias targets
17
 
"""
18
 
 
19
 
import os
20
 
import tempfile
21
 
 
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
29
 
 
30
 
 
31
 
def handle(result, line, filename, lineNo):
32
 
    parts = [p.strip() for p in line.split(':', 1)]
33
 
    if len(parts) != 2:
34
 
        fmt = "Invalid format on line %d of alias file %s."
35
 
        arg = (lineNo, filename)
36
 
        log.err(fmt % arg)
37
 
    else:
38
 
        user, alias = parts
39
 
        result.setdefault(user.strip(), []).extend(map(str.strip, alias.split(',')))
40
 
 
41
 
def loadAliasFile(domains, filename=None, fp=None):
42
 
    """Load a file containing email aliases.
43
 
 
44
 
    Lines in the file should be formatted like so::
45
 
 
46
 
        username: alias1,alias2,...,aliasN
47
 
 
48
 
    Aliases beginning with a | will be treated as programs, will be run, and
49
 
    the message will be written to their stdin.
50
 
 
51
 
    Aliases without a host part will be assumed to be addresses on localhost.
52
 
 
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.
55
 
 
56
 
    @type domains: C{dict} of implementor of C{IDomain}
57
 
    @param domains: The domains to which these aliases will belong.
58
 
 
59
 
    @type filename: C{str}
60
 
    @param filename: The filename from which to load aliases.
61
 
 
62
 
    @type fp: Any file-like object.
63
 
    @param fp: If specified, overrides C{filename}, and aliases are read from
64
 
    it.
65
 
 
66
 
    @rtype: C{dict}
67
 
    @return: A dictionary mapping usernames to C{AliasGroup} objects.
68
 
    """
69
 
    result = {}
70
 
    if fp is None:
71
 
        fp = file(filename)
72
 
    else:
73
 
        filename = getattr(fp, 'name', '<unknown>')
74
 
    i = 0
75
 
    prev = ''
76
 
    for line in fp:
77
 
        i += 1
78
 
        line = line.rstrip()
79
 
        if line.lstrip().startswith('#'):
80
 
            continue
81
 
        elif line.startswith(' ') or line.startswith('\t'):
82
 
            prev = prev + line
83
 
        else:
84
 
            if prev:
85
 
                handle(result, prev, filename, i)
86
 
            prev = line
87
 
    if prev:
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)
92
 
    return result
93
 
 
94
 
class IAlias(Interface):
95
 
    def createMessageReceiver():
96
 
        pass
97
 
 
98
 
class AliasBase:
99
 
    def __init__(self, domains, original):
100
 
        self.domains = domains
101
 
        self.original = smtp.Address(original)
102
 
    
103
 
    def domain(self):
104
 
        return self.domains[self.original.domain]
105
 
    
106
 
    def resolve(self, aliasmap, memo=None):
107
 
        if memo is None:
108
 
            memo = {}
109
 
        if str(self) in memo:
110
 
            return None
111
 
        memo[str(self)] = None
112
 
        return self.createMessageReceiver()
113
 
    
114
 
class AddressAlias(AliasBase):
115
 
    """The simplest alias, translating one email address into another."""
116
 
 
117
 
    implements(IAlias)
118
 
 
119
 
    def __init__(self, alias, *args):
120
 
        AliasBase.__init__(self, *args)
121
 
        self.alias = smtp.Address(alias)
122
 
 
123
 
    def __str__(self):
124
 
        return '<Address %s>' % (self.alias,)
125
 
    
126
 
    def createMessageReceiver(self):
127
 
        return self.domain().startMessage(str(self.alias))
128
 
    
129
 
    def resolve(self, aliasmap, memo=None):
130
 
        if memo is None:
131
 
            memo = {}
132
 
        if str(self) in memo:
133
 
            return None
134
 
        memo[str(self)] = None
135
 
        try:
136
 
            return self.domain().exists(smtp.User(self.alias, None, None, None), memo)()
137
 
        except smtp.SMTPBadRcpt:
138
 
            pass
139
 
        if self.alias.local in aliasmap:
140
 
            return aliasmap[self.alias.local].resolve(aliasmap, memo)
141
 
        return None
142
 
 
143
 
class FileWrapper:
144
 
    implements(smtp.IMessage)
145
 
    
146
 
    def __init__(self, filename):
147
 
        self.fp = tempfile.TemporaryFile()
148
 
        self.finalname = filename
149
 
    
150
 
    def lineReceived(self, line):
151
 
        self.fp.write(line + '\n')
152
 
    
153
 
    def eomReceived(self):
154
 
        self.fp.seek(0, 0)
155
 
        try:
156
 
            f = file(self.finalname, 'a')
157
 
        except:
158
 
            return defer.fail(failure.Failure())
159
 
        
160
 
        f.write(self.fp.read())
161
 
        self.fp.close()
162
 
        f.close()
163
 
        
164
 
        return defer.succeed(self.finalname)
165
 
    
166
 
    def connectionLost(self):
167
 
        self.fp.close()
168
 
        self.fp = None
169
 
 
170
 
    def __str__(self):
171
 
        return '<FileWrapper %s>' % (self.finalname,)
172
 
 
173
 
 
174
 
class FileAlias(AliasBase):
175
 
 
176
 
    implements(IAlias)
177
 
 
178
 
    def __init__(self, filename, *args):
179
 
        AliasBase.__init__(self, *args)
180
 
        self.filename = filename
181
 
 
182
 
    def __str__(self):
183
 
        return '<File %s>' % (self.filename,)
184
 
    
185
 
    def createMessageReceiver(self):
186
 
        return FileWrapper(self.filename)
187
 
 
188
 
class MessageWrapper:
189
 
    implements(smtp.IMessage)
190
 
    
191
 
    done = False
192
 
    
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)
199
 
    
200
 
    def _processEnded(self, result, err=0):
201
 
        self.done = True
202
 
        if err:
203
 
            raise result.value
204
 
    
205
 
    def lineReceived(self, line):
206
 
        if self.done:
207
 
            return
208
 
        self.protocol.transport.write(line + '\n')
209
 
    
210
 
    def eomReceived(self):
211
 
        if not self.done:
212
 
            self.protocol.transport.loseConnection()
213
 
            self.completion.setTimeout(60)
214
 
        return self.completion
215
 
    
216
 
    def connectionLost(self):
217
 
        # Heh heh
218
 
        pass
219
 
    
220
 
    def __str__(self):
221
 
        return '<ProcessWrapper %s>' % (self.processName,) 
222
 
 
223
 
class ProcessAliasProtocol(protocol.ProcessProtocol):
224
 
    def processEnded(self, reason):
225
 
        if reason.check(error.ProcessDone):
226
 
            self.onEnd.callback("Complete")
227
 
        else:
228
 
            self.onEnd.errback(reason)
229
 
 
230
 
class ProcessAlias(AliasBase):
231
 
    """An alias for a program."""
232
 
 
233
 
    implements(IAlias)
234
 
 
235
 
    def __init__(self, path, *args):
236
 
        AliasBase.__init__(self, *args)
237
 
        self.path = path.split()
238
 
        self.program = self.path[0]
239
 
    
240
 
    def __str__(self):
241
 
        return '<Process %s>' % (self.path,)
242
 
    
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)
248
 
        return m
249
 
 
250
 
class MultiWrapper:
251
 
    """Wrapper to deliver a single message to multiple recipients"""
252
 
 
253
 
    implements(smtp.IMessage)
254
 
 
255
 
    def __init__(self, objs):
256
 
        self.objs = objs
257
 
    
258
 
    def lineReceived(self, line):
259
 
        for o in self.objs:
260
 
            o.lineReceived(line)
261
 
    
262
 
    def eomReceived(self):
263
 
        return defer.DeferredList([
264
 
            o.eomReceived() for o in self.objs
265
 
        ])
266
 
 
267
 
    def connectionLost(self):
268
 
        for o in self.objs:
269
 
            o.connectionLost()
270
 
    
271
 
    def __str__(self):
272
 
        return '<GroupWrapper %r>' % (map(str, self.objs),)
273
 
 
274
 
class AliasGroup(AliasBase):
275
 
    """An alias which points to more than one recipient"""
276
 
 
277
 
    implements(IAlias)
278
 
 
279
 
    def __init__(self, items, *args):
280
 
        AliasBase.__init__(self, *args)
281
 
        self.aliases = []
282
 
        while items:
283
 
            addr = items.pop().strip()
284
 
            if addr.startswith(':'):
285
 
                try:
286
 
                    f = file(addr[1:])
287
 
                except:
288
 
                    log.err("Invalid filename in alias file %r" % (addr[1:],))
289
 
                else:
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")
297
 
                else:
298
 
                    self.aliases.append(FileAlias(addr, *args))
299
 
            else:
300
 
                self.aliases.append(AddressAlias(addr, *args))
301
 
 
302
 
    def __len__(self):
303
 
        return len(self.aliases)
304
 
    
305
 
    def __str__(self):
306
 
        return '<AliasGroup [%s]>' % (', '.join(map(str, self.aliases)))
307
 
    
308
 
    def createMessageReceiver(self):
309
 
        return MultiWrapper([a.createMessageReceiver() for a in self.aliases])
310
 
 
311
 
    def resolve(self, aliasmap, memo=None):
312
 
        if memo is None:
313
 
            memo = {}
314
 
        r = []
315
 
        for a in self.aliases:
316
 
            r.append(a.resolve(aliasmap, memo))
317
 
        return MultiWrapper(filter(None, r))
318