| 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 |