1
# -*- test-case-name: twisted.mail.test.test_mail -*-
2
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
3
# See LICENSE for details.
6
"""Maildir-style mailbox support
9
from __future__ import generators
18
from zope.interface import implements
21
import cStringIO as StringIO
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
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
41
From: Twisted.mail Internals
42
Subject: An Error Occurred
44
An internal server error has occurred. Please contact the
48
class _MaildirNameGenerator:
49
"""Utility class to generate a unique maildir name
53
s = socket.gethostname().replace('/', r'\057').replace(':', r'\072')
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)
63
_generateMaildirName = _MaildirNameGenerator().generate
65
def initializeMaildir(dir):
66
if not os.path.isdir(dir):
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)
73
open(os.path.join(dir, '.Trash', 'maildirfolder'), 'w').close()
76
class MaildirMessage(mail.FileMessage):
79
def __init__(self, address, fp, *a, **kw):
80
header = "Delivered-To: %s\n" % address
82
self.size = len(header)
83
mail.FileMessage.__init__(self, fp, *a, **kw)
85
def lineReceived(self, line):
86
mail.FileMessage.lineReceived(self, line)
87
self.size += len(line)+1
89
def eomReceived(self):
90
self.finalName = self.finalName+',S=%d' % self.size
91
return mail.FileMessage.eomReceived(self)
93
class AbstractMaildirDomain:
94
"""Abstract maildir-backed domain.
99
def __init__(self, service, root):
104
def userDirectory(self, user):
105
"""Get the maildir directory for a given user
107
Override to specify where to save mails for users.
108
Return None for non-existing users.
116
def setAliasGroup(self, alias):
122
def exists(self, user, memo=None):
123
"""Check for existence of user in the domain
125
if self.userDirectory(user.dest.local) is not None:
126
return lambda: self.startMessage(user)
128
a = self.alias[user.dest.local]
130
raise smtp.SMTPBadRcpt(user)
132
aliases = a.resolve(self.alias, memo)
134
return lambda: aliases
135
log.err("Bad alias configuration: " + str(user))
136
raise smtp.SMTPBadRcpt(user)
138
def startMessage(self, user):
139
"""Save a message for a given user
141
if isinstance(user, str):
142
name, domain = user.split('@', 1)
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))
152
def willRelay(self, user, protocol):
155
def addUser(self, user, password):
156
raise NotImplementedError
158
def getCredentialsCheckers(self):
159
raise NotImplementedError
164
class _MaildirMailboxAppendMessageTask:
165
implements(interfaces.IConsumer)
167
osopen = staticmethod(os.open)
168
oswrite = staticmethod(os.write)
169
osclose = staticmethod(os.close)
170
osrename = staticmethod(os.rename)
172
def __init__(self, mbox, msg):
174
self.defer = defer.Deferred()
176
if not hasattr(msg, "read"):
177
msg = StringIO.StringIO(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)
184
self.createTempFile()
186
self.filesender = basic.FileSender()
187
self.filesender.beginFileTransfer(self.msg, self)
189
def registerProducer(self, producer, streaming):
190
self.myproducer = producer
191
self.streaming = streaming
195
def prodProducer(self):
197
if self.myproducer is not None:
198
self.openCall = reactor.callLater(0, self.prodProducer)
199
self.myproducer.resumeProducing()
201
def unregisterProducer(self):
202
self.myproducer = None
203
self.streaming = None
204
self.osclose(self.fh)
207
def write(self, data):
209
self.oswrite(self.fh, data)
213
def fail(self, err=None):
215
err = failure.Failure()
216
if self.openCall is not None:
217
self.openCall.cancel()
218
self.defer.errback(err)
221
def moveFileToNew(self):
223
newname = os.path.join(self.mbox.path, "new", _generateMaildirName())
225
self.osrename(self.tmpname, newname)
227
except OSError, (err, estr):
229
# if the newname exists, retry with a new newname.
230
if err != errno.EEXIST:
234
if newname is not None:
235
self.mbox.list.append(newname)
236
self.defer.callback(None)
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))
246
self.tmpname = os.path.join(self.mbox.path, "tmp", _generateMaildirName())
248
self.fh = self.osopen(self.tmpname, attr, 0600)
253
self.defer.errback(RuntimeError("Could not create tmp file for %s" % self.mbox.path))
257
class MaildirMailbox(pop3.Mailbox):
258
"""Implement the POP3 mailbox semantics for a Maildir mailbox
260
AppendFactory = _MaildirMailboxAppendMessageTask
262
def __init__(self, path):
263
"""Initialize with name of the Maildir mailbox
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)))
273
self.list = [e[1] for e in self.list]
275
def listMessages(self, i=None):
276
"""Return a list of lengths of all files in new/ and cur/
280
for mess in self.list:
282
ret.append(os.stat(mess)[stat.ST_SIZE])
286
return self.list[i] and os.stat(self.list[i])[stat.ST_SIZE] or 0
288
def getMessage(self, i):
289
"""Return an open file-pointer to a message
291
return open(self.list[i])
293
def getUidl(self, i):
294
"""Return a unique identifier for a message
296
This is done using the basename of the filename.
297
It is globally unique because this is how Maildirs are designed.
299
# Returning the actual filename is a mistake. Hash it.
300
base = os.path.basename(self.list[i])
301
return md5.md5(base).hexdigest()
303
def deleteMessage(self, i):
306
This only moves a message to the .Trash/ subfolder,
307
so it can be undeleted by an administrator.
309
trashFile = os.path.join(
310
self.path, '.Trash', 'cur', os.path.basename(self.list[i])
312
os.rename(self.list[i], trashFile)
313
self.deleted[self.list[i]] = trashFile
316
def undeleteMessages(self):
317
"""Undelete any deleted messages it is possible to undelete
319
This moves any messages from .Trash/ subfolder back to their
320
original position, and empties out the deleted dictionary.
322
for (real, trash) in self.deleted.items():
324
os.rename(trash, real)
325
except OSError, (err, estr):
327
# If the file has been deleted from disk, oh well!
328
if err != errno.ENOENT:
333
self.list[self.list.index(0)] = real
335
self.list.append(real)
338
def appendMessage(self, txt):
339
"""Appends a message into the mailbox."""
340
task = self.AppendFactory(self, txt)
343
class StringListMailbox:
344
implements(pop3.IMailbox)
346
def __init__(self, msgs):
349
def listMessages(self, i=None):
351
return map(len, self.msgs)
352
return len(self.msgs[i])
354
def getMessage(self, i):
355
return StringIO.StringIO(self.msgs[i])
357
def getUidl(self, i):
358
return md5.new(self.msgs[i]).hexdigest()
360
def deleteMessage(self, i):
363
def undeleteMessages(self):
369
class MaildirDirdbmDomain(AbstractMaildirDomain):
370
"""A Maildir Domain where membership is checked by a dirdbm file
373
implements(cred.portal.IRealm, mail.IAliasableDomain)
378
def __init__(self, service, root, postmaster=0):
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
385
The directory structure of a MailddirDirdbmDomain is:
387
/passwd <-- a dirdbm file
388
/USER/{cur,new,del} <-- each user has these three directories
390
AbstractMaildirDomain.__init__(self, service, root)
391
dbm = os.path.join(root, 'passwd')
392
if not os.path.exists(dbm):
394
self.dbm = dirdbm.open(dbm)
395
self.postmaster = postmaster
397
def userDirectory(self, name):
398
"""Get the directory for a user
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
405
if not self.dbm.has_key(name):
406
if not self.postmaster:
409
dir = os.path.join(self.root, name)
410
if not os.path.exists(dir):
411
initializeMaildir(dir)
417
def addUser(self, user, password):
418
self.dbm[user] = password
419
# Ensure it is initialized
420
self.userDirectory(user)
422
def getCredentialsCheckers(self):
423
if self._credcheckers is None:
424
self._credcheckers = [DirdbmDatabase(self.dbm)]
425
return self._credcheckers
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])
436
mbox = MaildirMailbox(os.path.join(self.root, avatarId))
444
class DirdbmDatabase:
445
implements(cred.checkers.ICredentialsChecker)
447
credentialInterfaces = (
448
cred.credentials.IUsernamePassword,
449
cred.credentials.IUsernameHashedPassword
452
def __init__(self, dbm):
455
def requestAvatarId(self, c):
456
if c.username in self.dirdbm:
457
if c.checkPassword(self.dirdbm[c.username]):
459
raise cred.error.UnauthorizedLogin()