1
# -*- test-case-name: twisted.mail.test.test_mail -*-
2
# Copyright (c) 2001-2009 Twisted Matrix Laboratories.
3
# See LICENSE for details.
7
Maildir-style mailbox support
15
from zope.interface import implements
18
import cStringIO as StringIO
22
from twisted.python.compat import set
23
from twisted.mail import pop3
24
from twisted.mail import smtp
25
from twisted.protocols import basic
26
from twisted.persisted import dirdbm
27
from twisted.python import log, failure
28
from twisted.python.hashlib import md5
29
from twisted.mail import mail
30
from twisted.internet import interfaces, defer, reactor
32
from twisted import cred
33
import twisted.cred.portal
34
import twisted.cred.credentials
35
import twisted.cred.checkers
36
import twisted.cred.error
39
From: Twisted.mail Internals
40
Subject: An Error Occurred
42
An internal server error has occurred. Please contact the
46
class _MaildirNameGenerator:
48
Utility class to generate a unique maildir name
50
@ivar _clock: An L{IReactorTime} provider which will be used to learn
51
the current time to include in names returned by L{generate} so that
56
s = socket.gethostname().replace('/', r'\057').replace(':', r'\072')
58
def __init__(self, clock):
63
Return a string which is intended to unique across all calls to this
64
function (across all processes, reboots, etc).
66
Strings returned by earlier calls to this method will compare less
67
than strings returned by later calls as long as the clock provided
71
t = self._clock.seconds()
73
microseconds = '%07d' % (int((t - int(t)) * 10e6),)
74
return '%s.M%sP%sQ%s.%s' % (seconds, microseconds,
75
self.p, self.n, self.s)
77
_generateMaildirName = _MaildirNameGenerator(reactor).generate
79
def initializeMaildir(dir):
80
if not os.path.isdir(dir):
82
for subdir in ['new', 'cur', 'tmp', '.Trash']:
83
os.mkdir(os.path.join(dir, subdir), 0700)
84
for subdir in ['new', 'cur', 'tmp']:
85
os.mkdir(os.path.join(dir, '.Trash', subdir), 0700)
87
open(os.path.join(dir, '.Trash', 'maildirfolder'), 'w').close()
90
class MaildirMessage(mail.FileMessage):
93
def __init__(self, address, fp, *a, **kw):
94
header = "Delivered-To: %s\n" % address
96
self.size = len(header)
97
mail.FileMessage.__init__(self, fp, *a, **kw)
99
def lineReceived(self, line):
100
mail.FileMessage.lineReceived(self, line)
101
self.size += len(line)+1
103
def eomReceived(self):
104
self.finalName = self.finalName+',S=%d' % self.size
105
return mail.FileMessage.eomReceived(self)
107
class AbstractMaildirDomain:
108
"""Abstract maildir-backed domain.
113
def __init__(self, service, root):
118
def userDirectory(self, user):
119
"""Get the maildir directory for a given user
121
Override to specify where to save mails for users.
122
Return None for non-existing users.
130
def setAliasGroup(self, alias):
136
def exists(self, user, memo=None):
137
"""Check for existence of user in the domain
139
if self.userDirectory(user.dest.local) is not None:
140
return lambda: self.startMessage(user)
142
a = self.alias[user.dest.local]
144
raise smtp.SMTPBadRcpt(user)
146
aliases = a.resolve(self.alias, memo)
148
return lambda: aliases
149
log.err("Bad alias configuration: " + str(user))
150
raise smtp.SMTPBadRcpt(user)
152
def startMessage(self, user):
153
"""Save a message for a given user
155
if isinstance(user, str):
156
name, domain = user.split('@', 1)
158
name, domain = user.dest.local, user.dest.domain
159
dir = self.userDirectory(name)
160
fname = _generateMaildirName()
161
filename = os.path.join(dir, 'tmp', fname)
162
fp = open(filename, 'w')
163
return MaildirMessage('%s@%s' % (name, domain), fp, filename,
164
os.path.join(dir, 'new', fname))
166
def willRelay(self, user, protocol):
169
def addUser(self, user, password):
170
raise NotImplementedError
172
def getCredentialsCheckers(self):
173
raise NotImplementedError
178
class _MaildirMailboxAppendMessageTask:
179
implements(interfaces.IConsumer)
181
osopen = staticmethod(os.open)
182
oswrite = staticmethod(os.write)
183
osclose = staticmethod(os.close)
184
osrename = staticmethod(os.rename)
186
def __init__(self, mbox, msg):
188
self.defer = defer.Deferred()
190
if not hasattr(msg, "read"):
191
msg = StringIO.StringIO(msg)
193
# This is needed, as this startup phase might call defer.errback and zero out self.defer
194
# By doing it on the reactor iteration appendMessage is able to use .defer without problems.
195
reactor.callLater(0, self.startUp)
198
self.createTempFile()
200
self.filesender = basic.FileSender()
201
self.filesender.beginFileTransfer(self.msg, self)
203
def registerProducer(self, producer, streaming):
204
self.myproducer = producer
205
self.streaming = streaming
209
def prodProducer(self):
211
if self.myproducer is not None:
212
self.openCall = reactor.callLater(0, self.prodProducer)
213
self.myproducer.resumeProducing()
215
def unregisterProducer(self):
216
self.myproducer = None
217
self.streaming = None
218
self.osclose(self.fh)
221
def write(self, data):
223
self.oswrite(self.fh, data)
227
def fail(self, err=None):
229
err = failure.Failure()
230
if self.openCall is not None:
231
self.openCall.cancel()
232
self.defer.errback(err)
235
def moveFileToNew(self):
237
newname = os.path.join(self.mbox.path, "new", _generateMaildirName())
239
self.osrename(self.tmpname, newname)
241
except OSError, (err, estr):
243
# if the newname exists, retry with a new newname.
244
if err != errno.EEXIST:
248
if newname is not None:
249
self.mbox.list.append(newname)
250
self.defer.callback(None)
253
def createTempFile(self):
254
attr = (os.O_RDWR | os.O_CREAT | os.O_EXCL
255
| getattr(os, "O_NOINHERIT", 0)
256
| getattr(os, "O_NOFOLLOW", 0))
260
self.tmpname = os.path.join(self.mbox.path, "tmp", _generateMaildirName())
262
self.fh = self.osopen(self.tmpname, attr, 0600)
267
self.defer.errback(RuntimeError("Could not create tmp file for %s" % self.mbox.path))
271
class MaildirMailbox(pop3.Mailbox):
272
"""Implement the POP3 mailbox semantics for a Maildir mailbox
274
AppendFactory = _MaildirMailboxAppendMessageTask
276
def __init__(self, path):
277
"""Initialize with name of the Maildir mailbox
282
initializeMaildir(path)
283
for name in ('cur', 'new'):
284
for file in os.listdir(os.path.join(path, name)):
285
self.list.append((file, os.path.join(path, name, file)))
287
self.list = [e[1] for e in self.list]
289
def listMessages(self, i=None):
290
"""Return a list of lengths of all files in new/ and cur/
294
for mess in self.list:
296
ret.append(os.stat(mess)[stat.ST_SIZE])
300
return self.list[i] and os.stat(self.list[i])[stat.ST_SIZE] or 0
302
def getMessage(self, i):
303
"""Return an open file-pointer to a message
305
return open(self.list[i])
307
def getUidl(self, i):
308
"""Return a unique identifier for a message
310
This is done using the basename of the filename.
311
It is globally unique because this is how Maildirs are designed.
313
# Returning the actual filename is a mistake. Hash it.
314
base = os.path.basename(self.list[i])
315
return md5(base).hexdigest()
317
def deleteMessage(self, i):
320
This only moves a message to the .Trash/ subfolder,
321
so it can be undeleted by an administrator.
323
trashFile = os.path.join(
324
self.path, '.Trash', 'cur', os.path.basename(self.list[i])
326
os.rename(self.list[i], trashFile)
327
self.deleted[self.list[i]] = trashFile
330
def undeleteMessages(self):
331
"""Undelete any deleted messages it is possible to undelete
333
This moves any messages from .Trash/ subfolder back to their
334
original position, and empties out the deleted dictionary.
336
for (real, trash) in self.deleted.items():
338
os.rename(trash, real)
339
except OSError, (err, estr):
341
# If the file has been deleted from disk, oh well!
342
if err != errno.ENOENT:
347
self.list[self.list.index(0)] = real
349
self.list.append(real)
352
def appendMessage(self, txt):
353
"""Appends a message into the mailbox."""
354
task = self.AppendFactory(self, txt)
357
class StringListMailbox:
359
L{StringListMailbox} is an in-memory mailbox.
361
@ivar msgs: A C{list} of C{str} giving the contents of each message in the
364
@ivar _delete: A C{set} of the indexes of messages which have been deleted
365
since the last C{sync} call.
367
implements(pop3.IMailbox)
369
def __init__(self, msgs):
374
def listMessages(self, i=None):
376
Return the length of the message at the given offset, or a list of all
380
return [self.listMessages(i) for i in range(len(self.msgs))]
381
if i in self._delete:
383
return len(self.msgs[i])
386
def getMessage(self, i):
388
Return an in-memory file-like object for the message content at the
391
return StringIO.StringIO(self.msgs[i])
394
def getUidl(self, i):
396
Return a hash of the contents of the message at the given offset.
398
return md5(self.msgs[i]).hexdigest()
401
def deleteMessage(self, i):
403
Mark the given message for deletion.
408
def undeleteMessages(self):
410
Reset deletion tracking, undeleting any messages which have been
411
deleted since the last call to C{sync}.
418
Discard the contents of any message marked for deletion and reset
421
for index in self._delete:
422
self.msgs[index] = ""
427
class MaildirDirdbmDomain(AbstractMaildirDomain):
428
"""A Maildir Domain where membership is checked by a dirdbm file
431
implements(cred.portal.IRealm, mail.IAliasableDomain)
436
def __init__(self, service, root, postmaster=0):
439
The first argument is where the Domain directory is rooted.
440
The second is whether non-existing addresses are simply
441
forwarded to postmaster instead of outright bounce
443
The directory structure of a MailddirDirdbmDomain is:
445
/passwd <-- a dirdbm file
446
/USER/{cur,new,del} <-- each user has these three directories
448
AbstractMaildirDomain.__init__(self, service, root)
449
dbm = os.path.join(root, 'passwd')
450
if not os.path.exists(dbm):
452
self.dbm = dirdbm.open(dbm)
453
self.postmaster = postmaster
455
def userDirectory(self, name):
456
"""Get the directory for a user
458
If the user exists in the dirdbm file, return the directory
459
os.path.join(root, name), creating it if necessary.
460
Otherwise, returns postmaster's mailbox instead if bounces
461
go to postmaster, otherwise return None
463
if not self.dbm.has_key(name):
464
if not self.postmaster:
467
dir = os.path.join(self.root, name)
468
if not os.path.exists(dir):
469
initializeMaildir(dir)
475
def addUser(self, user, password):
476
self.dbm[user] = password
477
# Ensure it is initialized
478
self.userDirectory(user)
480
def getCredentialsCheckers(self):
481
if self._credcheckers is None:
482
self._credcheckers = [DirdbmDatabase(self.dbm)]
483
return self._credcheckers
488
def requestAvatar(self, avatarId, mind, *interfaces):
489
if pop3.IMailbox not in interfaces:
490
raise NotImplementedError("No interface")
491
if avatarId == cred.checkers.ANONYMOUS:
492
mbox = StringListMailbox([INTERNAL_ERROR])
494
mbox = MaildirMailbox(os.path.join(self.root, avatarId))
502
class DirdbmDatabase:
503
implements(cred.checkers.ICredentialsChecker)
505
credentialInterfaces = (
506
cred.credentials.IUsernamePassword,
507
cred.credentials.IUsernameHashedPassword
510
def __init__(self, dbm):
513
def requestAvatarId(self, c):
514
if c.username in self.dirdbm:
515
if c.checkPassword(self.dirdbm[c.username]):
517
raise cred.error.UnauthorizedLogin()