root/trunk/twisted/mail/maildir.py

Revision 30752, 15.1 KB (checked in by exarkun, 15 months ago)

Rewrite the copyright headers to exclude date information.

Author: exarkun
Reviewer: glyph
Fixes: #4857

To avoid the need to perpetually update copyright dates in each file in Twisted,
remove the dates from most files and just leave them in the LICENSE file.

As a side effect, some files also have had a trailing newline added where it was
missing before.

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