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