OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.mail.test.test_mail -*- | |
2 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
3 # See LICENSE for details. | |
4 | |
5 | |
6 """Maildir-style mailbox support | |
7 """ | |
8 | |
9 from __future__ import generators | |
10 | |
11 import os | |
12 import stat | |
13 import socket | |
14 import time | |
15 import md5 | |
16 import cStringIO | |
17 | |
18 from zope.interface import implements | |
19 | |
20 try: | |
21 import cStringIO as StringIO | |
22 except ImportError: | |
23 import StringIO | |
24 | |
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 | |
33 | |
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 | |
39 | |
40 INTERNAL_ERROR = '''\ | |
41 From: Twisted.mail Internals | |
42 Subject: An Error Occurred | |
43 | |
44 An internal server error has occurred. Please contact the | |
45 server administrator. | |
46 ''' | |
47 | |
48 class _MaildirNameGenerator: | |
49 """Utility class to generate a unique maildir name | |
50 """ | |
51 n = 0 | |
52 p = os.getpid() | |
53 s = socket.gethostname().replace('/', r'\057').replace(':', r'\072') | |
54 | |
55 def generate(self): | |
56 self.n = self.n + 1 | |
57 t = time.time() | |
58 seconds = str(int(t)) | |
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) | |
62 | |
63 _generateMaildirName = _MaildirNameGenerator().generate | |
64 | |
65 def initializeMaildir(dir): | |
66 if not os.path.isdir(dir): | |
67 os.mkdir(dir, 0700) | |
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) | |
72 # touch | |
73 open(os.path.join(dir, '.Trash', 'maildirfolder'), 'w').close() | |
74 | |
75 | |
76 class MaildirMessage(mail.FileMessage): | |
77 size = None | |
78 | |
79 def __init__(self, address, fp, *a, **kw): | |
80 header = "Delivered-To: %s\n" % address | |
81 fp.write(header) | |
82 self.size = len(header) | |
83 mail.FileMessage.__init__(self, fp, *a, **kw) | |
84 | |
85 def lineReceived(self, line): | |
86 mail.FileMessage.lineReceived(self, line) | |
87 self.size += len(line)+1 | |
88 | |
89 def eomReceived(self): | |
90 self.finalName = self.finalName+',S=%d' % self.size | |
91 return mail.FileMessage.eomReceived(self) | |
92 | |
93 class AbstractMaildirDomain: | |
94 """Abstract maildir-backed domain. | |
95 """ | |
96 alias = None | |
97 root = None | |
98 | |
99 def __init__(self, service, root): | |
100 """Initialize. | |
101 """ | |
102 self.root = root | |
103 | |
104 def userDirectory(self, user): | |
105 """Get the maildir directory for a given user | |
106 | |
107 Override to specify where to save mails for users. | |
108 Return None for non-existing users. | |
109 """ | |
110 return None | |
111 | |
112 ## | |
113 ## IAliasableDomain | |
114 ## | |
115 | |
116 def setAliasGroup(self, alias): | |
117 self.alias = alias | |
118 | |
119 ## | |
120 ## IDomain | |
121 ## | |
122 def exists(self, user, memo=None): | |
123 """Check for existence of user in the domain | |
124 """ | |
125 if self.userDirectory(user.dest.local) is not None: | |
126 return lambda: self.startMessage(user) | |
127 try: | |
128 a = self.alias[user.dest.local] | |
129 except: | |
130 raise smtp.SMTPBadRcpt(user) | |
131 else: | |
132 aliases = a.resolve(self.alias, memo) | |
133 if aliases: | |
134 return lambda: aliases | |
135 log.err("Bad alias configuration: " + str(user)) | |
136 raise smtp.SMTPBadRcpt(user) | |
137 | |
138 def startMessage(self, user): | |
139 """Save a message for a given user | |
140 """ | |
141 if isinstance(user, str): | |
142 name, domain = user.split('@', 1) | |
143 else: | |
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)) | |
151 | |
152 def willRelay(self, user, protocol): | |
153 return False | |
154 | |
155 def addUser(self, user, password): | |
156 raise NotImplementedError | |
157 | |
158 def getCredentialsCheckers(self): | |
159 raise NotImplementedError | |
160 ## | |
161 ## end of IDomain | |
162 ## | |
163 | |
164 class _MaildirMailboxAppendMessageTask: | |
165 implements(interfaces.IConsumer) | |
166 | |
167 osopen = staticmethod(os.open) | |
168 oswrite = staticmethod(os.write) | |
169 osclose = staticmethod(os.close) | |
170 osrename = staticmethod(os.rename) | |
171 | |
172 def __init__(self, mbox, msg): | |
173 self.mbox = mbox | |
174 self.defer = defer.Deferred() | |
175 self.openCall = None | |
176 if not hasattr(msg, "read"): | |
177 msg = StringIO.StringIO(msg) | |
178 self.msg = msg | |
179 # This is needed, as this startup phase might call defer.errback and zer
o out self.defer | |
180 # By doing it on the reactor iteration appendMessage is able to use .def
er without problems. | |
181 reactor.callLater(0, self.startUp) | |
182 | |
183 def startUp(self): | |
184 self.createTempFile() | |
185 if self.fh != -1: | |
186 self.filesender = basic.FileSender() | |
187 self.filesender.beginFileTransfer(self.msg, self) | |
188 | |
189 def registerProducer(self, producer, streaming): | |
190 self.myproducer = producer | |
191 self.streaming = streaming | |
192 if not streaming: | |
193 self.prodProducer() | |
194 | |
195 def prodProducer(self): | |
196 self.openCall = None | |
197 if self.myproducer is not None: | |
198 self.openCall = reactor.callLater(0, self.prodProducer) | |
199 self.myproducer.resumeProducing() | |
200 | |
201 def unregisterProducer(self): | |
202 self.myproducer = None | |
203 self.streaming = None | |
204 self.osclose(self.fh) | |
205 self.moveFileToNew() | |
206 | |
207 def write(self, data): | |
208 try: | |
209 self.oswrite(self.fh, data) | |
210 except: | |
211 self.fail() | |
212 | |
213 def fail(self, err=None): | |
214 if err is None: | |
215 err = failure.Failure() | |
216 if self.openCall is not None: | |
217 self.openCall.cancel() | |
218 self.defer.errback(err) | |
219 self.defer = None | |
220 | |
221 def moveFileToNew(self): | |
222 while True: | |
223 newname = os.path.join(self.mbox.path, "new", _generateMaildirName()
) | |
224 try: | |
225 self.osrename(self.tmpname, newname) | |
226 break | |
227 except OSError, (err, estr): | |
228 import errno | |
229 # if the newname exists, retry with a new newname. | |
230 if err != errno.EEXIST: | |
231 self.fail() | |
232 newname = None | |
233 break | |
234 if newname is not None: | |
235 self.mbox.list.append(newname) | |
236 self.defer.callback(None) | |
237 self.defer = None | |
238 | |
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)) | |
243 tries = 0 | |
244 self.fh = -1 | |
245 while True: | |
246 self.tmpname = os.path.join(self.mbox.path, "tmp", _generateMaildirN
ame()) | |
247 try: | |
248 self.fh = self.osopen(self.tmpname, attr, 0600) | |
249 return None | |
250 except OSError: | |
251 tries += 1 | |
252 if tries > 500: | |
253 self.defer.errback(RuntimeError("Could not create tmp file f
or %s" % self.mbox.path)) | |
254 self.defer = None | |
255 return None | |
256 | |
257 class MaildirMailbox(pop3.Mailbox): | |
258 """Implement the POP3 mailbox semantics for a Maildir mailbox | |
259 """ | |
260 AppendFactory = _MaildirMailboxAppendMessageTask | |
261 | |
262 def __init__(self, path): | |
263 """Initialize with name of the Maildir mailbox | |
264 """ | |
265 self.path = path | |
266 self.list = [] | |
267 self.deleted = {} | |
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))) | |
272 self.list.sort() | |
273 self.list = [e[1] for e in self.list] | |
274 | |
275 def listMessages(self, i=None): | |
276 """Return a list of lengths of all files in new/ and cur/ | |
277 """ | |
278 if i is None: | |
279 ret = [] | |
280 for mess in self.list: | |
281 if mess: | |
282 ret.append(os.stat(mess)[stat.ST_SIZE]) | |
283 else: | |
284 ret.append(0) | |
285 return ret | |
286 return self.list[i] and os.stat(self.list[i])[stat.ST_SIZE] or 0 | |
287 | |
288 def getMessage(self, i): | |
289 """Return an open file-pointer to a message | |
290 """ | |
291 return open(self.list[i]) | |
292 | |
293 def getUidl(self, i): | |
294 """Return a unique identifier for a message | |
295 | |
296 This is done using the basename of the filename. | |
297 It is globally unique because this is how Maildirs are designed. | |
298 """ | |
299 # Returning the actual filename is a mistake. Hash it. | |
300 base = os.path.basename(self.list[i]) | |
301 return md5.md5(base).hexdigest() | |
302 | |
303 def deleteMessage(self, i): | |
304 """Delete a message | |
305 | |
306 This only moves a message to the .Trash/ subfolder, | |
307 so it can be undeleted by an administrator. | |
308 """ | |
309 trashFile = os.path.join( | |
310 self.path, '.Trash', 'cur', os.path.basename(self.list[i]) | |
311 ) | |
312 os.rename(self.list[i], trashFile) | |
313 self.deleted[self.list[i]] = trashFile | |
314 self.list[i] = 0 | |
315 | |
316 def undeleteMessages(self): | |
317 """Undelete any deleted messages it is possible to undelete | |
318 | |
319 This moves any messages from .Trash/ subfolder back to their | |
320 original position, and empties out the deleted dictionary. | |
321 """ | |
322 for (real, trash) in self.deleted.items(): | |
323 try: | |
324 os.rename(trash, real) | |
325 except OSError, (err, estr): | |
326 import errno | |
327 # If the file has been deleted from disk, oh well! | |
328 if err != errno.ENOENT: | |
329 raise | |
330 # This is a pass | |
331 else: | |
332 try: | |
333 self.list[self.list.index(0)] = real | |
334 except ValueError: | |
335 self.list.append(real) | |
336 self.deleted.clear() | |
337 | |
338 def appendMessage(self, txt): | |
339 """Appends a message into the mailbox.""" | |
340 task = self.AppendFactory(self, txt) | |
341 return task.defer | |
342 | |
343 class StringListMailbox: | |
344 implements(pop3.IMailbox) | |
345 | |
346 def __init__(self, msgs): | |
347 self.msgs = msgs | |
348 | |
349 def listMessages(self, i=None): | |
350 if i is None: | |
351 return map(len, self.msgs) | |
352 return len(self.msgs[i]) | |
353 | |
354 def getMessage(self, i): | |
355 return StringIO.StringIO(self.msgs[i]) | |
356 | |
357 def getUidl(self, i): | |
358 return md5.new(self.msgs[i]).hexdigest() | |
359 | |
360 def deleteMessage(self, i): | |
361 pass | |
362 | |
363 def undeleteMessages(self): | |
364 pass | |
365 | |
366 def sync(self): | |
367 pass | |
368 | |
369 class MaildirDirdbmDomain(AbstractMaildirDomain): | |
370 """A Maildir Domain where membership is checked by a dirdbm file | |
371 """ | |
372 | |
373 implements(cred.portal.IRealm, mail.IAliasableDomain) | |
374 | |
375 portal = None | |
376 _credcheckers = None | |
377 | |
378 def __init__(self, service, root, postmaster=0): | |
379 """Initialize | |
380 | |
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 | |
384 | |
385 The directory structure of a MailddirDirdbmDomain is: | |
386 | |
387 /passwd <-- a dirdbm file | |
388 /USER/{cur,new,del} <-- each user has these three directories | |
389 """ | |
390 AbstractMaildirDomain.__init__(self, service, root) | |
391 dbm = os.path.join(root, 'passwd') | |
392 if not os.path.exists(dbm): | |
393 os.makedirs(dbm) | |
394 self.dbm = dirdbm.open(dbm) | |
395 self.postmaster = postmaster | |
396 | |
397 def userDirectory(self, name): | |
398 """Get the directory for a user | |
399 | |
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 | |
404 """ | |
405 if not self.dbm.has_key(name): | |
406 if not self.postmaster: | |
407 return None | |
408 name = 'postmaster' | |
409 dir = os.path.join(self.root, name) | |
410 if not os.path.exists(dir): | |
411 initializeMaildir(dir) | |
412 return dir | |
413 | |
414 ## | |
415 ## IDomain | |
416 ## | |
417 def addUser(self, user, password): | |
418 self.dbm[user] = password | |
419 # Ensure it is initialized | |
420 self.userDirectory(user) | |
421 | |
422 def getCredentialsCheckers(self): | |
423 if self._credcheckers is None: | |
424 self._credcheckers = [DirdbmDatabase(self.dbm)] | |
425 return self._credcheckers | |
426 | |
427 ## | |
428 ## IRealm | |
429 ## | |
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]) | |
435 else: | |
436 mbox = MaildirMailbox(os.path.join(self.root, avatarId)) | |
437 | |
438 return ( | |
439 pop3.IMailbox, | |
440 mbox, | |
441 lambda: None | |
442 ) | |
443 | |
444 class DirdbmDatabase: | |
445 implements(cred.checkers.ICredentialsChecker) | |
446 | |
447 credentialInterfaces = ( | |
448 cred.credentials.IUsernamePassword, | |
449 cred.credentials.IUsernameHashedPassword | |
450 ) | |
451 | |
452 def __init__(self, dbm): | |
453 self.dirdbm = dbm | |
454 | |
455 def requestAvatarId(self, c): | |
456 if c.username in self.dirdbm: | |
457 if c.checkPassword(self.dirdbm[c.username]): | |
458 return c.username | |
459 raise cred.error.UnauthorizedLogin() | |
OLD | NEW |