~certify-web-dev/twisted/certify-trunk

« back to all changes in this revision

Viewing changes to twisted/mail/alias.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2007-01-17 14:52:35 UTC
  • mfrom: (1.1.5 upstream) (2.1.2 etch)
  • Revision ID: james.westby@ubuntu.com-20070117145235-btmig6qfmqfen0om
Tags: 2.5.0-0ubuntu1
New upstream version, compatible with python2.5.

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