1 # -*- test-case-name: twisted.mail.test.test_mail -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
7 Maildir-style mailbox support
14 from zope.interface import implements
17 import cStringIO as StringIO
21 from twisted.python.compat import set
22 from twisted.mail import pop3
23 from twisted.mail import smtp
24 from twisted.protocols import basic
25 from twisted.persisted import dirdbm
26 from twisted.python import log, failure
27 from twisted.python.hashlib import md5
28 from twisted.mail import mail
29 from twisted.internet import interfaces, defer, reactor
30 from twisted.cred import portal, credentials, checkers
31 from twisted.cred.error import UnauthorizedLogin
34 From: Twisted.mail Internals
35 Subject: An Error Occurred
37 An internal server error has occurred. Please contact the
41 class _MaildirNameGenerator:
43 Utility class to generate a unique maildir name
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
51 s = socket.gethostname().replace('/', r'\057').replace(':', r'\072')
53 def __init__(self, clock):
58 Return a string which is intended to unique across all calls to this
59 function (across all processes, reboots, etc).
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
66 t = self._clock.seconds()
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)
72 _generateMaildirName = _MaildirNameGenerator(reactor).generate
74 def initializeMaildir(dir):
75 if not os.path.isdir(dir):
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)
82 open(os.path.join(dir, '.Trash', 'maildirfolder'), 'w').close()
85 class MaildirMessage(mail.FileMessage):
88 def __init__(self, address, fp, *a, **kw):
89 header = "Delivered-To: %s\n" % address
91 self.size = len(header)
92 mail.FileMessage.__init__(self, fp, *a, **kw)
94 def lineReceived(self, line):
95 mail.FileMessage.lineReceived(self, line)
96 self.size += len(line)+1
98 def eomReceived(self):
99 self.finalName = self.finalName+',S=%d' % self.size
100 return mail.FileMessage.eomReceived(self)
102 class AbstractMaildirDomain:
103 """Abstract maildir-backed domain.
108 def __init__(self, service, root):
113 def userDirectory(self, user):
114 """Get the maildir directory for a given user
116 Override to specify where to save mails for users.
117 Return None for non-existing users.
125 def setAliasGroup(self, alias):
131 def exists(self, user, memo=None):
132 """Check for existence of user in the domain
134 if self.userDirectory(user.dest.local) is not None:
135 return lambda: self.startMessage(user)
137 a = self.alias[user.dest.local]
139 raise smtp.SMTPBadRcpt(user)
141 aliases = a.resolve(self.alias, memo)
143 return lambda: aliases
144 log.err("Bad alias configuration: " + str(user))
145 raise smtp.SMTPBadRcpt(user)
147 def startMessage(self, user):
148 """Save a message for a given user
150 if isinstance(user, str):
151 name, domain = user.split('@', 1)
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))
161 def willRelay(self, user, protocol):
164 def addUser(self, user, password):
165 raise NotImplementedError
167 def getCredentialsCheckers(self):
168 raise NotImplementedError
173 class _MaildirMailboxAppendMessageTask:
174 implements(interfaces.IConsumer)
176 osopen = staticmethod(os.open)
177 oswrite = staticmethod(os.write)
178 osclose = staticmethod(os.close)
179 osrename = staticmethod(os.rename)
181 def __init__(self, mbox, msg):
183 self.defer = defer.Deferred()
185 if not hasattr(msg, "read"):
186 msg = StringIO.StringIO(msg)
190 self.createTempFile()
192 self.filesender = basic.FileSender()
193 self.filesender.beginFileTransfer(self.msg, self)
195 def registerProducer(self, producer, streaming):
196 self.myproducer = producer
197 self.streaming = streaming
201 def prodProducer(self):
203 if self.myproducer is not None:
204 self.openCall = reactor.callLater(0, self.prodProducer)
205 self.myproducer.resumeProducing()
207 def unregisterProducer(self):
208 self.myproducer = None
209 self.streaming = None
210 self.osclose(self.fh)
213 def write(self, data):
215 self.oswrite(self.fh, data)
219 def fail(self, err=None):
221 err = failure.Failure()
222 if self.openCall is not None:
223 self.openCall.cancel()
224 self.defer.errback(err)
227 def moveFileToNew(self):
229 newname = os.path.join(self.mbox.path, "new", _generateMaildirName())
231 self.osrename(self.tmpname, newname)
233 except OSError, (err, estr):
235 # if the newname exists, retry with a new newname.
236 if err != errno.EEXIST:
240 if newname is not None:
241 self.mbox.list.append(newname)
242 self.defer.callback(None)
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))
252 self.tmpname = os.path.join(self.mbox.path, "tmp", _generateMaildirName())
254 self.fh = self.osopen(self.tmpname, attr, 0600)
259 self.defer.errback(RuntimeError("Could not create tmp file for %s" % self.mbox.path))
263 class MaildirMailbox(pop3.Mailbox):
264 """Implement the POP3 mailbox semantics for a Maildir mailbox
266 AppendFactory = _MaildirMailboxAppendMessageTask
268 def __init__(self, path):
269 """Initialize with name of the Maildir mailbox
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)))
279 self.list = [e[1] for e in self.list]
281 def listMessages(self, i=None):
282 """Return a list of lengths of all files in new/ and cur/
286 for mess in self.list:
288 ret.append(os.stat(mess)[stat.ST_SIZE])
292 return self.list[i] and os.stat(self.list[i])[stat.ST_SIZE] or 0
294 def getMessage(self, i):
295 """Return an open file-pointer to a message
297 return open(self.list[i])
299 def getUidl(self, i):
300 """Return a unique identifier for a message
302 This is done using the basename of the filename.
303 It is globally unique because this is how Maildirs are designed.
305 # Returning the actual filename is a mistake. Hash it.
306 base = os.path.basename(self.list[i])
307 return md5(base).hexdigest()
309 def deleteMessage(self, i):
312 This only moves a message to the .Trash/ subfolder,
313 so it can be undeleted by an administrator.
315 trashFile = os.path.join(
316 self.path, '.Trash', 'cur', os.path.basename(self.list[i])
318 os.rename(self.list[i], trashFile)
319 self.deleted[self.list[i]] = trashFile
322 def undeleteMessages(self):
323 """Undelete any deleted messages it is possible to undelete
325 This moves any messages from .Trash/ subfolder back to their
326 original position, and empties out the deleted dictionary.
328 for (real, trash) in self.deleted.items():
330 os.rename(trash, real)
331 except OSError, (err, estr):
333 # If the file has been deleted from disk, oh well!
334 if err != errno.ENOENT:
339 self.list[self.list.index(0)] = real
341 self.list.append(real)
344 def appendMessage(self, txt):
346 Appends a message into the mailbox.
348 @param txt: A C{str} or file-like object giving the message to append.
350 @return: A L{Deferred} which fires when the message has been appended to
353 task = self.AppendFactory(self, txt)
358 class StringListMailbox:
360 L{StringListMailbox} is an in-memory mailbox.
362 @ivar msgs: A C{list} of C{str} giving the contents of each message in the
365 @ivar _delete: A C{set} of the indexes of messages which have been deleted
366 since the last C{sync} call.
368 implements(pop3.IMailbox)
370 def __init__(self, msgs):
375 def listMessages(self, i=None):
377 Return the length of the message at the given offset, or a list of all
381 return [self.listMessages(i) for i in range(len(self.msgs))]
382 if i in self._delete:
384 return len(self.msgs[i])
387 def getMessage(self, i):
389 Return an in-memory file-like object for the message content at the
392 return StringIO.StringIO(self.msgs[i])
395 def getUidl(self, i):
397 Return a hash of the contents of the message at the given offset.
399 return md5(self.msgs[i]).hexdigest()
402 def deleteMessage(self, i):
404 Mark the given message for deletion.
409 def undeleteMessages(self):
411 Reset deletion tracking, undeleting any messages which have been
412 deleted since the last call to C{sync}.
419 Discard the contents of any message marked for deletion and reset
422 for index in self._delete:
423 self.msgs[index] = ""
428 class MaildirDirdbmDomain(AbstractMaildirDomain):
429 """A Maildir Domain where membership is checked by a dirdbm file
432 implements(portal.IRealm, mail.IAliasableDomain)
437 def __init__(self, service, root, postmaster=0):
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
444 The directory structure of a MailddirDirdbmDomain is:
446 /passwd <-- a dirdbm file
447 /USER/{cur,new,del} <-- each user has these three directories
449 AbstractMaildirDomain.__init__(self, service, root)
450 dbm = os.path.join(root, 'passwd')
451 if not os.path.exists(dbm):
453 self.dbm = dirdbm.open(dbm)
454 self.postmaster = postmaster
456 def userDirectory(self, name):
457 """Get the directory for a user
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
464 if not self.dbm.has_key(name):
465 if not self.postmaster:
468 dir = os.path.join(self.root, name)
469 if not os.path.exists(dir):
470 initializeMaildir(dir)
476 def addUser(self, user, password):
477 self.dbm[user] = password
478 # Ensure it is initialized
479 self.userDirectory(user)
481 def getCredentialsCheckers(self):
482 if self._credcheckers is None:
483 self._credcheckers = [DirdbmDatabase(self.dbm)]
484 return self._credcheckers
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])
495 mbox = MaildirMailbox(os.path.join(self.root, avatarId))
503 class DirdbmDatabase:
504 implements(checkers.ICredentialsChecker)
506 credentialInterfaces = (
507 credentials.IUsernamePassword,
508 credentials.IUsernameHashedPassword
511 def __init__(self, dbm):
514 def requestAvatarId(self, c):
515 if c.username in self.dirdbm:
516 if c.checkPassword(self.dirdbm[c.username]):
518 raise UnauthorizedLogin()