1 #! /usr/bin/env python 2 3 """Read/write support for Maildir, mbox, MH, Babyl, and MMDF mailboxes.""" 4 5 # Notes for authors of new mailbox subclasses: 6 # 7 # Remember to fsync() changes to disk before closing a modified file 8 # or returning from a flush() method. See functions _sync_flush() and 9 # _sync_close(). 10 11 import sys 12 import os 13 import time 14 import calendar 15 import socket 16 import errno 17 import copy 18 import email 19 import email.message 20 import email.generator 21 import StringIO 22 try: 23 if sys.platform == 'os2emx': 24 # OS/2 EMX fcntl() not adequate 25 raise ImportError 26 import fcntl 27 except ImportError: 28 fcntl = None 29 30 import warnings 31 with warnings.catch_warnings(): 32 if sys.py3kwarning: 33 warnings.filterwarnings("ignore", ".*rfc822 has been removed", 34 DeprecationWarning) 35 import rfc822 36 37 __all__ = [ 'Mailbox', 'Maildir', 'mbox', 'MH', 'Babyl', 'MMDF', 38 'Message', 'MaildirMessage', 'mboxMessage', 'MHMessage', 39 'BabylMessage', 'MMDFMessage', 'UnixMailbox', 40 'PortableUnixMailbox', 'MmdfMailbox', 'MHMailbox', 'BabylMailbox' ] 41 42 class Mailbox: 43 """A group of messages in a particular place.""" 44 45 def __init__(self, path, factory=None, create=True): 46 """Initialize a Mailbox instance.""" 47 self._path = os.path.abspath(os.path.expanduser(path)) 48 self._factory = factory 49 50 def add(self, message): 51 """Add message and return assigned key.""" 52 raise NotImplementedError('Method must be implemented by subclass') 53 54 def remove(self, key): 55 """Remove the keyed message; raise KeyError if it doesn't exist.""" 56 raise NotImplementedError('Method must be implemented by subclass') 57 58 def __delitem__(self, key): 59 self.remove(key) 60 61 def discard(self, key): 62 """If the keyed message exists, remove it.""" 63 try: 64 self.remove(key) 65 except KeyError: 66 pass 67 68 def __setitem__(self, key, message): 69 """Replace the keyed message; raise KeyError if it doesn't exist.""" 70 raise NotImplementedError('Method must be implemented by subclass') 71 72 def get(self, key, default=None): 73 """Return the keyed message, or default if it doesn't exist.""" 74 try: 75 return self.__getitem__(key) 76 except KeyError: 77 return default 78 79 def __getitem__(self, key): 80 """Return the keyed message; raise KeyError if it doesn't exist.""" 81 if not self._factory: 82 return self.get_message(key) 83 else: 84 return self._factory(self.get_file(key)) 85 86 def get_message(self, key): 87 """Return a Message representation or raise a KeyError.""" 88 raise NotImplementedError('Method must be implemented by subclass') 89 90 def get_string(self, key): 91 """Return a string representation or raise a KeyError.""" 92 raise NotImplementedError('Method must be implemented by subclass') 93 94 def get_file(self, key): 95 """Return a file-like representation or raise a KeyError.""" 96 raise NotImplementedError('Method must be implemented by subclass') 97 98 def iterkeys(self): 99 """Return an iterator over keys.""" 100 raise NotImplementedError('Method must be implemented by subclass') 101 102 def keys(self): 103 """Return a list of keys.""" 104 return list(self.iterkeys()) 105 106 def itervalues(self): 107 """Return an iterator over all messages.""" 108 for key in self.iterkeys(): 109 try: 110 value = self[key] 111 except KeyError: 112 continue 113 yield value 114 115 def __iter__(self): 116 return self.itervalues() 117 118 def values(self): 119 """Return a list of messages. Memory intensive.""" 120 return list(self.itervalues()) 121 122 def iteritems(self): 123 """Return an iterator over (key, message) tuples.""" 124 for key in self.iterkeys(): 125 try: 126 value = self[key] 127 except KeyError: 128 continue 129 yield (key, value) 130 131 def items(self): 132 """Return a list of (key, message) tuples. Memory intensive.""" 133 return list(self.iteritems()) 134 135 def has_key(self, key): 136 """Return True if the keyed message exists, False otherwise.""" 137 raise NotImplementedError('Method must be implemented by subclass') 138 139 def __contains__(self, key): 140 return self.has_key(key) 141 142 def __len__(self): 143 """Return a count of messages in the mailbox.""" 144 raise NotImplementedError('Method must be implemented by subclass') 145 146 def clear(self): 147 """Delete all messages.""" 148 for key in self.iterkeys(): 149 self.discard(key) 150 151 def pop(self, key, default=None): 152 """Delete the keyed message and return it, or default.""" 153 try: 154 result = self[key] 155 except KeyError: 156 return default 157 self.discard(key) 158 return result 159 160 def popitem(self): 161 """Delete an arbitrary (key, message) pair and return it.""" 162 for key in self.iterkeys(): 163 return (key, self.pop(key)) # This is only run once. 164 else: 165 raise KeyError('No messages in mailbox') 166 167 def update(self, arg=None): 168 """Change the messages that correspond to certain keys.""" 169 if hasattr(arg, 'iteritems'): 170 source = arg.iteritems() 171 elif hasattr(arg, 'items'): 172 source = arg.items() 173 else: 174 source = arg 175 bad_key = False 176 for key, message in source: 177 try: 178 self[key] = message 179 except KeyError: 180 bad_key = True 181 if bad_key: 182 raise KeyError('No message with key(s)') 183 184 def flush(self): 185 """Write any pending changes to the disk.""" 186 raise NotImplementedError('Method must be implemented by subclass') 187 188 def lock(self): 189 """Lock the mailbox.""" 190 raise NotImplementedError('Method must be implemented by subclass') 191 192 def unlock(self): 193 """Unlock the mailbox if it is locked.""" 194 raise NotImplementedError('Method must be implemented by subclass') 195 196 def close(self): 197 """Flush and close the mailbox.""" 198 raise NotImplementedError('Method must be implemented by subclass') 199 200 # Whether each message must end in a newline 201 _append_newline = False 202 203 def _dump_message(self, message, target, mangle_from_=False): 204 # Most files are opened in binary mode to allow predictable seeking. 205 # To get native line endings on disk, the user-friendly \n line endings 206 # used in strings and by email.Message are translated here. 207 """Dump message contents to target file.""" 208 if isinstance(message, email.message.Message): 209 buffer = StringIO.StringIO() 210 gen = email.generator.Generator(buffer, mangle_from_, 0) 211 gen.flatten(message) 212 buffer.seek(0) 213 data = buffer.read().replace('\n', os.linesep) 214 target.write(data) 215 if self._append_newline and not data.endswith(os.linesep): 216 # Make sure the message ends with a newline 217 target.write(os.linesep) 218 elif isinstance(message, str): 219 if mangle_from_: 220 message = message.replace('\nFrom ', '\n>From ') 221 message = message.replace('\n', os.linesep) 222 target.write(message) 223 if self._append_newline and not message.endswith(os.linesep): 224 # Make sure the message ends with a newline 225 target.write(os.linesep) 226 elif hasattr(message, 'read'): 227 lastline = None 228 while True: 229 line = message.readline() 230 if line == '': 231 break 232 if mangle_from_ and line.startswith('From '): 233 line = '>From ' + line[5:] 234 line = line.replace('\n', os.linesep) 235 target.write(line) 236 lastline = line 237 if self._append_newline and lastline and not lastline.endswith(os.linesep): 238 # Make sure the message ends with a newline 239 target.write(os.linesep) 240 else: 241 raise TypeError('Invalid message type: %s' % type(message)) 242 243 244 class Maildir(Mailbox): 245 """A qmail-style Maildir mailbox.""" 246 247 colon = ':' 248 249 def __init__(self, dirname, factory=rfc822.Message, create=True): 250 """Initialize a Maildir instance.""" 251 Mailbox.__init__(self, dirname, factory, create) 252 self._paths = { 253 'tmp': os.path.join(self._path, 'tmp'), 254 'new': os.path.join(self._path, 'new'), 255 'cur': os.path.join(self._path, 'cur'), 256 } 257 if not os.path.exists(self._path): 258 if create: 259 os.mkdir(self._path, 0700) 260 for path in self._paths.values(): 261 os.mkdir(path, 0o700) 262 else: 263 raise NoSuchMailboxError(self._path) 264 self._toc = {} 265 self._toc_mtimes = {'cur': 0, 'new': 0} 266 self._last_read = 0 # Records last time we read cur/new 267 self._skewfactor = 0.1 # Adjust if os/fs clocks are skewing 268 269 def add(self, message): 270 """Add message and return assigned key.""" 271 tmp_file = self._create_tmp() 272 try: 273 self._dump_message(message, tmp_file) 274 except BaseException: 275 tmp_file.close() 276 os.remove(tmp_file.name) 277 raise 278 _sync_close(tmp_file) 279 if isinstance(message, MaildirMessage): 280 subdir = message.get_subdir() 281 suffix = self.colon + message.get_info() 282 if suffix == self.colon: 283 suffix = '' 284 else: 285 subdir = 'new' 286 suffix = '' 287 uniq = os.path.basename(tmp_file.name).split(self.colon)[0] 288 dest = os.path.join(self._path, subdir, uniq + suffix) 289 try: 290 if hasattr(os, 'link'): 291 os.link(tmp_file.name, dest) 292 os.remove(tmp_file.name) 293 else: 294 os.rename(tmp_file.name, dest) 295 except OSError, e: 296 os.remove(tmp_file.name) 297 if e.errno == errno.EEXIST: 298 raise ExternalClashError('Name clash with existing message: %s' 299 % dest) 300 else: 301 raise 302 if isinstance(message, MaildirMessage): 303 os.utime(dest, (os.path.getatime(dest), message.get_date())) 304 return uniq 305 306 def remove(self, key): 307 """Remove the keyed message; raise KeyError if it doesn't exist.""" 308 os.remove(os.path.join(self._path, self._lookup(key))) 309 310 def discard(self, key): 311 """If the keyed message exists, remove it.""" 312 # This overrides an inapplicable implementation in the superclass. 313 try: 314 self.remove(key) 315 except KeyError: 316 pass 317 except OSError, e: 318 if e.errno != errno.ENOENT: 319 raise 320 321 def __setitem__(self, key, message): 322 """Replace the keyed message; raise KeyError if it doesn't exist.""" 323 old_subpath = self._lookup(key) 324 temp_key = self.add(message) 325 temp_subpath = self._lookup(temp_key) 326 if isinstance(message, MaildirMessage): 327 # temp's subdir and suffix were specified by message. 328 dominant_subpath = temp_subpath 329 else: 330 # temp's subdir and suffix were defaults from add(). 331 dominant_subpath = old_subpath 332 subdir = os.path.dirname(dominant_subpath) 333 if self.colon in dominant_subpath: 334 suffix = self.colon + dominant_subpath.split(self.colon)[-1] 335 else: 336 suffix = '' 337 self.discard(key) 338 new_path = os.path.join(self._path, subdir, key + suffix) 339 os.rename(os.path.join(self._path, temp_subpath), new_path) 340 if isinstance(message, MaildirMessage): 341 os.utime(new_path, (os.path.getatime(new_path), 342 message.get_date())) 343 344 def get_message(self, key): 345 """Return a Message representation or raise a KeyError.""" 346 subpath = self._lookup(key) 347 f = open(os.path.join(self._path, subpath), 'r') 348 try: 349 if self._factory: 350 msg = self._factory(f) 351 else: 352 msg = MaildirMessage(f) 353 finally: 354 f.close() 355 subdir, name = os.path.split(subpath) 356 msg.set_subdir(subdir) 357 if self.colon in name: 358 msg.set_info(name.split(self.colon)[-1]) 359 msg.set_date(os.path.getmtime(os.path.join(self._path, subpath))) 360 return msg 361 362 def get_string(self, key): 363 """Return a string representation or raise a KeyError.""" 364 f = open(os.path.join(self._path, self._lookup(key)), 'r') 365 try: 366 return f.read() 367 finally: 368 f.close() 369 370 def get_file(self, key): 371 """Return a file-like representation or raise a KeyError.""" 372 f = open(os.path.join(self._path, self._lookup(key)), 'rb') 373 return _ProxyFile(f) 374 375 def iterkeys(self): 376 """Return an iterator over keys.""" 377 self._refresh() 378 for key in self._toc: 379 try: 380 self._lookup(key) 381 except KeyError: 382 continue 383 yield key 384 385 def has_key(self, key): 386 """Return True if the keyed message exists, False otherwise.""" 387 self._refresh() 388 return key in self._toc 389 390 def __len__(self): 391 """Return a count of messages in the mailbox.""" 392 self._refresh() 393 return len(self._toc) 394 395 def flush(self): 396 """Write any pending changes to disk.""" 397 # Maildir changes are always written immediately, so there's nothing 398 # to do. 399 pass 400 401 def lock(self): 402 """Lock the mailbox.""" 403 return 404 405 def unlock(self): 406 """Unlock the mailbox if it is locked.""" 407 return 408 409 def close(self): 410 """Flush and close the mailbox.""" 411 return 412 413 def list_folders(self): 414 """Return a list of folder names.""" 415 result = [] 416 for entry in os.listdir(self._path): 417 if len(entry) > 1 and entry[0] == '.' and \ 418 os.path.isdir(os.path.join(self._path, entry)): 419 result.append(entry[1:]) 420 return result 421 422 def get_folder(self, folder): 423 """Return a Maildir instance for the named folder.""" 424 return Maildir(os.path.join(self._path, '.' + folder), 425 factory=self._factory, 426 create=False) 427 428 def add_folder(self, folder): 429 """Create a folder and return a Maildir instance representing it.""" 430 path = os.path.join(self._path, '.' + folder) 431 result = Maildir(path, factory=self._factory) 432 maildirfolder_path = os.path.join(path, 'maildirfolder') 433 if not os.path.exists(maildirfolder_path): 434 os.close(os.open(maildirfolder_path, os.O_CREAT | os.O_WRONLY, 435 0666)) 436 return result 437 438 def remove_folder(self, folder): 439 """Delete the named folder, which must be empty.""" 440 path = os.path.join(self._path, '.' + folder) 441 for entry in os.listdir(os.path.join(path, 'new')) + \ 442 os.listdir(os.path.join(path, 'cur')): 443 if len(entry) < 1 or entry[0] != '.': 444 raise NotEmptyError('Folder contains message(s): %s' % folder) 445 for entry in os.listdir(path): 446 if entry != 'new' and entry != 'cur' and entry != 'tmp' and \ 447 os.path.isdir(os.path.join(path, entry)): 448 raise NotEmptyError("Folder contains subdirectory '%s': %s" % 449 (folder, entry)) 450 for root, dirs, files in os.walk(path, topdown=False): 451 for entry in files: 452 os.remove(os.path.join(root, entry)) 453 for entry in dirs: 454 os.rmdir(os.path.join(root, entry)) 455 os.rmdir(path) 456 457 def clean(self): 458 """Delete old files in "tmp".""" 459 now = time.time() 460 for entry in os.listdir(os.path.join(self._path, 'tmp')): 461 path = os.path.join(self._path, 'tmp', entry) 462 if now - os.path.getatime(path) > 129600: # 60 * 60 * 36 463 os.remove(path) 464 465 _count = 1 # This is used to generate unique file names. 466 467 def _create_tmp(self): 468 """Create a file in the tmp subdirectory and open and return it.""" 469 now = time.time() 470 hostname = socket.gethostname() 471 if '/' in hostname: 472 hostname = hostname.replace('/', r'\057') 473 if ':' in hostname: 474 hostname = hostname.replace(':', r'\072') 475 uniq = "%s.M%sP%sQ%s.%s" % (int(now), int(now % 1 * 1e6), os.getpid(), 476 Maildir._count, hostname) 477 path = os.path.join(self._path, 'tmp', uniq) 478 try: 479 os.stat(path) 480 except OSError, e: 481 if e.errno == errno.ENOENT: 482 Maildir._count += 1 483 try: 484 return _create_carefully(path) 485 except OSError, e: 486 if e.errno != errno.EEXIST: 487 raise 488 else: 489 raise 490 491 # Fall through to here if stat succeeded or open raised EEXIST. 492 raise ExternalClashError('Name clash prevented file creation: %s' % 493 path) 494 495 def _refresh(self): 496 """Update table of contents mapping.""" 497 # If it has been less than two seconds since the last _refresh() call, 498 # we have to unconditionally re-read the mailbox just in case it has 499 # been modified, because os.path.mtime() has a 2 sec resolution in the 500 # most common worst case (FAT) and a 1 sec resolution typically. This 501 # results in a few unnecessary re-reads when _refresh() is called 502 # multiple times in that interval, but once the clock ticks over, we 503 # will only re-read as needed. Because the filesystem might be being 504 # served by an independent system with its own clock, we record and 505 # compare with the mtimes from the filesystem. Because the other 506 # system's clock might be skewing relative to our clock, we add an 507 # extra delta to our wait. The default is one tenth second, but is an 508 # instance variable and so can be adjusted if dealing with a 509 # particularly skewed or irregular system. 510 if time.time() - self._last_read > 2 + self._skewfactor: 511 refresh = False 512 for subdir in self._toc_mtimes: 513 mtime = os.path.getmtime(self._paths[subdir]) 514 if mtime > self._toc_mtimes[subdir]: 515 refresh = True 516 self._toc_mtimes[subdir] = mtime 517 if not refresh: 518 return 519 # Refresh toc 520 self._toc = {} 521 for subdir in self._toc_mtimes: 522 path = self._paths[subdir] 523 for entry in os.listdir(path): 524 p = os.path.join(path, entry) 525 if os.path.isdir(p): 526 continue 527 uniq = entry.split(self.colon)[0] 528 self._toc[uniq] = os.path.join(subdir, entry) 529 self._last_read = time.time() 530 531 def _lookup(self, key): 532 """Use TOC to return subpath for given key, or raise a KeyError.""" 533 try: 534 if os.path.exists(os.path.join(self._path, self._toc[key])): 535 return self._toc[key] 536 except KeyError: 537 pass 538 self._refresh() 539 try: 540 return self._toc[key] 541 except KeyError: 542 raise KeyError('No message with key: %s' % key) 543 544 # This method is for backward compatibility only. 545 def next(self): 546 """Return the next message in a one-time iteration.""" 547 if not hasattr(self, '_onetime_keys'): 548 self._onetime_keys = self.iterkeys() 549 while True: 550 try: 551 return self[self._onetime_keys.next()] 552 except StopIteration: 553 return None 554 except KeyError: 555 continue 556 557 558 class _singlefileMailbox(Mailbox): 559 """A single-file mailbox.""" 560 561 def __init__(self, path, factory=None, create=True): 562 """Initialize a single-file mailbox.""" 563 Mailbox.__init__(self, path, factory, create) 564 try: 565 f = open(self._path, 'rb+') 566 except IOError, e: 567 if e.errno == errno.ENOENT: 568 if create: 569 f = open(self._path, 'wb+') 570 else: 571 raise NoSuchMailboxError(self._path) 572 elif e.errno in (errno.EACCES, errno.EROFS): 573 f = open(self._path, 'rb') 574 else: 575 raise 576 self._file = f 577 self._toc = None 578 self._next_key = 0 579 self._pending = False # No changes require rewriting the file. 580 self._pending_sync = False # No need to sync the file 581 self._locked = False 582 self._file_length = None # Used to record mailbox size 583 584 def add(self, message): 585 """Add message and return assigned key.""" 586 self._lookup() 587 self._toc[self._next_key] = self._append_message(message) 588 self._next_key += 1 589 # _append_message appends the message to the mailbox file. We 590 # don't need a full rewrite + rename, sync is enough. 591 self._pending_sync = True 592 return self._next_key - 1 593 594 def remove(self, key): 595 """Remove the keyed message; raise KeyError if it doesn't exist.""" 596 self._lookup(key) 597 del self._toc[key] 598 self._pending = True 599 600 def __setitem__(self, key, message): 601 """Replace the keyed message; raise KeyError if it doesn't exist.""" 602 self._lookup(key) 603 self._toc[key] = self._append_message(message) 604 self._pending = True 605 606 def iterkeys(self): 607 """Return an iterator over keys.""" 608 self._lookup() 609 for key in self._toc.keys(): 610 yield key 611 612 def has_key(self, key): 613 """Return True if the keyed message exists, False otherwise.""" 614 self._lookup() 615 return key in self._toc 616 617 def __len__(self): 618 """Return a count of messages in the mailbox.""" 619 self._lookup() 620 return len(self._toc) 621 622 def lock(self): 623 """Lock the mailbox.""" 624 if not self._locked: 625 _lock_file(self._file) 626 self._locked = True 627 628 def unlock(self): 629 """Unlock the mailbox if it is locked.""" 630 if self._locked: 631 _unlock_file(self._file) 632 self._locked = False 633 634 def flush(self): 635 """Write any pending changes to disk.""" 636 if not self._pending: 637 if self._pending_sync: 638 # Messages have only been added, so syncing the file 639 # is enough. 640 _sync_flush(self._file) 641 self._pending_sync = False 642 return 643 644 # In order to be writing anything out at all, self._toc must 645 # already have been generated (and presumably has been modified 646 # by adding or deleting an item). 647 assert self._toc is not None 648 649 # Check length of self._file; if it's changed, some other process 650 # has modified the mailbox since we scanned it. 651 self._file.seek(0, 2) 652 cur_len = self._file.tell() 653 if cur_len != self._file_length: 654 raise ExternalClashError('Size of mailbox file changed ' 655 '(expected %i, found %i)' % 656 (self._file_length, cur_len)) 657 658 new_file = _create_temporary(self._path) 659 try: 660 new_toc = {} 661 self._pre_mailbox_hook(new_file) 662 for key in sorted(self._toc.keys()): 663 start, stop = self._toc[key] 664 self._file.seek(start) 665 self._pre_message_hook(new_file) 666 new_start = new_file.tell() 667 while True: 668 buffer = self._file.read(min(4096, 669 stop - self._file.tell())) 670 if buffer == '': 671 break 672 new_file.write(buffer) 673 new_toc[key] = (new_start, new_file.tell()) 674 self._post_message_hook(new_file) 675 self._file_length = new_file.tell() 676 except: 677 new_file.close() 678 os.remove(new_file.name) 679 raise 680 _sync_close(new_file) 681 # self._file is about to get replaced, so no need to sync. 682 self._file.close() 683 # Make sure the new file's mode is the same as the old file's 684 mode = os.stat(self._path).st_mode 685 os.chmod(new_file.name, mode) 686 try: 687 os.rename(new_file.name, self._path) 688 except OSError, e: 689 if e.errno == errno.EEXIST or \ 690 (os.name == 'os2' and e.errno == errno.EACCES): 691 os.remove(self._path) 692 os.rename(new_file.name, self._path) 693 else: 694 raise 695 self._file = open(self._path, 'rb+') 696 self._toc = new_toc 697 self._pending = False 698 self._pending_sync = False 699 if self._locked: 700 _lock_file(self._file, dotlock=False) 701 702 def _pre_mailbox_hook(self, f): 703 """Called before writing the mailbox to file f.""" 704 return 705 706 def _pre_message_hook(self, f): 707 """Called before writing each message to file f.""" 708 return 709 710 def _post_message_hook(self, f): 711 """Called after writing each message to file f.""" 712 return 713 714 def close(self): 715 """Flush and close the mailbox.""" 716 self.flush() 717 if self._locked: 718 self.unlock() 719 self._file.close() # Sync has been done by self.flush() above. 720 721 def _lookup(self, key=None): 722 """Return (start, stop) or raise KeyError.""" 723 if self._toc is None: 724 self._generate_toc() 725 if key is not None: 726 try: 727 return self._toc[key] 728 except KeyError: 729 raise KeyError('No message with key: %s' % key) 730 731 def _append_message(self, message): 732 """Append message to mailbox and return (start, stop) offsets.""" 733 self._file.seek(0, 2) 734 before = self._file.tell() 735 if len(self._toc) == 0 and not self._pending: 736 # This is the first message, and the _pre_mailbox_hook 737 # hasn't yet been called. If self._pending is True, 738 # messages have been removed, so _pre_mailbox_hook must 739 # have been called already. 740 self._pre_mailbox_hook(self._file) 741 try: 742 self._pre_message_hook(self._file) 743 offsets = self._install_message(message) 744 self._post_message_hook(self._file) 745 except BaseException: 746 self._file.truncate(before) 747 raise 748 self._file.flush() 749 self._file_length = self._file.tell() # Record current length of mailbox 750 return offsets 751 752 753 754 class _mboxMMDF(_singlefileMailbox): 755 """An mbox or MMDF mailbox.""" 756 757 _mangle_from_ = True 758 759 def get_message(self, key): 760 """Return a Message representation or raise a KeyError.""" 761 start, stop = self._lookup(key) 762 self._file.seek(start) 763 from_line = self._file.readline().replace(os.linesep, '') 764 string = self._file.read(stop - self._file.tell()) 765 msg = self._message_factory(string.replace(os.linesep, '\n')) 766 msg.set_from(from_line[5:]) 767 return msg 768 769 def get_string(self, key, from_=False): 770 """Return a string representation or raise a KeyError.""" 771 start, stop = self._lookup(key) 772 self._file.seek(start) 773 if not from_: 774 self._file.readline() 775 string = self._file.read(stop - self._file.tell()) 776 return string.replace(os.linesep, '\n') 777 778 def get_file(self, key, from_=False): 779 """Return a file-like representation or raise a KeyError.""" 780 start, stop = self._lookup(key) 781 self._file.seek(start) 782 if not from_: 783 self._file.readline() 784 return _PartialFile(self._file, self._file.tell(), stop) 785 786 def _install_message(self, message): 787 """Format a message and blindly write to self._file.""" 788 from_line = None 789 if isinstance(message, str) and message.startswith('From '): 790 newline = message.find('\n') 791 if newline != -1: 792 from_line = message[:newline] 793 message = message[newline + 1:] 794 else: 795 from_line = message 796 message = '' 797 elif isinstance(message, _mboxMMDFMessage): 798 from_line = 'From ' + message.get_from() 799 elif isinstance(message, email.message.Message): 800 from_line = message.get_unixfrom() # May be None. 801 if from_line is None: 802 from_line = 'From MAILER-DAEMON %s' % time.asctime(time.gmtime()) 803 start = self._file.tell() 804 self._file.write(from_line + os.linesep) 805 self._dump_message(message, self._file, self._mangle_from_) 806 stop = self._file.tell() 807 return (start, stop) 808 809 810 class mbox(_mboxMMDF): 811 """A classic mbox mailbox.""" 812 813 _mangle_from_ = True 814 815 # All messages must end in a newline character, and 816 # _post_message_hooks outputs an empty line between messages. 817 _append_newline = True 818 819 def __init__(self, path, factory=None, create=True): 820 """Initialize an mbox mailbox.""" 821 self._message_factory = mboxMessage 822 _mboxMMDF.__init__(self, path, factory, create) 823 824 def _post_message_hook(self, f): 825 """Called after writing each message to file f.""" 826 f.write(os.linesep) 827 828 def _generate_toc(self): 829 """Generate key-to-(start, stop) table of contents.""" 830 starts, stops = [], [] 831 last_was_empty = False 832 self._file.seek(0) 833 while True: 834 line_pos = self._file.tell() 835 line = self._file.readline() 836 if line.startswith('From '): 837 if len(stops) < len(starts): 838 if last_was_empty: 839 stops.append(line_pos - len(os.linesep)) 840 else: 841 # The last line before the "From " line wasn't 842 # blank, but we consider it a start of a 843 # message anyway. 844 stops.append(line_pos) 845 starts.append(line_pos) 846 last_was_empty = False 847 elif not line: 848 if last_was_empty: 849 stops.append(line_pos - len(os.linesep)) 850 else: 851 stops.append(line_pos) 852 break 853 elif line == os.linesep: 854 last_was_empty = True 855 else: 856 last_was_empty = False 857 self._toc = dict(enumerate(zip(starts, stops))) 858 self._next_key = len(self._toc) 859 self._file_length = self._file.tell() 860 861 862 class MMDF(_mboxMMDF): 863 """An MMDF mailbox.""" 864 865 def __init__(self, path, factory=None, create=True): 866 """Initialize an MMDF mailbox.""" 867 self._message_factory = MMDFMessage 868 _mboxMMDF.__init__(self, path, factory, create) 869 870 def _pre_message_hook(self, f): 871 """Called before writing each message to file f.""" 872 f.write('\001\001\001\001' + os.linesep) 873 874 def _post_message_hook(self, f): 875 """Called after writing each message to file f.""" 876 f.write(os.linesep + '\001\001\001\001' + os.linesep) 877 878 def _generate_toc(self): 879 """Generate key-to-(start, stop) table of contents.""" 880 starts, stops = [], [] 881 self._file.seek(0) 882 next_pos = 0 883 while True: 884 line_pos = next_pos 885 line = self._file.readline() 886 next_pos = self._file.tell() 887 if line.startswith('\001\001\001\001' + os.linesep): 888 starts.append(next_pos) 889 while True: 890 line_pos = next_pos 891 line = self._file.readline() 892 next_pos = self._file.tell() 893 if line == '\001\001\001\001' + os.linesep: 894 stops.append(line_pos - len(os.linesep)) 895 break 896 elif line == '': 897 stops.append(line_pos) 898 break 899 elif line == '': 900 break 901 self._toc = dict(enumerate(zip(starts, stops))) 902 self._next_key = len(self._toc) 903 self._file.seek(0, 2) 904 self._file_length = self._file.tell() 905 906 907 class MH(Mailbox): 908 """An MH mailbox.""" 909 910 def __init__(self, path, factory=None, create=True): 911 """Initialize an MH instance.""" 912 Mailbox.__init__(self, path, factory, create) 913 if not os.path.exists(self._path): 914 if create: 915 os.mkdir(self._path, 0700) 916 os.close(os.open(os.path.join(self._path, '.mh_sequences'), 917 os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0600)) 918 else: 919 raise NoSuchMailboxError(self._path) 920 self._locked = False 921 922 def add(self, message): 923 """Add message and return assigned key.""" 924 keys = self.keys() 925 if len(keys) == 0: 926 new_key = 1 927 else: 928 new_key = max(keys) + 1 929 new_path = os.path.join(self._path, str(new_key)) 930 f = _create_carefully(new_path) 931 closed = False 932 try: 933 if self._locked: 934 _lock_file(f) 935 try: 936 try: 937 self._dump_message(message, f) 938 except BaseException: 939 # Unlock and close so it can be deleted on Windows 940 if self._locked: 941 _unlock_file(f) 942 _sync_close(f) 943 closed = True 944 os.remove(new_path) 945 raise 946 if isinstance(message, MHMessage): 947 self._dump_sequences(message, new_key) 948 finally: 949 if self._locked: 950 _unlock_file(f) 951 finally: 952 if not closed: 953 _sync_close(f) 954 return new_key 955 956 def remove(self, key): 957 """Remove the keyed message; raise KeyError if it doesn't exist.""" 958 path = os.path.join(self._path, str(key)) 959 try: 960 f = open(path, 'rb+') 961 except IOError, e: 962 if e.errno == errno.ENOENT: 963 raise KeyError('No message with key: %s' % key) 964 else: 965 raise 966 else: 967 f.close() 968 os.remove(path) 969 970 def __setitem__(self, key, message): 971 """Replace the keyed message; raise KeyError if it doesn't exist.""" 972 path = os.path.join(self._path, str(key)) 973 try: 974 f = open(path, 'rb+') 975 except IOError, e: 976 if e.errno == errno.ENOENT: 977 raise KeyError('No message with key: %s' % key) 978 else: 979 raise 980 try: 981 if self._locked: 982 _lock_file(f) 983 try: 984 os.close(os.open(path, os.O_WRONLY | os.O_TRUNC)) 985 self._dump_message(message, f) 986 if isinstance(message, MHMessage): 987 self._dump_sequences(message, key) 988 finally: 989 if self._locked: 990 _unlock_file(f) 991 finally: 992 _sync_close(f) 993 994 def get_message(self, key): 995 """Return a Message representation or raise a KeyError.""" 996 try: 997 if self._locked: 998 f = open(os.path.join(self._path, str(key)), 'r+') 999 else: 1000 f = open(os.path.join(self._path, str(key)), 'r') 1001 except IOError, e: 1002 if e.errno == errno.ENOENT: 1003 raise KeyError('No message with key: %s' % key) 1004 else: 1005 raise 1006 try: 1007 if self._locked: 1008 _lock_file(f) 1009 try: 1010 msg = MHMessage(f) 1011 finally: 1012 if self._locked: 1013 _unlock_file(f) 1014 finally: 1015 f.close() 1016 for name, key_list in self.get_sequences().iteritems(): 1017 if key in key_list: 1018 msg.add_sequence(name) 1019 return msg 1020 1021 def get_string(self, key): 1022 """Return a string representation or raise a KeyError.""" 1023 try: 1024 if self._locked: 1025 f = open(os.path.join(self._path, str(key)), 'r+') 1026 else: 1027 f = open(os.path.join(self._path, str(key)), 'r') 1028 except IOError, e: 1029 if e.errno == errno.ENOENT: 1030 raise KeyError('No message with key: %s' % key) 1031 else: 1032 raise 1033 try: 1034 if self._locked: 1035 _lock_file(f) 1036 try: 1037 return f.read() 1038 finally: 1039 if self._locked: 1040 _unlock_file(f) 1041 finally: 1042 f.close() 1043 1044 def get_file(self, key): 1045 """Return a file-like representation or raise a KeyError.""" 1046 try: 1047 f = open(os.path.join(self._path, str(key)), 'rb') 1048 except IOError, e: 1049 if e.errno == errno.ENOENT: 1050 raise KeyError('No message with key: %s' % key) 1051 else: 1052 raise 1053 return _ProxyFile(f) 1054 1055 def iterkeys(self): 1056 """Return an iterator over keys.""" 1057 return iter(sorted(int(entry) for entry in os.listdir(self._path) 1058 if entry.isdigit())) 1059 1060 def has_key(self, key): 1061 """Return True if the keyed message exists, False otherwise.""" 1062 return os.path.exists(os.path.join(self._path, str(key))) 1063 1064 def __len__(self): 1065 """Return a count of messages in the mailbox.""" 1066 return len(list(self.iterkeys())) 1067 1068 def lock(self): 1069 """Lock the mailbox.""" 1070 if not self._locked: 1071 self._file = open(os.path.join(self._path, '.mh_sequences'), 'rb+') 1072 _lock_file(self._file) 1073 self._locked = True 1074 1075 def unlock(self): 1076 """Unlock the mailbox if it is locked.""" 1077 if self._locked: 1078 _unlock_file(self._file) 1079 _sync_close(self._file) 1080 del self._file 1081 self._locked = False 1082 1083 def flush(self): 1084 """Write any pending changes to the disk.""" 1085 return 1086 1087 def close(self): 1088 """Flush and close the mailbox.""" 1089 if self._locked: 1090 self.unlock() 1091 1092 def list_folders(self): 1093 """Return a list of folder names.""" 1094 result = [] 1095 for entry in os.listdir(self._path): 1096 if os.path.isdir(os.path.join(self._path, entry)): 1097 result.append(entry) 1098 return result 1099 1100 def get_folder(self, folder): 1101 """Return an MH instance for the named folder.""" 1102 return MH(os.path.join(self._path, folder), 1103 factory=self._factory, create=False) 1104 1105 def add_folder(self, folder): 1106 """Create a folder and return an MH instance representing it.""" 1107 return MH(os.path.join(self._path, folder), 1108 factory=self._factory) 1109 1110 def remove_folder(self, folder): 1111 """Delete the named folder, which must be empty.""" 1112 path = os.path.join(self._path, folder) 1113 entries = os.listdir(path) 1114 if entries == ['.mh_sequences']: 1115 os.remove(os.path.join(path, '.mh_sequences')) 1116 elif entries == []: 1117 pass 1118 else: 1119 raise NotEmptyError('Folder not empty: %s' % self._path) 1120 os.rmdir(path) 1121 1122 def get_sequences(self): 1123 """Return a name-to-key-list dictionary to define each sequence.""" 1124 results = {} 1125 f = open(os.path.join(self._path, '.mh_sequences'), 'r') 1126 try: 1127 all_keys = set(self.keys()) 1128 for line in f: 1129 try: 1130 name, contents = line.split(':') 1131 keys = set() 1132 for spec in contents.split(): 1133 if spec.isdigit(): 1134 keys.add(int(spec)) 1135 else: 1136 start, stop = (int(x) for x in spec.split('-')) 1137 keys.update(range(start, stop + 1)) 1138 results[name] = [key for key in sorted(keys) \ 1139 if key in all_keys] 1140 if len(results[name]) == 0: 1141 del results[name] 1142 except ValueError: 1143 raise FormatError('Invalid sequence specification: %s' % 1144 line.rstrip()) 1145 finally: 1146 f.close() 1147 return results 1148 1149 def set_sequences(self, sequences): 1150 """Set sequences using the given name-to-key-list dictionary.""" 1151 f = open(os.path.join(self._path, '.mh_sequences'), 'r+') 1152 try: 1153 os.close(os.open(f.name, os.O_WRONLY | os.O_TRUNC)) 1154 for name, keys in sequences.iteritems(): 1155 if len(keys) == 0: 1156 continue 1157 f.write('%s:' % name) 1158 prev = None 1159 completing = False 1160 for key in sorted(set(keys)): 1161 if key - 1 == prev: 1162 if not completing: 1163 completing = True 1164 f.write('-') 1165 elif completing: 1166 completing = False 1167 f.write('%s %s' % (prev, key)) 1168 else: 1169 f.write(' %s' % key) 1170 prev = key 1171 if completing: 1172 f.write(str(prev) + '\n') 1173 else: 1174 f.write('\n') 1175 finally: 1176 _sync_close(f) 1177 1178 def pack(self): 1179 """Re-name messages to eliminate numbering gaps. Invalidates keys.""" 1180 sequences = self.get_sequences() 1181 prev = 0 1182 changes = [] 1183 for key in self.iterkeys(): 1184 if key - 1 != prev: 1185 changes.append((key, prev + 1)) 1186 if hasattr(os, 'link'): 1187 os.link(os.path.join(self._path, str(key)), 1188 os.path.join(self._path, str(prev + 1))) 1189 os.unlink(os.path.join(self._path, str(key))) 1190 else: 1191 os.rename(os.path.join(self._path, str(key)), 1192 os.path.join(self._path, str(prev + 1))) 1193 prev += 1 1194 self._next_key = prev + 1 1195 if len(changes) == 0: 1196 return 1197 for name, key_list in sequences.items(): 1198 for old, new in changes: 1199 if old in key_list: 1200 key_list[key_list.index(old)] = new 1201 self.set_sequences(sequences) 1202 1203 def _dump_sequences(self, message, key): 1204 """Inspect a new MHMessage and update sequences appropriately.""" 1205 pending_sequences = message.get_sequences() 1206 all_sequences = self.get_sequences() 1207 for name, key_list in all_sequences.iteritems(): 1208 if name in pending_sequences: 1209 key_list.append(key) 1210 elif key in key_list: 1211 del key_list[key_list.index(key)] 1212 for sequence in pending_sequences: 1213 if sequence not in all_sequences: 1214 all_sequences[sequence] = [key] 1215 self.set_sequences(all_sequences) 1216 1217 1218 class Babyl(_singlefileMailbox): 1219 """An Rmail-style Babyl mailbox.""" 1220 1221 _special_labels = frozenset(('unseen', 'deleted', 'filed', 'answered', 1222 'forwarded', 'edited', 'resent')) 1223 1224 def __init__(self, path, factory=None, create=True): 1225 """Initialize a Babyl mailbox.""" 1226 _singlefileMailbox.__init__(self, path, factory, create) 1227 self._labels = {} 1228 1229 def add(self, message): 1230 """Add message and return assigned key.""" 1231 key = _singlefileMailbox.add(self, message) 1232 if isinstance(message, BabylMessage): 1233 self._labels[key] = message.get_labels() 1234 return key 1235 1236 def remove(self, key): 1237 """Remove the keyed message; raise KeyError if it doesn't exist.""" 1238 _singlefileMailbox.remove(self, key) 1239 if key in self._labels: 1240 del self._labels[key] 1241 1242 def __setitem__(self, key, message): 1243 """Replace the keyed message; raise KeyError if it doesn't exist.""" 1244 _singlefileMailbox.__setitem__(self, key, message) 1245 if isinstance(message, BabylMessage): 1246 self._labels[key] = message.get_labels() 1247 1248 def get_message(self, key): 1249 """Return a Message representation or raise a KeyError.""" 1250 start, stop = self._lookup(key) 1251 self._file.seek(start) 1252 self._file.readline() # Skip '1,' line specifying labels. 1253 original_headers = StringIO.StringIO() 1254 while True: 1255 line = self._file.readline() 1256 if line == '*** EOOH ***' + os.linesep or line == '': 1257 break 1258 original_headers.write(line.replace(os.linesep, '\n')) 1259 visible_headers = StringIO.StringIO() 1260 while True: 1261 line = self._file.readline() 1262 if line == os.linesep or line == '': 1263 break 1264 visible_headers.write(line.replace(os.linesep, '\n')) 1265 body = self._file.read(stop - self._file.tell()).replace(os.linesep, 1266 '\n') 1267 msg = BabylMessage(original_headers.getvalue() + body) 1268 msg.set_visible(visible_headers.getvalue()) 1269 if key in self._labels: 1270 msg.set_labels(self._labels[key]) 1271 return msg 1272 1273 def get_string(self, key): 1274 """Return a string representation or raise a KeyError.""" 1275 start, stop = self._lookup(key) 1276 self._file.seek(start) 1277 self._file.readline() # Skip '1,' line specifying labels. 1278 original_headers = StringIO.StringIO() 1279 while True: 1280 line = self._file.readline() 1281 if line == '*** EOOH ***' + os.linesep or line == '': 1282 break 1283 original_headers.write(line.replace(os.linesep, '\n')) 1284 while True: 1285 line = self._file.readline() 1286 if line == os.linesep or line == '': 1287 break 1288 return original_headers.getvalue() + \ 1289 self._file.read(stop - self._file.tell()).replace(os.linesep, 1290 '\n') 1291 1292 def get_file(self, key): 1293 """Return a file-like representation or raise a KeyError.""" 1294 return StringIO.StringIO(self.get_string(key).replace('\n', 1295 os.linesep)) 1296 1297 def get_labels(self): 1298 """Return a list of user-defined labels in the mailbox.""" 1299 self._lookup() 1300 labels = set() 1301 for label_list in self._labels.values(): 1302 labels.update(label_list) 1303 labels.difference_update(self._special_labels) 1304 return list(labels) 1305 1306 def _generate_toc(self): 1307 """Generate key-to-(start, stop) table of contents.""" 1308 starts, stops = [], [] 1309 self._file.seek(0) 1310 next_pos = 0 1311 label_lists = [] 1312 while True: 1313 line_pos = next_pos 1314 line = self._file.readline() 1315 next_pos = self._file.tell() 1316 if line == '\037\014' + os.linesep: 1317 if len(stops) < len(starts): 1318 stops.append(line_pos - len(os.linesep)) 1319 starts.append(next_pos) 1320 labels = [label.strip() for label 1321 in self._file.readline()[1:].split(',') 1322 if label.strip() != ''] 1323 label_lists.append(labels) 1324 elif line == '\037' or line == '\037' + os.linesep: 1325 if len(stops) < len(starts): 1326 stops.append(line_pos - len(os.linesep)) 1327 elif line == '': 1328 stops.append(line_pos - len(os.linesep)) 1329 break 1330 self._toc = dict(enumerate(zip(starts, stops))) 1331 self._labels = dict(enumerate(label_lists)) 1332 self._next_key = len(self._toc) 1333 self._file.seek(0, 2) 1334 self._file_length = self._file.tell() 1335 1336 def _pre_mailbox_hook(self, f): 1337 """Called before writing the mailbox to file f.""" 1338 f.write('BABYL OPTIONS:%sVersion: 5%sLabels:%s%s\037' % 1339 (os.linesep, os.linesep, ','.join(self.get_labels()), 1340 os.linesep)) 1341 1342 def _pre_message_hook(self, f): 1343 """Called before writing each message to file f.""" 1344 f.write('\014' + os.linesep) 1345 1346 def _post_message_hook(self, f): 1347 """Called after writing each message to file f.""" 1348 f.write(os.linesep + '\037') 1349 1350 def _install_message(self, message): 1351 """Write message contents and return (start, stop).""" 1352 start = self._file.tell() 1353 if isinstance(message, BabylMessage): 1354 special_labels = [] 1355 labels = [] 1356 for label in message.get_labels(): 1357 if label in self._special_labels: 1358 special_labels.append(label) 1359 else: 1360 labels.append(label) 1361 self._file.write('1') 1362 for label in special_labels: 1363 self._file.write(', ' + label) 1364 self._file.write(',,') 1365 for label in labels: 1366 self._file.write(' ' + label + ',') 1367 self._file.write(os.linesep) 1368 else: 1369 self._file.write('1,,' + os.linesep) 1370 if isinstance(message, email.message.Message): 1371 orig_buffer = StringIO.StringIO() 1372 orig_generator = email.generator.Generator(orig_buffer, False, 0) 1373 orig_generator.flatten(message) 1374 orig_buffer.seek(0) 1375 while True: 1376 line = orig_buffer.readline() 1377 self._file.write(line.replace('\n', os.linesep)) 1378 if line == '\n' or line == '': 1379 break 1380 self._file.write('*** EOOH ***' + os.linesep) 1381 if isinstance(message, BabylMessage): 1382 vis_buffer = StringIO.StringIO() 1383 vis_generator = email.generator.Generator(vis_buffer, False, 0) 1384 vis_generator.flatten(message.get_visible()) 1385 while True: 1386 line = vis_buffer.readline() 1387 self._file.write(line.replace('\n', os.linesep)) 1388 if line == '\n' or line == '': 1389 break 1390 else: 1391 orig_buffer.seek(0) 1392 while True: 1393 line = orig_buffer.readline() 1394 self._file.write(line.replace('\n', os.linesep)) 1395 if line == '\n' or line == '': 1396 break 1397 while True: 1398 buffer = orig_buffer.read(4096) # Buffer size is arbitrary. 1399 if buffer == '': 1400 break 1401 self._file.write(buffer.replace('\n', os.linesep)) 1402 elif isinstance(message, str): 1403 body_start = message.find('\n\n') + 2 1404 if body_start - 2 != -1: 1405 self._file.write(message[:body_start].replace('\n', 1406 os.linesep)) 1407 self._file.write('*** EOOH ***' + os.linesep) 1408 self._file.write(message[:body_start].replace('\n', 1409 os.linesep)) 1410 self._file.write(message[body_start:].replace('\n', 1411 os.linesep)) 1412 else: 1413 self._file.write('*** EOOH ***' + os.linesep + os.linesep) 1414 self._file.write(message.replace('\n', os.linesep)) 1415 elif hasattr(message, 'readline'): 1416 original_pos = message.tell() 1417 first_pass = True 1418 while True: 1419 line = message.readline() 1420 self._file.write(line.replace('\n', os.linesep)) 1421 if line == '\n' or line == '': 1422 if first_pass: 1423 first_pass = False 1424 self._file.write('*** EOOH ***' + os.linesep) 1425 message.seek(original_pos) 1426 else: 1427 break 1428 while True: 1429 buffer = message.read(4096) # Buffer size is arbitrary. 1430 if buffer == '': 1431 break 1432 self._file.write(buffer.replace('\n', os.linesep)) 1433 else: 1434 raise TypeError('Invalid message type: %s' % type(message)) 1435 stop = self._file.tell() 1436 return (start, stop) 1437 1438 1439 class Message(email.message.Message): 1440 """Message with mailbox-format-specific properties.""" 1441 1442 def __init__(self, message=None): 1443 """Initialize a Message instance.""" 1444 if isinstance(message, email.message.Message): 1445 self._become_message(copy.deepcopy(message)) 1446 if isinstance(message, Message): 1447 message._explain_to(self) 1448 elif isinstance(message, str): 1449 self._become_message(email.message_from_string(message)) 1450 elif hasattr(message, "read"): 1451 self._become_message(email.message_from_file(message)) 1452 elif message is None: 1453 email.message.Message.__init__(self) 1454 else: 1455 raise TypeError('Invalid message type: %s' % type(message)) 1456 1457 def _become_message(self, message): 1458 """Assume the non-format-specific state of message.""" 1459 for name in ('_headers', '_unixfrom', '_payload', '_charset', 1460 'preamble', 'epilogue', 'defects', '_default_type'): 1461 self.__dict__[name] = message.__dict__[name] 1462 1463 def _explain_to(self, message): 1464 """Copy format-specific state to message insofar as possible.""" 1465 if isinstance(message, Message): 1466 return # There's nothing format-specific to explain. 1467 else: 1468 raise TypeError('Cannot convert to specified type') 1469 1470 1471 class MaildirMessage(Message): 1472 """Message with Maildir-specific properties.""" 1473 1474 def __init__(self, message=None): 1475 """Initialize a MaildirMessage instance.""" 1476 self._subdir = 'new' 1477 self._info = '' 1478 self._date = time.time() 1479 Message.__init__(self, message) 1480 1481 def get_subdir(self): 1482 """Return 'new' or 'cur'.""" 1483 return self._subdir 1484 1485 def set_subdir(self, subdir): 1486 """Set subdir to 'new' or 'cur'.""" 1487 if subdir == 'new' or subdir == 'cur': 1488 self._subdir = subdir 1489 else: 1490 raise ValueError("subdir must be 'new' or 'cur': %s" % subdir) 1491 1492 def get_flags(self): 1493 """Return as a string the flags that are set.""" 1494 if self._info.startswith('2,'): 1495 return self._info[2:] 1496 else: 1497 return '' 1498 1499 def set_flags(self, flags): 1500 """Set the given flags and unset all others.""" 1501 self._info = '2,' + ''.join(sorted(flags)) 1502 1503 def add_flag(self, flag): 1504 """Set the given flag(s) without changing others.""" 1505 self.set_flags(''.join(set(self.get_flags()) | set(flag))) 1506 1507 def remove_flag(self, flag): 1508 """Unset the given string flag(s) without changing others.""" 1509 if self.get_flags() != '': 1510 self.set_flags(''.join(set(self.get_flags()) - set(flag))) 1511 1512 def get_date(self): 1513 """Return delivery date of message, in seconds since the epoch.""" 1514 return self._date 1515 1516 def set_date(self, date): 1517 """Set delivery date of message, in seconds since the epoch.""" 1518 try: 1519 self._date = float(date) 1520 except ValueError: 1521 raise TypeError("can't convert to float: %s" % date) 1522 1523 def get_info(self): 1524 """Get the message's "info" as a string.""" 1525 return self._info 1526 1527 def set_info(self, info): 1528 """Set the message's "info" string.""" 1529 if isinstance(info, str): 1530 self._info = info 1531 else: 1532 raise TypeError('info must be a string: %s' % type(info)) 1533 1534 def _explain_to(self, message): 1535 """Copy Maildir-specific state to message insofar as possible.""" 1536 if isinstance(message, MaildirMessage): 1537 message.set_flags(self.get_flags()) 1538 message.set_subdir(self.get_subdir()) 1539 message.set_date(self.get_date()) 1540 elif isinstance(message, _mboxMMDFMessage): 1541 flags = set(self.get_flags()) 1542 if 'S' in flags: 1543 message.add_flag('R') 1544 if self.get_subdir() == 'cur': 1545 message.add_flag('O') 1546 if 'T' in flags: 1547 message.add_flag('D') 1548 if 'F' in flags: 1549 message.add_flag('F') 1550 if 'R' in flags: 1551 message.add_flag('A') 1552 message.set_from('MAILER-DAEMON', time.gmtime(self.get_date())) 1553 elif isinstance(message, MHMessage): 1554 flags = set(self.get_flags()) 1555 if 'S' not in flags: 1556 message.add_sequence('unseen') 1557 if 'R' in flags: 1558 message.add_sequence('replied') 1559 if 'F' in flags: 1560 message.add_sequence('flagged') 1561 elif isinstance(message, BabylMessage): 1562 flags = set(self.get_flags()) 1563 if 'S' not in flags: 1564 message.add_label('unseen') 1565 if 'T' in flags: 1566 message.add_label('deleted') 1567 if 'R' in flags: 1568 message.add_label('answered') 1569 if 'P' in flags: 1570 message.add_label('forwarded') 1571 elif isinstance(message, Message): 1572 pass 1573 else: 1574 raise TypeError('Cannot convert to specified type: %s' % 1575 type(message)) 1576 1577 1578 class _mboxMMDFMessage(Message): 1579 """Message with mbox- or MMDF-specific properties.""" 1580 1581 def __init__(self, message=None): 1582 """Initialize an mboxMMDFMessage instance.""" 1583 self.set_from('MAILER-DAEMON', True) 1584 if isinstance(message, email.message.Message): 1585 unixfrom = message.get_unixfrom() 1586 if unixfrom is not None and unixfrom.startswith('From '): 1587 self.set_from(unixfrom[5:]) 1588 Message.__init__(self, message) 1589 1590 def get_from(self): 1591 """Return contents of "From " line.""" 1592 return self._from 1593 1594 def set_from(self, from_, time_=None): 1595 """Set "From " line, formatting and appending time_ if specified.""" 1596 if time_ is not None: 1597 if time_ is True: 1598 time_ = time.gmtime() 1599 from_ += ' ' + time.asctime(time_) 1600 self._from = from_ 1601 1602 def get_flags(self): 1603 """Return as a string the flags that are set.""" 1604 return self.get('Status', '') + self.get('X-Status', '') 1605 1606 def set_flags(self, flags): 1607 """Set the given flags and unset all others.""" 1608 flags = set(flags) 1609 status_flags, xstatus_flags = '', '' 1610 for flag in ('R', 'O'): 1611 if flag in flags: 1612 status_flags += flag 1613 flags.remove(flag) 1614 for flag in ('D', 'F', 'A'): 1615 if flag in flags: 1616 xstatus_flags += flag 1617 flags.remove(flag) 1618 xstatus_flags += ''.join(sorted(flags)) 1619 try: 1620 self.replace_header('Status', status_flags) 1621 except KeyError: 1622 self.add_header('Status', status_flags) 1623 try: 1624 self.replace_header('X-Status', xstatus_flags) 1625 except KeyError: 1626 self.add_header('X-Status', xstatus_flags) 1627 1628 def add_flag(self, flag): 1629 """Set the given flag(s) without changing others.""" 1630 self.set_flags(''.join(set(self.get_flags()) | set(flag))) 1631 1632 def remove_flag(self, flag): 1633 """Unset the given string flag(s) without changing others.""" 1634 if 'Status' in self or 'X-Status' in self: 1635 self.set_flags(''.join(set(self.get_flags()) - set(flag))) 1636 1637 def _explain_to(self, message): 1638 """Copy mbox- or MMDF-specific state to message insofar as possible.""" 1639 if isinstance(message, MaildirMessage): 1640 flags = set(self.get_flags()) 1641 if 'O' in flags: 1642 message.set_subdir('cur') 1643 if 'F' in flags: 1644 message.add_flag('F') 1645 if 'A' in flags: 1646 message.add_flag('R') 1647 if 'R' in flags: 1648 message.add_flag('S') 1649 if 'D' in flags: 1650 message.add_flag('T') 1651 del message['status'] 1652 del message['x-status'] 1653 maybe_date = ' '.join(self.get_from().split()[-5:]) 1654 try: 1655 message.set_date(calendar.timegm(time.strptime(maybe_date, 1656 '%a %b %d %H:%M:%S %Y'))) 1657 except (ValueError, OverflowError): 1658 pass 1659 elif isinstance(message, _mboxMMDFMessage): 1660 message.set_flags(self.get_flags()) 1661 message.set_from(self.get_from()) 1662 elif isinstance(message, MHMessage): 1663 flags = set(self.get_flags()) 1664 if 'R' not in flags: 1665 message.add_sequence('unseen') 1666 if 'A' in flags: 1667 message.add_sequence('replied') 1668 if 'F' in flags: 1669 message.add_sequence('flagged') 1670 del message['status'] 1671 del message['x-status'] 1672 elif isinstance(message, BabylMessage): 1673 flags = set(self.get_flags()) 1674 if 'R' not in flags: 1675 message.add_label('unseen') 1676 if 'D' in flags: 1677 message.add_label('deleted') 1678 if 'A' in flags: 1679 message.add_label('answered') 1680 del message['status'] 1681 del message['x-status'] 1682 elif isinstance(message, Message): 1683 pass 1684 else: 1685 raise TypeError('Cannot convert to specified type: %s' % 1686 type(message)) 1687 1688 1689 class mboxMessage(_mboxMMDFMessage): 1690 """Message with mbox-specific properties.""" 1691 1692 1693 class MHMessage(Message): 1694 """Message with MH-specific properties.""" 1695 1696 def __init__(self, message=None): 1697 """Initialize an MHMessage instance.""" 1698 self._sequences = [] 1699 Message.__init__(self, message) 1700 1701 def get_sequences(self): 1702 """Return a list of sequences that include the message.""" 1703 return self._sequences[:] 1704 1705 def set_sequences(self, sequences): 1706 """Set the list of sequences that include the message.""" 1707 self._sequences = list(sequences) 1708 1709 def add_sequence(self, sequence): 1710 """Add sequence to list of sequences including the message.""" 1711 if isinstance(sequence, str): 1712 if not sequence in self._sequences: 1713 self._sequences.append(sequence) 1714 else: 1715 raise TypeError('sequence must be a string: %s' % type(sequence)) 1716 1717 def remove_sequence(self, sequence): 1718 """Remove sequence from the list of sequences including the message.""" 1719 try: 1720 self._sequences.remove(sequence) 1721 except ValueError: 1722 pass 1723 1724 def _explain_to(self, message): 1725 """Copy MH-specific state to message insofar as possible.""" 1726 if isinstance(message, MaildirMessage): 1727 sequences = set(self.get_sequences()) 1728 if 'unseen' in sequences: 1729 message.set_subdir('cur') 1730 else: 1731 message.set_subdir('cur') 1732 message.add_flag('S') 1733 if 'flagged' in sequences: 1734 message.add_flag('F') 1735 if 'replied' in sequences: 1736 message.add_flag('R') 1737 elif isinstance(message, _mboxMMDFMessage): 1738 sequences = set(self.get_sequences()) 1739 if 'unseen' not in sequences: 1740 message.add_flag('RO') 1741 else: 1742 message.add_flag('O') 1743 if 'flagged' in sequences: 1744 message.add_flag('F') 1745 if 'replied' in sequences: 1746 message.add_flag('A') 1747 elif isinstance(message, MHMessage): 1748 for sequence in self.get_sequences(): 1749 message.add_sequence(sequence) 1750 elif isinstance(message, BabylMessage): 1751 sequences = set(self.get_sequences()) 1752 if 'unseen' in sequences: 1753 message.add_label('unseen') 1754 if 'replied' in sequences: 1755 message.add_label('answered') 1756 elif isinstance(message, Message): 1757 pass 1758 else: 1759 raise TypeError('Cannot convert to specified type: %s' % 1760 type(message)) 1761 1762 1763 class BabylMessage(Message): 1764 """Message with Babyl-specific properties.""" 1765 1766 def __init__(self, message=None): 1767 """Initialize an BabylMessage instance.""" 1768 self._labels = [] 1769 self._visible = Message() 1770 Message.__init__(self, message) 1771 1772 def get_labels(self): 1773 """Return a list of labels on the message.""" 1774 return self._labels[:] 1775 1776 def set_labels(self, labels): 1777 """Set the list of labels on the message.""" 1778 self._labels = list(labels) 1779 1780 def add_label(self, label): 1781 """Add label to list of labels on the message.""" 1782 if isinstance(label, str): 1783 if label not in self._labels: 1784 self._labels.append(label) 1785 else: 1786 raise TypeError('label must be a string: %s' % type(label)) 1787 1788 def remove_label(self, label): 1789 """Remove label from the list of labels on the message.""" 1790 try: 1791 self._labels.remove(label) 1792 except ValueError: 1793 pass 1794 1795 def get_visible(self): 1796 """Return a Message representation of visible headers.""" 1797 return Message(self._visible) 1798 1799 def set_visible(self, visible): 1800 """Set the Message representation of visible headers.""" 1801 self._visible = Message(visible) 1802 1803 def update_visible(self): 1804 """Update and/or sensibly generate a set of visible headers.""" 1805 for header in self._visible.keys(): 1806 if header in self: 1807 self._visible.replace_header(header, self[header]) 1808 else: 1809 del self._visible[header] 1810 for header in ('Date', 'From', 'Reply-To', 'To', 'CC', 'Subject'): 1811 if header in self and header not in self._visible: 1812 self._visible[header] = self[header] 1813 1814 def _explain_to(self, message): 1815 """Copy Babyl-specific state to message insofar as possible.""" 1816 if isinstance(message, MaildirMessage): 1817 labels = set(self.get_labels()) 1818 if 'unseen' in labels: 1819 message.set_subdir('cur') 1820 else: 1821 message.set_subdir('cur') 1822 message.add_flag('S') 1823 if 'forwarded' in labels or 'resent' in labels: 1824 message.add_flag('P') 1825 if 'answered' in labels: 1826 message.add_flag('R') 1827 if 'deleted' in labels: 1828 message.add_flag('T') 1829 elif isinstance(message, _mboxMMDFMessage): 1830 labels = set(self.get_labels()) 1831 if 'unseen' not in labels: 1832 message.add_flag('RO') 1833 else: 1834 message.add_flag('O') 1835 if 'deleted' in labels: 1836 message.add_flag('D') 1837 if 'answered' in labels: 1838 message.add_flag('A') 1839 elif isinstance(message, MHMessage): 1840 labels = set(self.get_labels()) 1841 if 'unseen' in labels: 1842 message.add_sequence('unseen') 1843 if 'answered' in labels: 1844 message.add_sequence('replied') 1845 elif isinstance(message, BabylMessage): 1846 message.set_visible(self.get_visible()) 1847 for label in self.get_labels(): 1848 message.add_label(label) 1849 elif isinstance(message, Message): 1850 pass 1851 else: 1852 raise TypeError('Cannot convert to specified type: %s' % 1853 type(message)) 1854 1855 1856 class MMDFMessage(_mboxMMDFMessage): 1857 """Message with MMDF-specific properties.""" 1858 1859 1860 class _ProxyFile: 1861 """A read-only wrapper of a file.""" 1862 1863 def __init__(self, f, pos=None): 1864 """Initialize a _ProxyFile.""" 1865 self._file = f 1866 if pos is None: 1867 self._pos = f.tell() 1868 else: 1869 self._pos = pos 1870 1871 def read(self, size=None): 1872 """Read bytes.""" 1873 return self._read(size, self._file.read) 1874 1875 def readline(self, size=None): 1876 """Read a line.""" 1877 return self._read(size, self._file.readline) 1878 1879 def readlines(self, sizehint=None): 1880 """Read multiple lines.""" 1881 result = [] 1882 for line in self: 1883 result.append(line) 1884 if sizehint is not None: 1885 sizehint -= len(line) 1886 if sizehint <= 0: 1887 break 1888 return result 1889 1890 def __iter__(self): 1891 """Iterate over lines.""" 1892 return iter(self.readline, "") 1893 1894 def tell(self): 1895 """Return the position.""" 1896 return self._pos 1897 1898 def seek(self, offset, whence=0): 1899 """Change position.""" 1900 if whence == 1: 1901 self._file.seek(self._pos) 1902 self._file.seek(offset, whence) 1903 self._pos = self._file.tell() 1904 1905 def close(self): 1906 """Close the file.""" 1907 if hasattr(self, '_file'): 1908 if hasattr(self._file, 'close'): 1909 self._file.close() 1910 del self._file 1911 1912 def _read(self, size, read_method): 1913 """Read size bytes using read_method.""" 1914 if size is None: 1915 size = -1 1916 self._file.seek(self._pos) 1917 result = read_method(size) 1918 self._pos = self._file.tell() 1919 return result 1920 1921 1922 class _PartialFile(_ProxyFile): 1923 """A read-only wrapper of part of a file.""" 1924 1925 def __init__(self, f, start=None, stop=None): 1926 """Initialize a _PartialFile.""" 1927 _ProxyFile.__init__(self, f, start) 1928 self._start = start 1929 self._stop = stop 1930 1931 def tell(self): 1932 """Return the position with respect to start.""" 1933 return _ProxyFile.tell(self) - self._start 1934 1935 def seek(self, offset, whence=0): 1936 """Change position, possibly with respect to start or stop.""" 1937 if whence == 0: 1938 self._pos = self._start 1939 whence = 1 1940 elif whence == 2: 1941 self._pos = self._stop 1942 whence = 1 1943 _ProxyFile.seek(self, offset, whence) 1944 1945 def _read(self, size, read_method): 1946 """Read size bytes using read_method, honoring start and stop.""" 1947 remaining = self._stop - self._pos 1948 if remaining <= 0: 1949 return '' 1950 if size is None or size < 0 or size > remaining: 1951 size = remaining 1952 return _ProxyFile._read(self, size, read_method) 1953 1954 def close(self): 1955 # do *not* close the underlying file object for partial files, 1956 # since it's global to the mailbox object 1957 if hasattr(self, '_file'): 1958 del self._file 1959 1960 1961 def _lock_file(f, dotlock=True): 1962 """Lock file f using lockf and dot locking.""" 1963 dotlock_done = False 1964 try: 1965 if fcntl: 1966 try: 1967 fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB) 1968 except IOError, e: 1969 if e.errno in (errno.EAGAIN, errno.EACCES, errno.EROFS): 1970 raise ExternalClashError('lockf: lock unavailable: %s' % 1971 f.name) 1972 else: 1973 raise 1974 if dotlock: 1975 try: 1976 pre_lock = _create_temporary(f.name + '.lock') 1977 pre_lock.close() 1978 except IOError, e: 1979 if e.errno in (errno.EACCES, errno.EROFS): 1980 return # Without write access, just skip dotlocking. 1981 else: 1982 raise 1983 try: 1984 if hasattr(os, 'link'): 1985 os.link(pre_lock.name, f.name + '.lock') 1986 dotlock_done = True 1987 os.unlink(pre_lock.name) 1988 else: 1989 os.rename(pre_lock.name, f.name + '.lock') 1990 dotlock_done = True 1991 except OSError, e: 1992 if e.errno == errno.EEXIST or \ 1993 (os.name == 'os2' and e.errno == errno.EACCES): 1994 os.remove(pre_lock.name) 1995 raise ExternalClashError('dot lock unavailable: %s' % 1996 f.name) 1997 else: 1998 raise 1999 except: 2000 if fcntl: 2001 fcntl.lockf(f, fcntl.LOCK_UN) 2002 if dotlock_done: 2003 os.remove(f.name + '.lock') 2004 raise 2005 2006 def _unlock_file(f): 2007 """Unlock file f using lockf and dot locking.""" 2008 if fcntl: 2009 fcntl.lockf(f, fcntl.LOCK_UN) 2010 if os.path.exists(f.name + '.lock'): 2011 os.remove(f.name + '.lock') 2012 2013 def _create_carefully(path): 2014 """Create a file if it doesn't exist and open for reading and writing.""" 2015 fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR, 0666) 2016 try: 2017 return open(path, 'rb+') 2018 finally: 2019 os.close(fd) 2020 2021 def _create_temporary(path): 2022 """Create a temp file based on path and open for reading and writing.""" 2023 return _create_carefully('%s.%s.%s.%s' % (path, int(time.time()), 2024 socket.gethostname(), 2025 os.getpid())) 2026 2027 def _sync_flush(f): 2028 """Ensure changes to file f are physically on disk.""" 2029 f.flush() 2030 if hasattr(os, 'fsync'): 2031 os.fsync(f.fileno()) 2032 2033 def _sync_close(f): 2034 """Close file f, ensuring all changes are physically on disk.""" 2035 _sync_flush(f) 2036 f.close() 2037 2038 ## Start: classes from the original module (for backward compatibility). 2039 2040 # Note that the Maildir class, whose name is unchanged, itself offers a next() 2041 # method for backward compatibility. 2042 2043 class _Mailbox: 2044 2045 def __init__(self, fp, factory=rfc822.Message): 2046 self.fp = fp 2047 self.seekp = 0 2048 self.factory = factory 2049 2050 def __iter__(self): 2051 return iter(self.next, None) 2052 2053 def next(self): 2054 while 1: 2055 self.fp.seek(self.seekp) 2056 try: 2057 self._search_start() 2058 except EOFError: 2059 self.seekp = self.fp.tell() 2060 return None 2061 start = self.fp.tell() 2062 self._search_end() 2063 self.seekp = stop = self.fp.tell() 2064 if start != stop: 2065 break 2066 return self.factory(_PartialFile(self.fp, start, stop)) 2067 2068 # Recommended to use PortableUnixMailbox instead! 2069 class UnixMailbox(_Mailbox): 2070 2071 def _search_start(self): 2072 while 1: 2073 pos = self.fp.tell() 2074 line = self.fp.readline() 2075 if not line: 2076 raise EOFError 2077 if line[:5] == 'From ' and self._isrealfromline(line): 2078 self.fp.seek(pos) 2079 return 2080 2081 def _search_end(self): 2082 self.fp.readline() # Throw away header line 2083 while 1: 2084 pos = self.fp.tell() 2085 line = self.fp.readline() 2086 if not line: 2087 return 2088 if line[:5] == 'From ' and self._isrealfromline(line): 2089 self.fp.seek(pos) 2090 return 2091 2092 # An overridable mechanism to test for From-line-ness. You can either 2093 # specify a different regular expression or define a whole new 2094 # _isrealfromline() method. Note that this only gets called for lines 2095 # starting with the 5 characters "From ". 2096 # 2097 # BAW: According to 2098 #http://home.netscape.com/eng/mozilla/2.0/relnotes/demo/content-length.html 2099 # the only portable, reliable way to find message delimiters in a BSD (i.e 2100 # Unix mailbox) style folder is to search for "\n\nFrom .*\n", or at the 2101 # beginning of the file, "^From .*\n". While _fromlinepattern below seems 2102 # like a good idea, in practice, there are too many variations for more 2103 # strict parsing of the line to be completely accurate. 2104 # 2105 # _strict_isrealfromline() is the old version which tries to do stricter 2106 # parsing of the From_ line. _portable_isrealfromline() simply returns 2107 # true, since it's never called if the line doesn't already start with 2108 # "From ". 2109 # 2110 # This algorithm, and the way it interacts with _search_start() and 2111 # _search_end() may not be completely correct, because it doesn't check 2112 # that the two characters preceding "From " are \n\n or the beginning of 2113 # the file. Fixing this would require a more extensive rewrite than is 2114 # necessary. For convenience, we've added a PortableUnixMailbox class 2115 # which does no checking of the format of the 'From' line. 2116 2117 _fromlinepattern = (r"From \s*[^\s]+\s+\w\w\w\s+\w\w\w\s+\d?\d\s+" 2118 r"\d?\d:\d\d(:\d\d)?(\s+[^\s]+)?\s+\d\d\d\d\s*" 2119 r"[^\s]*\s*" 2120 "$") 2121 _regexp = None 2122 2123 def _strict_isrealfromline(self, line): 2124 if not self._regexp: 2125 import re 2126 self._regexp = re.compile(self._fromlinepattern) 2127 return self._regexp.match(line) 2128 2129 def _portable_isrealfromline(self, line): 2130 return True 2131 2132 _isrealfromline = _strict_isrealfromline 2133 2134 2135 class PortableUnixMailbox(UnixMailbox): 2136 _isrealfromline = UnixMailbox._portable_isrealfromline 2137 2138 2139 class MmdfMailbox(_Mailbox): 2140 2141 def _search_start(self): 2142 while 1: 2143 line = self.fp.readline() 2144 if not line: 2145 raise EOFError 2146 if line[:5] == '\001\001\001\001\n': 2147 return 2148 2149 def _search_end(self): 2150 while 1: 2151 pos = self.fp.tell() 2152 line = self.fp.readline() 2153 if not line: 2154 return 2155 if line == '\001\001\001\001\n': 2156 self.fp.seek(pos) 2157 return 2158 2159 2160 class MHMailbox: 2161 2162 def __init__(self, dirname, factory=rfc822.Message): 2163 import re 2164 pat = re.compile('^[1-9][0-9]*$') 2165 self.dirname = dirname 2166 # the three following lines could be combined into: 2167 # list = map(long, filter(pat.match, os.listdir(self.dirname))) 2168 list = os.listdir(self.dirname) 2169 list = filter(pat.match, list) 2170 list = map(long, list) 2171 list.sort() 2172 # This only works in Python 1.6 or later; 2173 # before that str() added 'L': 2174 self.boxes = map(str, list) 2175 self.boxes.reverse() 2176 self.factory = factory 2177 2178 def __iter__(self): 2179 return iter(self.next, None) 2180 2181 def next(self): 2182 if not self.boxes: 2183 return None 2184 fn = self.boxes.pop() 2185 fp = open(os.path.join(self.dirname, fn)) 2186 msg = self.factory(fp) 2187 try: 2188 msg._mh_msgno = fn 2189 except (AttributeError, TypeError): 2190 pass 2191 return msg 2192 2193 2194 class BabylMailbox(_Mailbox): 2195 2196 def _search_start(self): 2197 while 1: 2198 line = self.fp.readline() 2199 if not line: 2200 raise EOFError 2201 if line == '*** EOOH ***\n': 2202 return 2203 2204 def _search_end(self): 2205 while 1: 2206 pos = self.fp.tell() 2207 line = self.fp.readline() 2208 if not line: 2209 return 2210 if line == '\037\014\n' or line == '\037': 2211 self.fp.seek(pos) 2212 return 2213 2214 ## End: classes from the original module (for backward compatibility). 2215 2216 2217 class Error(Exception): 2218 """Raised for module-specific errors.""" 2219 2220 class NoSuchMailboxError(Error): 2221 """The specified mailbox does not exist and won't be created.""" 2222 2223 class NotEmptyError(Error): 2224 """The specified mailbox is not empty and deletion was requested.""" 2225 2226 class ExternalClashError(Error): 2227 """Another process caused an action to fail.""" 2228 2229 class FormatError(Error): 2230 """A file appears to have an invalid format.""" 2231