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