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

« back to all changes in this revision

Viewing changes to twisted/mail/maildir.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
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
 
3
# See LICENSE for details.
 
4
 
 
5
 
 
6
"""Maildir-style mailbox support
 
7
"""
 
8
 
 
9
from __future__ import generators
 
10
 
 
11
import os
 
12
import stat
 
13
import socket
 
14
import time
 
15
import md5
 
16
import cStringIO
 
17
 
 
18
from zope.interface import implements
 
19
 
 
20
try:
 
21
    import cStringIO as StringIO
 
22
except ImportError:
 
23
    import StringIO
 
24
 
 
25
from twisted.mail import pop3
 
26
from twisted.mail import smtp
 
27
from twisted.protocols import basic
 
28
from twisted.persisted import dirdbm
 
29
from twisted.python import log, failure
 
30
from twisted.mail import mail
 
31
from twisted.mail import alias
 
32
from twisted.internet import interfaces, defer, reactor
 
33
 
 
34
from twisted import cred
 
35
import twisted.cred.portal
 
36
import twisted.cred.credentials
 
37
import twisted.cred.checkers
 
38
import twisted.cred.error
 
39
 
 
40
INTERNAL_ERROR = '''\
 
41
From: Twisted.mail Internals
 
42
Subject: An Error Occurred
 
43
 
 
44
  An internal server error has occurred.  Please contact the
 
45
  server administrator.
 
46
'''
 
47
 
 
48
class _MaildirNameGenerator:
 
49
    """Utility class to generate a unique maildir name
 
50
    """
 
51
    n = 0
 
52
    p = os.getpid()
 
53
    s = socket.gethostname().replace('/', r'\057').replace(':', r'\072')
 
54
 
 
55
    def generate(self):
 
56
        self.n = self.n + 1
 
57
        t = time.time()
 
58
        seconds = str(int(t))
 
59
        microseconds = str(int((t-int(t))*10e6))
 
60
        return '%s.M%sP%sQ%s.%s' % (seconds, microseconds,
 
61
                                    self.p, self.n, self.s)
 
62
 
 
63
_generateMaildirName = _MaildirNameGenerator().generate
 
64
 
 
65
def initializeMaildir(dir):
 
66
    if not os.path.isdir(dir):
 
67
        os.mkdir(dir, 0700)
 
68
        for subdir in ['new', 'cur', 'tmp', '.Trash']:
 
69
            os.mkdir(os.path.join(dir, subdir), 0700)
 
70
        for subdir in ['new', 'cur', 'tmp']:
 
71
            os.mkdir(os.path.join(dir, '.Trash', subdir), 0700)
 
72
        # touch
 
73
        open(os.path.join(dir, '.Trash', 'maildirfolder'), 'w').close()
 
74
 
 
75
 
 
76
class MaildirMessage(mail.FileMessage):
 
77
    size = None
 
78
 
 
79
    def __init__(self, address, fp, *a, **kw):
 
80
        header = "Delivered-To: %s\n" % address
 
81
        fp.write(header)
 
82
        self.size = len(header)
 
83
        mail.FileMessage.__init__(self, fp, *a, **kw)
 
84
 
 
85
    def lineReceived(self, line):
 
86
        mail.FileMessage.lineReceived(self, line)
 
87
        self.size += len(line)+1
 
88
 
 
89
    def eomReceived(self):
 
90
        self.finalName = self.finalName+',S=%d' % self.size
 
91
        return mail.FileMessage.eomReceived(self)
 
92
 
 
93
class AbstractMaildirDomain:
 
94
    """Abstract maildir-backed domain.
 
95
    """
 
96
    alias = None
 
97
    root = None
 
98
 
 
99
    def __init__(self, service, root):
 
100
        """Initialize.
 
101
        """
 
102
        self.root = root
 
103
 
 
104
    def userDirectory(self, user):
 
105
        """Get the maildir directory for a given user
 
106
 
 
107
        Override to specify where to save mails for users.
 
108
        Return None for non-existing users.
 
109
        """
 
110
        return None
 
111
 
 
112
    ##
 
113
    ## IAliasableDomain
 
114
    ##
 
115
 
 
116
    def setAliasGroup(self, alias):
 
117
        self.alias = alias
 
118
 
 
119
    ##
 
120
    ## IDomain
 
121
    ##
 
122
    def exists(self, user, memo=None):
 
123
        """Check for existence of user in the domain
 
124
        """
 
