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