Initial import to Tizen
[profile/ivi/python-twisted.git] / twisted / mail / maildir.py
1 # -*- test-case-name: twisted.mail.test.test_mail -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
4
5
6 """
7 Maildir-style mailbox support
8 """
9
10 import os
11 import stat
12 import socket
13
14 from zope.interface import implements
15
16 try:
17     import cStringIO as StringIO
18 except ImportError:
19     import StringIO
20
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
32
33 INTERNAL_ERROR = '''\
34 From: Twisted.mail Internals
35 Subject: An Error Occurred
36
37   An internal server error has occurred.  Please contact the
38   server administrator.
39 '''
40
41 class _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
74 def 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
85 class 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
102 class 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
173 class _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
263 class 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
358 class 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
428 class 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
503 class 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()