125
        if self.userDirectory(user.dest.local) is not None:
 
126
            return lambda: self.startMessage(user)
 
127
        try:
 
128
            a = self.alias[user.dest.local]
 
129
        except:
 
130
            raise smtp.SMTPBadRcpt(user)
 
131
        else:
 
132
            aliases = a.resolve(self.alias, memo)
 
133
            if aliases:
 
134
                return lambda: aliases
 
135
            log.err("Bad alias configuration: " + str(user))
 
136
            raise smtp.SMTPBadRcpt(user)
 
137
 
 
138
    def startMessage(self, user):
 
139
        """Save a message for a given user
 
140
        """
 
141
        if isinstance(user, str):
 
142
            name, domain = user.split('@', 1)
 
143
        else:
 
144
            name, domain = user.dest.local, user.dest.domain
 
145
        dir = self.userDirectory(name)
 
146
        fname = _generateMaildirName()
 
147
        filename = os.path.join(dir, 'tmp', fname)
 
148
        fp = open(filename, 'w')
 
149
        return MaildirMessage('%s@%s' % (name, domain), fp, filename,
 
150
                              os.path.join(dir, 'new', fname))
 
151
 
 
152
    def willRelay(self, user, protocol):
 
153
        return False
 
154
 
 
155
    def addUser(self, user, password):
 
156
        raise NotImplementedError
 
157
 
 
158
    def getCredentialsCheckers(self):
 
159
        raise NotImplementedError
 
160
    ##
 
161
    ## end of IDomain
 
162
    ##
 
163
 
 
164
class _MaildirMailboxAppendMessageTask:
 
165
    implements(interfaces.IConsumer)
 
166
 
 
167
    osopen = staticmethod(os.open)
 
168
    oswrite = staticmethod(os.write)
 
169
    osclose = staticmethod(os.close)
 
170
    osrename = staticmethod(os.rename)
 
171
 
 
172
    def __init__(self, mbox, msg):
 
173
        self.mbox = mbox
 
174
        self.defer = defer.Deferred()
 
175
        self.openCall = None
 
176
        if not hasattr(msg, "read"):
 
177
            msg = StringIO.StringIO(msg)
 
178
        self.msg = msg
 
179
        # This is needed, as this startup phase might call defer.errback and zero out self.defer
 
180
        # By doing it on the reactor iteration appendMessage is able to use .defer without problems.
 
181
        reactor.callLater(0, self.startUp)
 
182
 
 
183
    def startUp(self):
 
184
        self.createTempFile()
 
185
        if self.fh != -1:
 
186
            self.filesender = basic.FileSender()
 
187
            self.filesender.beginFileTransfer(self.msg, self)
 
188
 
 
189
    def registerProducer(self, producer, streaming):
 
190
        self.myproducer = producer
 
191
        self.streaming = streaming
 
192
        if not streaming:
 
193
            self.prodProducer()
 
194
 
 
195
    def prodProducer(self):
 
196
        self.openCall = None
 
197
        if self.myproducer is not None:
 
198
            self.openCall = reactor.callLater(0, self.prodProducer)
 
199
            self.myproducer.resumeProducing()
 
200
 
 
201
    def unregisterProducer(self):
 
202
        self.myproducer = None
 
203
        self.streaming = None
 
204
        self.osclose(self.fh)
 
205
        self.moveFileToNew()
 
206
 
 
207
    def write(self, data):
 
208
        try:
 
209
            self.oswrite(self.fh, data)
 
210
        except:
 
211
            self.fail()
 
212
 
 
213
    def fail(self, err=None):
 
214
        if err is None:
 
215
            err = failure.Failure()
 
216
        if self.openCall is not None:
 
217
            self.openCall.cancel()
 
218
        self.defer.errback(err)
 
219
        self.defer = None
 
220
 
 
221
    def moveFileToNew(self):
 
222
        while True:
 
223
            newname = os.path.join(self.mbox.path, "new", _generateMaildirName())
 
224
            try:
 
225
                self.osrename(self.tmpname, newname)
 
226
                break
 
227
            except OSError, (err, estr):
 
228
                import errno
 
229
                # if the newname exists, retry with a new newname.
 
230
                if err != errno.EEXIST:
 
231
                    self.fail()
 
232
                    newname = None
 
233
                    break
 
234
        if newname is not None:
 
235
            self.mbox.list.append(newname)
 
236
            self.defer.callback(None)
 
237
            self.defer = None
 
238
 
 
239
    def createTempFile(self):
 
240
        attr = (os.O_RDWR | os.O_CREAT | os.O_EXCL
 
241
                | getattr(os, "O_NOINHERIT", 0)
 
242
                | getattr(os, "O_NOFOLLOW", 0))
 
243
        tries = 0
 
244
        self.fh = -1
 
245
        while True:
 
246
            self.tmpname = os.path.join(self.mbox.path, "tmp", _generateMaildirName())
 
247
            try:
 
248
                self.fh = self.osopen(self.tmpname, attr, 0600)
 
249
                return None
 
250
            except OSError:
 
251
                tries += 1
 
252
                if tries > 500:
 
253
                    self.defer.errback(RuntimeError("Could not create tmp file for %s" % self.mbox.path))
 
254
                    self.defer = None
 
255
                    return None
 
256
 
 
257
class MaildirMailbox(pop3.Mailbox):
 
258
    """Implement the POP3 mailbox semantics for a Maildir mailbox
 
259
    """
 
260
    AppendFactory = _MaildirMailboxAppendMessageTask
 
261
 
 
262
    def __init__(self, path):
 
263
        """Initialize with name of the Maildir mailbox
 
264
        """
 
265
        self.path = path
 
266
        self.list = []
 
267
        self.deleted = {}
 
268
        initializeMaildir(path)
 
269
        for name in ('cur', 'new'):
 
270
            for file in os.listdir(os.path.join(path, name)):
 
271
                self.list.append((file, os.path.join(path, name, file)))
 
272
        self.list.sort()
 
273
        self.list = [e[1] for e in self.list]
 
274
 
 
275
    def listMessages(self, i=None):
 
276
        """Return a list of lengths of all files in new/ and cur/
 
277
        """
 
278
        if i is None:
 
279
            ret = []
 
280
            for mess in self.list:
 
281
                if mess:
 
282
                    ret.append(os.stat(mess)[stat.ST_SIZE])
 
283
                else:
 
284
                    ret.append(0)
 
285
            return ret
 
286
        return self.list[i] and os.stat(self.list[i])[stat.ST_SIZE] or 0
 
287
 
 
288
    def getMessage(self, i):
 
289
        """Return an open file-pointer to a message
 
290
        """
 
291
        return open(self.list[i])
 
292
 
 
293
    def getUidl(self, i):
 
294
        """Return a unique identifier for a message
 
295
 
 
296
        This is done using the basename of the filename.
 
297
        It is globally unique because this is how Maildirs are designed.
 
298
        """
 
299
        # Returning the actual filename is a mistake.  Hash it.
 
300
        base = os.path.basename(self.list[i])
 
301
        return md5.md5(base).hexdigest()
 
302
 
 
303
    def deleteMessage(self, i):
 
304
        """Delete a message
 
305
 
 
306
        This only moves a message to the .Trash/ subfolder,
 
307
        so it can be undeleted by an administrator.
 
308
        """
 
309
        trashFile = os.path.join(
 
310
            self.path, '.Trash', 'cur', os.path.basename(self.list[i])
 
311
        )
 
312
        os.rename(self.list[i], trashFile)
 
313
        self.deleted[self.list[i]] = trashFile
 
314
        self.list[i] = 0
 
315
 
 
316
    def undeleteMessages(self):
 
317
        """Undelete any deleted messages it is possible to undelete
 
318
 
 
319
        This moves any messages from .Trash/ subfolder back to their
 
320
        original position, and empties out the deleted dictionary.
 
321
        """
 
322
        for (real, trash) in self.deleted.items():
 
323
            try:
 
324
                os.rename(trash, real)
 
325
            except OSError, (err, estr):
 
326
                import errno
 
327
                # If the file has been deleted from disk, oh well!
 
328
                if err != errno.ENOENT:
 
329
                    raise
 
330
                # This is a pass
 
331
            else:
 
332
                try:
 
333
                    self.list[self.list.index(0)] = real
 
334
                except ValueError:
 
335
                    self.list.append(real)
 
336
        self.deleted.clear()
 
337
 
 
338
    def appendMessage(self, txt):
 
339
        """Appends a message into the mailbox."""
 
340
        task = self.AppendFactory(self, txt)
 
341
        return task.defer
 
342
 
 
343
class StringListMailbox:
 
344
    implements(pop3.IMailbox)
 
345
 
 
346
    def __init__(self, msgs):
 
347
        self.msgs = msgs
 
348
 
 
349
    def listMessages(self, i=None):
 
350
        if i is None:
 
351
            return map(len, self.msgs)
 
352
        return len(self.msgs[i])
 
353
 
 
354
    def getMessage(self, i):
 
355
        return StringIO.StringIO(self.msgs[i])
 
356
 
 
357
    def getUidl(self, i):
 
358
        return md5.new(self.msgs[i]).hexdigest()
 
359
 
 
360
    def deleteMessage(self, i):
 
361
        pass
 
362
 
 
363
    def undeleteMessages(self):
 
364
        pass
 
365
 
 
366
    def sync(self):
 
367
        pass
 
368
 
 
369
class MaildirDirdbmDomain(AbstractMaildirDomain):
 
370
    """A Maildir Domain where membership is checked by a dirdbm file
 
371
    """
 
372
 
 
373
    implements(cred.portal.IRealm, mail.IAliasableDomain)
 
374
 
 
375
    portal = None
 
376
    _credcheckers = None
 
377
 
 
378
    def __init__(self, service, root, postmaster=0):
 
379
        """Initialize
 
380
 
 
381
        The first argument is where the Domain directory is rooted.
 
382
        The second is whether non-existing addresses are simply
 
383
        forwarded to postmaster instead of outright bounce
 
384
 
 
385
        The directory structure of a MailddirDirdbmDomain is:
 
386
 
 
387
        /passwd <-- a dirdbm file
 
388
        /USER/{cur,new,del} <-- each user has these three directories
 
389
        """
 
390
        AbstractMaildirDomain.__init__(self, service, root)
 
391
        dbm = os.path.join(root, 'passwd')
 
392
        if not os.path.exists(dbm):
 
393
            os.makedirs(dbm)
 
394
        self.dbm = dirdbm.open(dbm)
 
395
        self.postmaster = postmaster
 
396
 
 
397
    def userDirectory(self, name):
 
398
        """Get the directory for a user
 
399
 
 
400
        If the user exists in the dirdbm file, return the directory
 
401
        os.path.join(root, name), creating it if necessary.
 
402
        Otherwise, returns postmaster's mailbox instead if bounces
 
403
        go to postmaster, otherwise return None
 
404
        """
 
405
        if not self.dbm.has_key(name):
 
406
            if not self.postmaster:
 
407
                return None
 
408
            name = 'postmaster'
 
409
        dir = os.path.join(self.root, name)
 
410
        if not os.path.exists(dir):
 
411
            initializeMaildir(dir)
 
412
        return dir
 
413
 
 
414
    ##
 
415
    ## IDomain
 
416
    ##
 
417
    def addUser(self, user, password):
 
418
        self.dbm[user] = password
 
419
        # Ensure it is initialized
 
420
        self.userDirectory(user)
 
421
 
 
422
    def getCredentialsCheckers(self):
 
423
        if self._credcheckers is None:
 
424
            self._credcheckers = [DirdbmDatabase(self.dbm)]
 
425
        return self._credcheckers
 
426
 
 
427
    ##
 
428
    ## IRealm
 
429
    ##
 
430
    def requestAvatar(self, avatarId, mind, *interfaces):
 
431
        if pop3.IMailbox not in interfaces:
 
432
            raise NotImplementedError("No interface")
 
433
        if avatarId == cred.checkers.ANONYMOUS:
 
434
            mbox = StringListMailbox([INTERNAL_ERROR])
 
435
        else:
 
436
            mbox = MaildirMailbox(os.path.join(self.root, avatarId))
 
437
 
 
438
        return (
 
439
            pop3.IMailbox,
 
440
            mbox,
 
441
            lambda: None
 
442
        )
 
443
 
 
444
class DirdbmDatabase:
 
445
    implements(cred.checkers.ICredentialsChecker)
 
446
 
 
447
    credentialInterfaces = (
 
448
        cred.credentials.IUsernamePassword,
 
449
        cred.credentials.IUsernameHashedPassword
 
450
    )
 
451
 
 
452
    def __init__(self, dbm):
 
453
        self.dirdbm = dbm
 
454
 
 
455
    def requestAvatarId(self, c):
 
456
        if c.username in self.dirdbm:
 
457
            if c.checkPassword(self.dirdbm[c.username]):
 
458
                return c.username
 
459
        raise cred.error.UnauthorizedLogin()