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
20 import email
.generator
24 if sys
.platform
== 'os2emx':
25 # OS/2 EMX fcntl() not adequate
31 __all__
= [ 'Mailbox', 'Maildir', 'mbox', 'MH', 'Babyl', 'MMDF',
32 'Message', 'MaildirMessage', 'mboxMessage', 'MHMessage',
33 'BabylMessage', 'MMDFMessage', 'UnixMailbox',
34 'PortableUnixMailbox', 'MmdfMailbox', 'MHMailbox', 'BabylMailbox' ]
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
):
55 def discard(self
, key
):
56 """If the keyed message exists, remove it."""
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."""
69 return self
.__getitem
__(key
)
73 def __getitem__(self
, key
):
74 """Return the keyed message; raise KeyError if it doesn't exist."""
76 return self
.get_message(key
)
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')
93 """Return an iterator over keys."""
94 raise NotImplementedError('Method must be implemented by subclass')
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():
110 return self
.itervalues()
113 """Return a list of messages. Memory intensive."""
114 return list(self
.itervalues())
117 """Return an iterator over (key, message) tuples."""
118 for key
in self
.iterkeys():
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
)
137 """Return a count of messages in the mailbox."""
138 raise NotImplementedError('Method must be implemented by subclass')
141 """Delete all messages."""
142 for key
in self
.iterkeys():
145 def pop(self
, key
, default
=None):
146 """Delete the keyed message and return it, or default."""
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.
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'):
170 for key
, message
in source
:
176 raise KeyError('No message with key(s)')
179 """Write any pending changes to the disk."""
180 raise NotImplementedError('Method must be implemented by subclass')
183 """Lock the mailbox."""
184 raise NotImplementedError('Method must be implemented by subclass')
187 """Unlock the mailbox if it is locked."""
188 raise NotImplementedError('Method must be implemented by subclass')
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)
204 target
.write(buffer.read().replace('\n', os
.linesep
))
205 elif isinstance(message
, str):
207 message
= message
.replace('\nFrom ', '\n>From ')
208 message
= message
.replace('\n', os
.linesep
)
209 target
.write(message
)
210 elif hasattr(message
, 'read'):
212 line
= message
.readline()
215 if mangle_from_
and line
.startswith('From '):
216 line
= '>From ' + line
[5:]
217 line
= line
.replace('\n', os
.linesep
)
220 raise TypeError('Invalid message type: %s' % type(message
))
223 class Maildir(Mailbox
):
224 """A qmail-style Maildir mailbox."""
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
):
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)
238 raise NoSuchMailboxError(self
._path
)
241 def add(self
, message
):
242 """Add message and return assigned key."""
243 tmp_file
= self
._create
_tmp
()
245 self
._dump
_message
(message
, tmp_file
)
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
:
256 uniq
= os
.path
.basename(tmp_file
.name
).split(self
.colon
)[0]
257 dest
= os
.path
.join(self
._path
, subdir
, uniq
+ suffix
)
259 if hasattr(os
, 'link'):
260 os
.link(tmp_file
.name
, dest
)
261 os
.remove(tmp_file
.name
)
263 os
.rename(tmp_file
.name
, dest
)
265 os
.remove(tmp_file
.name
)
266 if e
.errno
== errno
.EEXIST
:
267 raise ExternalClashError('Name clash with existing message: %s'
271 if isinstance(message
, MaildirMessage
):
272 os
.utime(dest
, (os
.path
.getatime(dest
), message
.get_date()))
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.
287 if e
.errno
!= errno
.ENOENT
:
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
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]
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
),
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')
318 msg
= MaildirMessage(f
)
321 subdir
, name
= os
.path
.split(subpath
)
322 msg
.set_subdir(subdir
)
323 if self
.colon
in name
:
324 msg
.set_info(name
.split(self
.colon
)[-1])
325 msg
.set_date(os
.path
.getmtime(os
.path
.join(self
._path
, subpath
)))
328 def get_string(self
, key
):
329 """Return a string representation or raise a KeyError."""
330 f
= open(os
.path
.join(self
._path
, self
._lookup
(key
)), 'r')
336 def get_file(self
, key
):
337 """Return a file-like representation or raise a KeyError."""
338 f
= open(os
.path
.join(self
._path
, self
._lookup
(key
)), 'rb')
342 """Return an iterator over keys."""
344 for key
in self
._toc
:
351 def has_key(self
, key
):
352 """Return True if the keyed message exists, False otherwise."""
354 return key
in self
._toc
357 """Return a count of messages in the mailbox."""
359 return len(self
._toc
)
362 """Write any pending changes to disk."""
363 return # Maildir changes are always written immediately.
366 """Lock the mailbox."""
370 """Unlock the mailbox if it is locked."""
374 """Flush and close the mailbox."""
377 def list_folders(self
):
378 """Return a list of folder names."""
380 for entry
in os
.listdir(self
._path
):
381 if len(entry
) > 1 and entry
[0] == '.' and \
382 os
.path
.isdir(os
.path
.join(self
._path
, entry
)):
383 result
.append(entry
[1:])
386 def get_folder(self
, folder
):
387 """Return a Maildir instance for the named folder."""
388 return Maildir(os
.path
.join(self
._path
, '.' + folder
),
389 factory
=self
._factory
,
392 def add_folder(self
, folder
):
393 """Create a folder and return a Maildir instance representing it."""
394 path
= os
.path
.join(self
._path
, '.' + folder
)
395 result
= Maildir(path
, factory
=self
._factory
)
396 maildirfolder_path
= os
.path
.join(path
, 'maildirfolder')
397 if not os
.path
.exists(maildirfolder_path
):
398 os
.close(os
.open(maildirfolder_path
, os
.O_CREAT | os
.O_WRONLY
))
401 def remove_folder(self
, folder
):
402 """Delete the named folder, which must be empty."""
403 path
= os
.path
.join(self
._path
, '.' + folder
)
404 for entry
in os
.listdir(os
.path
.join(path
, 'new')) + \
405 os
.listdir(os
.path
.join(path
, 'cur')):
406 if len(entry
) < 1 or entry
[0] != '.':
407 raise NotEmptyError('Folder contains message(s): %s' % folder
)
408 for entry
in os
.listdir(path
):
409 if entry
!= 'new' and entry
!= 'cur' and entry
!= 'tmp' and \
410 os
.path
.isdir(os
.path
.join(path
, entry
)):
411 raise NotEmptyError("Folder contains subdirectory '%s': %s" %
413 for root
, dirs
, files
in os
.walk(path
, topdown
=False):
415 os
.remove(os
.path
.join(root
, entry
))
417 os
.rmdir(os
.path
.join(root
, entry
))
421 """Delete old files in "tmp"."""
423 for entry
in os
.listdir(os
.path
.join(self
._path
, 'tmp')):
424 path
= os
.path
.join(self
._path
, 'tmp', entry
)
425 if now
- os
.path
.getatime(path
) > 129600: # 60 * 60 * 36
428 _count
= 1 # This is used to generate unique file names.
430 def _create_tmp(self
):
431 """Create a file in the tmp subdirectory and open and return it."""
433 hostname
= socket
.gethostname()
435 hostname
= hostname
.replace('/', r
'\057')
437 hostname
= hostname
.replace(':', r
'\072')
438 uniq
= "%s.M%sP%sQ%s.%s" % (int(now
), int(now
% 1 * 1e6
), os
.getpid(),
439 Maildir
._count
, hostname
)
440 path
= os
.path
.join(self
._path
, 'tmp', uniq
)
444 if e
.errno
== errno
.ENOENT
:
447 return _create_carefully(path
)
449 if e
.errno
!= errno
.EEXIST
:
454 # Fall through to here if stat succeeded or open raised EEXIST.
455 raise ExternalClashError('Name clash prevented file creation: %s' %
459 """Update table of contents mapping."""
461 for subdir
in ('new', 'cur'):
462 subdir_path
= os
.path
.join(self
._path
, subdir
)
463 for entry
in os
.listdir(subdir_path
):
464 p
= os
.path
.join(subdir_path
, entry
)
467 uniq
= entry
.split(self
.colon
)[0]
468 self
._toc
[uniq
] = os
.path
.join(subdir
, entry
)
470 def _lookup(self
, key
):
471 """Use TOC to return subpath for given key, or raise a KeyError."""
473 if os
.path
.exists(os
.path
.join(self
._path
, self
._toc
[key
])):
474 return self
._toc
[key
]
479 return self
._toc
[key
]
481 raise KeyError('No message with key: %s' % key
)
483 # This method is for backward compatibility only.
485 """Return the next message in a one-time iteration."""
486 if not hasattr(self
, '_onetime_keys'):
487 self
._onetime
_keys
= self
.iterkeys()
490 return self
[self
._onetime
_keys
.next()]
491 except StopIteration:
497 class _singlefileMailbox(Mailbox
):
498 """A single-file mailbox."""
500 def __init__(self
, path
, factory
=None, create
=True):
501 """Initialize a single-file mailbox."""
502 Mailbox
.__init
__(self
, path
, factory
, create
)
504 f
= open(self
._path
, 'rb+')
506 if e
.errno
== errno
.ENOENT
:
508 f
= open(self
._path
, 'wb+')
510 raise NoSuchMailboxError(self
._path
)
511 elif e
.errno
== errno
.EACCES
:
512 f
= open(self
._path
, 'rb')
518 self
._pending
= False # No changes require rewriting the file.
520 self
._file
_length
= None # Used to record mailbox size
522 def add(self
, message
):
523 """Add message and return assigned key."""
525 self
._toc
[self
._next
_key
] = self
._append
_message
(message
)
528 return self
._next
_key
- 1
530 def remove(self
, key
):
531 """Remove the keyed message; raise KeyError if it doesn't exist."""
536 def __setitem__(self
, key
, message
):
537 """Replace the keyed message; raise KeyError if it doesn't exist."""
539 self
._toc
[key
] = self
._append
_message
(message
)
543 """Return an iterator over keys."""
545 for key
in self
._toc
.keys():
548 def has_key(self
, key
):
549 """Return True if the keyed message exists, False otherwise."""
551 return key
in self
._toc
554 """Return a count of messages in the mailbox."""
556 return len(self
._toc
)
559 """Lock the mailbox."""
561 _lock_file(self
._file
)
565 """Unlock the mailbox if it is locked."""
567 _unlock_file(self
._file
)
571 """Write any pending changes to disk."""
572 if not self
._pending
:
575 # In order to be writing anything out at all, self._toc must
576 # already have been generated (and presumably has been modified
577 # by adding or deleting an item).
578 assert self
._toc
is not None
580 # Check length of self._file; if it's changed, some other process
581 # has modified the mailbox since we scanned it.
582 self
._file
.seek(0, 2)
583 cur_len
= self
._file
.tell()
584 if cur_len
!= self
._file
_length
:
585 raise ExternalClashError('Size of mailbox file changed '
586 '(expected %i, found %i)' %
587 (self
._file
_length
, cur_len
))
589 new_file
= _create_temporary(self
._path
)
592 self
._pre
_mailbox
_hook
(new_file
)
593 for key
in sorted(self
._toc
.keys()):
594 start
, stop
= self
._toc
[key
]
595 self
._file
.seek(start
)
596 self
._pre
_message
_hook
(new_file
)
597 new_start
= new_file
.tell()
599 buffer = self
._file
.read(min(4096,
600 stop
- self
._file
.tell()))
603 new_file
.write(buffer)
604 new_toc
[key
] = (new_start
, new_file
.tell())
605 self
._post
_message
_hook
(new_file
)
608 os
.remove(new_file
.name
)
610 _sync_close(new_file
)
611 # self._file is about to get replaced, so no need to sync.
614 os
.rename(new_file
.name
, self
._path
)
616 if e
.errno
== errno
.EEXIST
or \
617 (os
.name
== 'os2' and e
.errno
== errno
.EACCES
):
618 os
.remove(self
._path
)
619 os
.rename(new_file
.name
, self
._path
)
622 self
._file
= open(self
._path
, 'rb+')
624 self
._pending
= False
626 _lock_file(self
._file
, dotlock
=False)
628 def _pre_mailbox_hook(self
, f
):
629 """Called before writing the mailbox to file f."""
632 def _pre_message_hook(self
, f
):
633 """Called before writing each message to file f."""
636 def _post_message_hook(self
, f
):
637 """Called after writing each message to file f."""
641 """Flush and close the mailbox."""
645 self
._file
.close() # Sync has been done by self.flush() above.
647 def _lookup(self
, key
=None):
648 """Return (start, stop) or raise KeyError."""
649 if self
._toc
is None:
653 return self
._toc
[key
]
655 raise KeyError('No message with key: %s' % key
)
657 def _append_message(self
, message
):
658 """Append message to mailbox and return (start, stop) offsets."""
659 self
._file
.seek(0, 2)
660 self
._pre
_message
_hook
(self
._file
)
661 offsets
= self
._install
_message
(message
)
662 self
._post
_message
_hook
(self
._file
)
664 self
._file
_length
= self
._file
.tell() # Record current length of mailbox
669 class _mboxMMDF(_singlefileMailbox
):
670 """An mbox or MMDF mailbox."""
674 def get_message(self
, key
):
675 """Return a Message representation or raise a KeyError."""
676 start
, stop
= self
._lookup
(key
)
677 self
._file
.seek(start
)
678 from_line
= self
._file
.readline().replace(os
.linesep
, '')
679 string
= self
._file
.read(stop
- self
._file
.tell())
680 msg
= self
._message
_factory
(string
.replace(os
.linesep
, '\n'))
681 msg
.set_from(from_line
[5:])
684 def get_string(self
, key
, from_
=False):
685 """Return a string representation or raise a KeyError."""
686 start
, stop
= self
._lookup
(key
)
687 self
._file
.seek(start
)
689 self
._file
.readline()
690 string
= self
._file
.read(stop
- self
._file
.tell())
691 return string
.replace(os
.linesep
, '\n')
693 def get_file(self
, key
, from_
=False):
694 """Return a file-like representation or raise a KeyError."""
695 start
, stop
= self
._lookup
(key
)
696 self
._file
.seek(start
)
698 self
._file
.readline()
699 return _PartialFile(self
._file
, self
._file
.tell(), stop
)
701 def _install_message(self
, message
):
702 """Format a message and blindly write to self._file."""
704 if isinstance(message
, str) and message
.startswith('From '):
705 newline
= message
.find('\n')
707 from_line
= message
[:newline
]
708 message
= message
[newline
+ 1:]
712 elif isinstance(message
, _mboxMMDFMessage
):
713 from_line
= 'From ' + message
.get_from()
714 elif isinstance(message
, email
.message
.Message
):
715 from_line
= message
.get_unixfrom() # May be None.
716 if from_line
is None:
717 from_line
= 'From MAILER-DAEMON %s' % time
.asctime(time
.gmtime())
718 start
= self
._file
.tell()
719 self
._file
.write(from_line
+ os
.linesep
)
720 self
._dump
_message
(message
, self
._file
, self
._mangle
_from
_)
721 stop
= self
._file
.tell()
725 class mbox(_mboxMMDF
):
726 """A classic mbox mailbox."""
730 def __init__(self
, path
, factory
=None, create
=True):
731 """Initialize an mbox mailbox."""
732 self
._message
_factory
= mboxMessage
733 _mboxMMDF
.__init
__(self
, path
, factory
, create
)
735 def _pre_message_hook(self
, f
):
736 """Called before writing each message to file f."""
740 def _generate_toc(self
):
741 """Generate key-to-(start, stop) table of contents."""
742 starts
, stops
= [], []
745 line_pos
= self
._file
.tell()
746 line
= self
._file
.readline()
747 if line
.startswith('From '):
748 if len(stops
) < len(starts
):
749 stops
.append(line_pos
- len(os
.linesep
))
750 starts
.append(line_pos
)
752 stops
.append(line_pos
)
754 self
._toc
= dict(enumerate(zip(starts
, stops
)))
755 self
._next
_key
= len(self
._toc
)
756 self
._file
_length
= self
._file
.tell()
759 class MMDF(_mboxMMDF
):
760 """An MMDF mailbox."""
762 def __init__(self
, path
, factory
=None, create
=True):
763 """Initialize an MMDF mailbox."""
764 self
._message
_factory
= MMDFMessage
765 _mboxMMDF
.__init
__(self
, path
, factory
, create
)
767 def _pre_message_hook(self
, f
):
768 """Called before writing each message to file f."""
769 f
.write('\001\001\001\001' + os
.linesep
)
771 def _post_message_hook(self
, f
):
772 """Called after writing each message to file f."""
773 f
.write(os
.linesep
+ '\001\001\001\001' + os
.linesep
)
775 def _generate_toc(self
):
776 """Generate key-to-(start, stop) table of contents."""
777 starts
, stops
= [], []
782 line
= self
._file
.readline()
783 next_pos
= self
._file
.tell()
784 if line
.startswith('\001\001\001\001' + os
.linesep
):
785 starts
.append(next_pos
)
788 line
= self
._file
.readline()
789 next_pos
= self
._file
.tell()
790 if line
== '\001\001\001\001' + os
.linesep
:
791 stops
.append(line_pos
- len(os
.linesep
))
794 stops
.append(line_pos
)
798 self
._toc
= dict(enumerate(zip(starts
, stops
)))
799 self
._next
_key
= len(self
._toc
)
800 self
._file
.seek(0, 2)
801 self
._file
_length
= self
._file
.tell()
807 def __init__(self
, path
, factory
=None, create
=True):
808 """Initialize an MH instance."""
809 Mailbox
.__init
__(self
, path
, factory
, create
)
810 if not os
.path
.exists(self
._path
):
812 os
.mkdir(self
._path
, 0700)
813 os
.close(os
.open(os
.path
.join(self
._path
, '.mh_sequences'),
814 os
.O_CREAT | os
.O_EXCL | os
.O_WRONLY
, 0600))
816 raise NoSuchMailboxError(self
._path
)
819 def add(self
, message
):
820 """Add message and return assigned key."""
825 new_key
= max(keys
) + 1
826 new_path
= os
.path
.join(self
._path
, str(new_key
))
827 f
= _create_carefully(new_path
)
832 self
._dump
_message
(message
, f
)
833 if isinstance(message
, MHMessage
):
834 self
._dump
_sequences
(message
, new_key
)
842 def remove(self
, key
):
843 """Remove the keyed message; raise KeyError if it doesn't exist."""
844 path
= os
.path
.join(self
._path
, str(key
))
846 f
= open(path
, 'rb+')
848 if e
.errno
== errno
.ENOENT
:
849 raise KeyError('No message with key: %s' % key
)
857 os
.remove(os
.path
.join(self
._path
, str(key
)))
864 def __setitem__(self
, key
, message
):
865 """Replace the keyed message; raise KeyError if it doesn't exist."""
866 path
= os
.path
.join(self
._path
, str(key
))
868 f
= open(path
, 'rb+')
870 if e
.errno
== errno
.ENOENT
:
871 raise KeyError('No message with key: %s' % key
)
878 os
.close(os
.open(path
, os
.O_WRONLY | os
.O_TRUNC
))
879 self
._dump
_message
(message
, f
)
880 if isinstance(message
, MHMessage
):
881 self
._dump
_sequences
(message
, key
)
888 def get_message(self
, key
):
889 """Return a Message representation or raise a KeyError."""
892 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+')
894 f
= open(os
.path
.join(self
._path
, str(key
)), 'r')
896 if e
.errno
== errno
.ENOENT
:
897 raise KeyError('No message with key: %s' % key
)
910 for name
, key_list
in self
.get_sequences():
912 msg
.add_sequence(name
)
915 def get_string(self
, key
):
916 """Return a string representation or raise a KeyError."""
919 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+')
921 f
= open(os
.path
.join(self
._path
, str(key
)), 'r')
923 if e
.errno
== errno
.ENOENT
:
924 raise KeyError('No message with key: %s' % key
)
938 def get_file(self
, key
):
939 """Return a file-like representation or raise a KeyError."""
941 f
= open(os
.path
.join(self
._path
, str(key
)), 'rb')
943 if e
.errno
== errno
.ENOENT
:
944 raise KeyError('No message with key: %s' % key
)
950 """Return an iterator over keys."""
951 return iter(sorted(int(entry
) for entry
in os
.listdir(self
._path
)
954 def has_key(self
, key
):
955 """Return True if the keyed message exists, False otherwise."""
956 return os
.path
.exists(os
.path
.join(self
._path
, str(key
)))
959 """Return a count of messages in the mailbox."""
960 return len(list(self
.iterkeys()))
963 """Lock the mailbox."""
965 self
._file
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'rb+')
966 _lock_file(self
._file
)
970 """Unlock the mailbox if it is locked."""
972 _unlock_file(self
._file
)
973 _sync_close(self
._file
)
978 """Write any pending changes to the disk."""
982 """Flush and close the mailbox."""
986 def list_folders(self
):
987 """Return a list of folder names."""
989 for entry
in os
.listdir(self
._path
):
990 if os
.path
.isdir(os
.path
.join(self
._path
, entry
)):
994 def get_folder(self
, folder
):
995 """Return an MH instance for the named folder."""
996 return MH(os
.path
.join(self
._path
, folder
),
997 factory
=self
._factory
, create
=False)
999 def add_folder(self
, folder
):
1000 """Create a folder and return an MH instance representing it."""
1001 return MH(os
.path
.join(self
._path
, folder
),
1002 factory
=self
._factory
)
1004 def remove_folder(self
, folder
):
1005 """Delete the named folder, which must be empty."""
1006 path
= os
.path
.join(self
._path
, folder
)
1007 entries
= os
.listdir(path
)
1008 if entries
== ['.mh_sequences']:
1009 os
.remove(os
.path
.join(path
, '.mh_sequences'))
1013 raise NotEmptyError('Folder not empty: %s' % self
._path
)
1016 def get_sequences(self
):
1017 """Return a name-to-key-list dictionary to define each sequence."""
1019 f
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'r')
1021 all_keys
= set(self
.keys())
1024 name
, contents
= line
.split(':')
1026 for spec
in contents
.split():
1030 start
, stop
= (int(x
) for x
in spec
.split('-'))
1031 keys
.update(range(start
, stop
+ 1))
1032 results
[name
] = [key
for key
in sorted(keys
) \
1034 if len(results
[name
]) == 0:
1037 raise FormatError('Invalid sequence specification: %s' %
1043 def set_sequences(self
, sequences
):
1044 """Set sequences using the given name-to-key-list dictionary."""
1045 f
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'r+')
1047 os
.close(os
.open(f
.name
, os
.O_WRONLY | os
.O_TRUNC
))
1048 for name
, keys
in sequences
.iteritems():
1051 f
.write('%s:' % name
)
1054 for key
in sorted(set(keys
)):
1061 f
.write('%s %s' % (prev
, key
))
1063 f
.write(' %s' % key
)
1066 f
.write(str(prev
) + '\n')
1073 """Re-name messages to eliminate numbering gaps. Invalidates keys."""
1074 sequences
= self
.get_sequences()
1077 for key
in self
.iterkeys():
1079 changes
.append((key
, prev
+ 1))
1080 if hasattr(os
, 'link'):
1081 os
.link(os
.path
.join(self
._path
, str(key
)),
1082 os
.path
.join(self
._path
, str(prev
+ 1)))
1083 os
.unlink(os
.path
.join(self
._path
, str(key
)))
1085 os
.rename(os
.path
.join(self
._path
, str(key
)),
1086 os
.path
.join(self
._path
, str(prev
+ 1)))
1088 self
._next
_key
= prev
+ 1
1089 if len(changes
) == 0:
1091 for name
, key_list
in sequences
.items():
1092 for old
, new
in changes
:
1094 key_list
[key_list
.index(old
)] = new
1095 self
.set_sequences(sequences
)
1097 def _dump_sequences(self
, message
, key
):
1098 """Inspect a new MHMessage and update sequences appropriately."""
1099 pending_sequences
= message
.get_sequences()
1100 all_sequences
= self
.get_sequences()
1101 for name
, key_list
in all_sequences
.iteritems():
1102 if name
in pending_sequences
:
1103 key_list
.append(key
)
1104 elif key
in key_list
:
1105 del key_list
[key_list
.index(key
)]
1106 for sequence
in pending_sequences
:
1107 if sequence
not in all_sequences
:
1108 all_sequences
[sequence
] = [key
]
1109 self
.set_sequences(all_sequences
)
1112 class Babyl(_singlefileMailbox
):
1113 """An Rmail-style Babyl mailbox."""
1115 _special_labels
= frozenset(('unseen', 'deleted', 'filed', 'answered',
1116 'forwarded', 'edited', 'resent'))
1118 def __init__(self
, path
, factory
=None, create
=True):
1119 """Initialize a Babyl mailbox."""
1120 _singlefileMailbox
.__init
__(self
, path
, factory
, create
)
1123 def add(self
, message
):
1124 """Add message and return assigned key."""
1125 key
= _singlefileMailbox
.add(self
, message
)
1126 if isinstance(message
, BabylMessage
):
1127 self
._labels
[key
] = message
.get_labels()
1130 def remove(self
, key
):
1131 """Remove the keyed message; raise KeyError if it doesn't exist."""
1132 _singlefileMailbox
.remove(self
, key
)
1133 if key
in self
._labels
:
1134 del self
._labels
[key
]
1136 def __setitem__(self
, key
, message
):
1137 """Replace the keyed message; raise KeyError if it doesn't exist."""
1138 _singlefileMailbox
.__setitem
__(self
, key
, message
)
1139 if isinstance(message
, BabylMessage
):
1140 self
._labels
[key
] = message
.get_labels()
1142 def get_message(self
, key
):
1143 """Return a Message representation or raise a KeyError."""
1144 start
, stop
= self
._lookup
(key
)
1145 self
._file
.seek(start
)
1146 self
._file
.readline() # Skip '1,' line specifying labels.
1147 original_headers
= StringIO
.StringIO()
1149 line
= self
._file
.readline()
1150 if line
== '*** EOOH ***' + os
.linesep
or line
== '':
1152 original_headers
.write(line
.replace(os
.linesep
, '\n'))
1153 visible_headers
= StringIO
.StringIO()
1155 line
= self
._file
.readline()
1156 if line
== os
.linesep
or line
== '':
1158 visible_headers
.write(line
.replace(os
.linesep
, '\n'))
1159 body
= self
._file
.read(stop
- self
._file
.tell()).replace(os
.linesep
,
1161 msg
= BabylMessage(original_headers
.getvalue() + body
)
1162 msg
.set_visible(visible_headers
.getvalue())
1163 if key
in self
._labels
:
1164 msg
.set_labels(self
._labels
[key
])
1167 def get_string(self
, key
):
1168 """Return a string representation or raise a KeyError."""
1169 start
, stop
= self
._lookup
(key
)
1170 self
._file
.seek(start
)
1171 self
._file
.readline() # Skip '1,' line specifying labels.
1172 original_headers
= StringIO
.StringIO()
1174 line
= self
._file
.readline()
1175 if line
== '*** EOOH ***' + os
.linesep
or line
== '':
1177 original_headers
.write(line
.replace(os
.linesep
, '\n'))
1179 line
= self
._file
.readline()
1180 if line
== os
.linesep
or line
== '':
1182 return original_headers
.getvalue() + \
1183 self
._file
.read(stop
- self
._file
.tell()).replace(os
.linesep
,
1186 def get_file(self
, key
):
1187 """Return a file-like representation or raise a KeyError."""
1188 return StringIO
.StringIO(self
.get_string(key
).replace('\n',
1191 def get_labels(self
):
1192 """Return a list of user-defined labels in the mailbox."""
1195 for label_list
in self
._labels
.values():
1196 labels
.update(label_list
)
1197 labels
.difference_update(self
._special
_labels
)
1200 def _generate_toc(self
):
1201 """Generate key-to-(start, stop) table of contents."""
1202 starts
, stops
= [], []
1208 line
= self
._file
.readline()
1209 next_pos
= self
._file
.tell()
1210 if line
== '\037\014' + os
.linesep
:
1211 if len(stops
) < len(starts
):
1212 stops
.append(line_pos
- len(os
.linesep
))
1213 starts
.append(next_pos
)
1214 labels
= [label
.strip() for label
1215 in self
._file
.readline()[1:].split(',')
1216 if label
.strip() != '']
1217 label_lists
.append(labels
)
1218 elif line
== '\037' or line
== '\037' + os
.linesep
:
1219 if len(stops
) < len(starts
):
1220 stops
.append(line_pos
- len(os
.linesep
))
1222 stops
.append(line_pos
- len(os
.linesep
))
1224 self
._toc
= dict(enumerate(zip(starts
, stops
)))
1225 self
._labels
= dict(enumerate(label_lists
))
1226 self
._next
_key
= len(self
._toc
)
1227 self
._file
.seek(0, 2)
1228 self
._file
_length
= self
._file
.tell()
1230 def _pre_mailbox_hook(self
, f
):
1231 """Called before writing the mailbox to file f."""
1232 f
.write('BABYL OPTIONS:%sVersion: 5%sLabels:%s%s\037' %
1233 (os
.linesep
, os
.linesep
, ','.join(self
.get_labels()),
1236 def _pre_message_hook(self
, f
):
1237 """Called before writing each message to file f."""
1238 f
.write('\014' + os
.linesep
)
1240 def _post_message_hook(self
, f
):
1241 """Called after writing each message to file f."""
1242 f
.write(os
.linesep
+ '\037')
1244 def _install_message(self
, message
):
1245 """Write message contents and return (start, stop)."""
1246 start
= self
._file
.tell()
1247 if isinstance(message
, BabylMessage
):
1250 for label
in message
.get_labels():
1251 if label
in self
._special
_labels
:
1252 special_labels
.append(label
)
1254 labels
.append(label
)
1255 self
._file
.write('1')
1256 for label
in special_labels
:
1257 self
._file
.write(', ' + label
)
1258 self
._file
.write(',,')
1259 for label
in labels
:
1260 self
._file
.write(' ' + label
+ ',')
1261 self
._file
.write(os
.linesep
)
1263 self
._file
.write('1,,' + os
.linesep
)
1264 if isinstance(message
, email
.message
.Message
):
1265 orig_buffer
= StringIO
.StringIO()
1266 orig_generator
= email
.generator
.Generator(orig_buffer
, False, 0)
1267 orig_generator
.flatten(message
)
1270 line
= orig_buffer
.readline()
1271 self
._file
.write(line
.replace('\n', os
.linesep
))
1272 if line
== '\n' or line
== '':
1274 self
._file
.write('*** EOOH ***' + os
.linesep
)
1275 if isinstance(message
, BabylMessage
):
1276 vis_buffer
= StringIO
.StringIO()
1277 vis_generator
= email
.generator
.Generator(vis_buffer
, False, 0)
1278 vis_generator
.flatten(message
.get_visible())
1280 line
= vis_buffer
.readline()
1281 self
._file
.write(line
.replace('\n', os
.linesep
))
1282 if line
== '\n' or line
== '':
1287 line
= orig_buffer
.readline()
1288 self
._file
.write(line
.replace('\n', os
.linesep
))
1289 if line
== '\n' or line
== '':
1292 buffer = orig_buffer
.read(4096) # Buffer size is arbitrary.
1295 self
._file
.write(buffer.replace('\n', os
.linesep
))
1296 elif isinstance(message
, str):
1297 body_start
= message
.find('\n\n') + 2
1298 if body_start
- 2 != -1:
1299 self
._file
.write(message
[:body_start
].replace('\n',
1301 self
._file
.write('*** EOOH ***' + os
.linesep
)
1302 self
._file
.write(message
[:body_start
].replace('\n',
1304 self
._file
.write(message
[body_start
:].replace('\n',
1307 self
._file
.write('*** EOOH ***' + os
.linesep
+ os
.linesep
)
1308 self
._file
.write(message
.replace('\n', os
.linesep
))
1309 elif hasattr(message
, 'readline'):
1310 original_pos
= message
.tell()
1313 line
= message
.readline()
1314 self
._file
.write(line
.replace('\n', os
.linesep
))
1315 if line
== '\n' or line
== '':
1316 self
._file
.write('*** EOOH ***' + os
.linesep
)
1319 message
.seek(original_pos
)
1323 buffer = message
.read(4096) # Buffer size is arbitrary.
1326 self
._file
.write(buffer.replace('\n', os
.linesep
))
1328 raise TypeError('Invalid message type: %s' % type(message
))
1329 stop
= self
._file
.tell()
1330 return (start
, stop
)
1333 class Message(email
.message
.Message
):
1334 """Message with mailbox-format-specific properties."""
1336 def __init__(self
, message
=None):
1337 """Initialize a Message instance."""
1338 if isinstance(message
, email
.message
.Message
):
1339 self
._become
_message
(copy
.deepcopy(message
))
1340 if isinstance(message
, Message
):
1341 message
._explain
_to
(self
)
1342 elif isinstance(message
, str):
1343 self
._become
_message
(email
.message_from_string(message
))
1344 elif hasattr(message
, "read"):
1345 self
._become
_message
(email
.message_from_file(message
))
1346 elif message
is None:
1347 email
.message
.Message
.__init
__(self
)
1349 raise TypeError('Invalid message type: %s' % type(message
))
1351 def _become_message(self
, message
):
1352 """Assume the non-format-specific state of message."""
1353 for name
in ('_headers', '_unixfrom', '_payload', '_charset',
1354 'preamble', 'epilogue', 'defects', '_default_type'):
1355 self
.__dict
__[name
] = message
.__dict
__[name
]
1357 def _explain_to(self
, message
):
1358 """Copy format-specific state to message insofar as possible."""
1359 if isinstance(message
, Message
):
1360 return # There's nothing format-specific to explain.
1362 raise TypeError('Cannot convert to specified type')
1365 class MaildirMessage(Message
):
1366 """Message with Maildir-specific properties."""
1368 def __init__(self
, message
=None):
1369 """Initialize a MaildirMessage instance."""
1370 self
._subdir
= 'new'
1372 self
._date
= time
.time()
1373 Message
.__init
__(self
, message
)
1375 def get_subdir(self
):
1376 """Return 'new' or 'cur'."""
1379 def set_subdir(self
, subdir
):
1380 """Set subdir to 'new' or 'cur'."""
1381 if subdir
== 'new' or subdir
== 'cur':
1382 self
._subdir
= subdir
1384 raise ValueError("subdir must be 'new' or 'cur': %s" % subdir
)
1386 def get_flags(self
):
1387 """Return as a string the flags that are set."""
1388 if self
._info
.startswith('2,'):
1389 return self
._info
[2:]
1393 def set_flags(self
, flags
):
1394 """Set the given flags and unset all others."""
1395 self
._info
= '2,' + ''.join(sorted(flags
))
1397 def add_flag(self
, flag
):
1398 """Set the given flag(s) without changing others."""
1399 self
.set_flags(''.join(set(self
.get_flags()) |
set(flag
)))
1401 def remove_flag(self
, flag
):
1402 """Unset the given string flag(s) without changing others."""
1403 if self
.get_flags() != '':
1404 self
.set_flags(''.join(set(self
.get_flags()) - set(flag
)))
1407 """Return delivery date of message, in seconds since the epoch."""
1410 def set_date(self
, date
):
1411 """Set delivery date of message, in seconds since the epoch."""
1413 self
._date
= float(date
)
1415 raise TypeError("can't convert to float: %s" % date
)
1418 """Get the message's "info" as a string."""
1421 def set_info(self
, info
):
1422 """Set the message's "info" string."""
1423 if isinstance(info
, str):
1426 raise TypeError('info must be a string: %s' % type(info
))
1428 def _explain_to(self
, message
):
1429 """Copy Maildir-specific state to message insofar as possible."""
1430 if isinstance(message
, MaildirMessage
):
1431 message
.set_flags(self
.get_flags())
1432 message
.set_subdir(self
.get_subdir())
1433 message
.set_date(self
.get_date())
1434 elif isinstance(message
, _mboxMMDFMessage
):
1435 flags
= set(self
.get_flags())
1437 message
.add_flag('R')
1438 if self
.get_subdir() == 'cur':
1439 message
.add_flag('O')
1441 message
.add_flag('D')
1443 message
.add_flag('F')
1445 message
.add_flag('A')
1446 message
.set_from('MAILER-DAEMON', time
.gmtime(self
.get_date()))
1447 elif isinstance(message
, MHMessage
):
1448 flags
= set(self
.get_flags())
1449 if 'S' not in flags
:
1450 message
.add_sequence('unseen')
1452 message
.add_sequence('replied')
1454 message
.add_sequence('flagged')
1455 elif isinstance(message
, BabylMessage
):
1456 flags
= set(self
.get_flags())
1457 if 'S' not in flags
:
1458 message
.add_label('unseen')
1460 message
.add_label('deleted')
1462 message
.add_label('answered')
1464 message
.add_label('forwarded')
1465 elif isinstance(message
, Message
):
1468 raise TypeError('Cannot convert to specified type: %s' %
1472 class _mboxMMDFMessage(Message
):
1473 """Message with mbox- or MMDF-specific properties."""
1475 def __init__(self
, message
=None):
1476 """Initialize an mboxMMDFMessage instance."""
1477 self
.set_from('MAILER-DAEMON', True)
1478 if isinstance(message
, email
.message
.Message
):
1479 unixfrom
= message
.get_unixfrom()
1480 if unixfrom
is not None and unixfrom
.startswith('From '):
1481 self
.set_from(unixfrom
[5:])
1482 Message
.__init
__(self
, message
)
1485 """Return contents of "From " line."""
1488 def set_from(self
, from_
, time_
=None):
1489 """Set "From " line, formatting and appending time_ if specified."""
1490 if time_
is not None:
1492 time_
= time
.gmtime()
1493 from_
+= ' ' + time
.asctime(time_
)
1496 def get_flags(self
):
1497 """Return as a string the flags that are set."""
1498 return self
.get('Status', '') + self
.get('X-Status', '')
1500 def set_flags(self
, flags
):
1501 """Set the given flags and unset all others."""
1503 status_flags
, xstatus_flags
= '', ''
1504 for flag
in ('R', 'O'):
1506 status_flags
+= flag
1508 for flag
in ('D', 'F', 'A'):
1510 xstatus_flags
+= flag
1512 xstatus_flags
+= ''.join(sorted(flags
))
1514 self
.replace_header('Status', status_flags
)
1516 self
.add_header('Status', status_flags
)
1518 self
.replace_header('X-Status', xstatus_flags
)
1520 self
.add_header('X-Status', xstatus_flags
)
1522 def add_flag(self
, flag
):
1523 """Set the given flag(s) without changing others."""
1524 self
.set_flags(''.join(set(self
.get_flags()) |
set(flag
)))
1526 def remove_flag(self
, flag
):
1527 """Unset the given string flag(s) without changing others."""
1528 if 'Status' in self
or 'X-Status' in self
:
1529 self
.set_flags(''.join(set(self
.get_flags()) - set(flag
)))
1531 def _explain_to(self
, message
):
1532 """Copy mbox- or MMDF-specific state to message insofar as possible."""
1533 if isinstance(message
, MaildirMessage
):
1534 flags
= set(self
.get_flags())
1536 message
.set_subdir('cur')
1538 message
.add_flag('F')
1540 message
.add_flag('R')
1542 message
.add_flag('S')
1544 message
.add_flag('T')
1545 del message
['status']
1546 del message
['x-status']
1547 maybe_date
= ' '.join(self
.get_from().split()[-5:])
1549 message
.set_date(calendar
.timegm(time
.strptime(maybe_date
,
1550 '%a %b %d %H:%M:%S %Y')))
1551 except (ValueError, OverflowError):
1553 elif isinstance(message
, _mboxMMDFMessage
):
1554 message
.set_flags(self
.get_flags())
1555 message
.set_from(self
.get_from())
1556 elif isinstance(message
, MHMessage
):
1557 flags
= set(self
.get_flags())
1558 if 'R' not in flags
:
1559 message
.add_sequence('unseen')
1561 message
.add_sequence('replied')
1563 message
.add_sequence('flagged')
1564 del message
['status']
1565 del message
['x-status']
1566 elif isinstance(message
, BabylMessage
):
1567 flags
= set(self
.get_flags())
1568 if 'R' not in flags
:
1569 message
.add_label('unseen')
1571 message
.add_label('deleted')
1573 message
.add_label('answered')
1574 del message
['status']
1575 del message
['x-status']
1576 elif isinstance(message
, Message
):
1579 raise TypeError('Cannot convert to specified type: %s' %
1583 class mboxMessage(_mboxMMDFMessage
):
1584 """Message with mbox-specific properties."""
1587 class MHMessage(Message
):
1588 """Message with MH-specific properties."""
1590 def __init__(self
, message
=None):
1591 """Initialize an MHMessage instance."""
1592 self
._sequences
= []
1593 Message
.__init
__(self
, message
)
1595 def get_sequences(self
):
1596 """Return a list of sequences that include the message."""
1597 return self
._sequences
[:]
1599 def set_sequences(self
, sequences
):
1600 """Set the list of sequences that include the message."""
1601 self
._sequences
= list(sequences
)
1603 def add_sequence(self
, sequence
):
1604 """Add sequence to list of sequences including the message."""
1605 if isinstance(sequence
, str):
1606 if not sequence
in self
._sequences
:
1607 self
._sequences
.append(sequence
)
1609 raise TypeError('sequence must be a string: %s' % type(sequence
))
1611 def remove_sequence(self
, sequence
):
1612 """Remove sequence from the list of sequences including the message."""
1614 self
._sequences
.remove(sequence
)
1618 def _explain_to(self
, message
):
1619 """Copy MH-specific state to message insofar as possible."""
1620 if isinstance(message
, MaildirMessage
):
1621 sequences
= set(self
.get_sequences())
1622 if 'unseen' in sequences
:
1623 message
.set_subdir('cur')
1625 message
.set_subdir('cur')
1626 message
.add_flag('S')
1627 if 'flagged' in sequences
:
1628 message
.add_flag('F')
1629 if 'replied' in sequences
:
1630 message
.add_flag('R')
1631 elif isinstance(message
, _mboxMMDFMessage
):
1632 sequences
= set(self
.get_sequences())
1633 if 'unseen' not in sequences
:
1634 message
.add_flag('RO')
1636 message
.add_flag('O')
1637 if 'flagged' in sequences
:
1638 message
.add_flag('F')
1639 if 'replied' in sequences
:
1640 message
.add_flag('A')
1641 elif isinstance(message
, MHMessage
):
1642 for sequence
in self
.get_sequences():
1643 message
.add_sequence(sequence
)
1644 elif isinstance(message
, BabylMessage
):
1645 sequences
= set(self
.get_sequences())
1646 if 'unseen' in sequences
:
1647 message
.add_label('unseen')
1648 if 'replied' in sequences
:
1649 message
.add_label('answered')
1650 elif isinstance(message
, Message
):
1653 raise TypeError('Cannot convert to specified type: %s' %
1657 class BabylMessage(Message
):
1658 """Message with Babyl-specific properties."""
1660 def __init__(self
, message
=None):
1661 """Initialize an BabylMessage instance."""
1663 self
._visible
= Message()
1664 Message
.__init
__(self
, message
)
1666 def get_labels(self
):
1667 """Return a list of labels on the message."""
1668 return self
._labels
[:]
1670 def set_labels(self
, labels
):
1671 """Set the list of labels on the message."""
1672 self
._labels
= list(labels
)
1674 def add_label(self
, label
):
1675 """Add label to list of labels on the message."""
1676 if isinstance(label
, str):
1677 if label
not in self
._labels
:
1678 self
._labels
.append(label
)
1680 raise TypeError('label must be a string: %s' % type(label
))
1682 def remove_label(self
, label
):
1683 """Remove label from the list of labels on the message."""
1685 self
._labels
.remove(label
)
1689 def get_visible(self
):
1690 """Return a Message representation of visible headers."""
1691 return Message(self
._visible
)
1693 def set_visible(self
, visible
):
1694 """Set the Message representation of visible headers."""
1695 self
._visible
= Message(visible
)
1697 def update_visible(self
):
1698 """Update and/or sensibly generate a set of visible headers."""
1699 for header
in self
._visible
.keys():
1701 self
._visible
.replace_header(header
, self
[header
])
1703 del self
._visible
[header
]
1704 for header
in ('Date', 'From', 'Reply-To', 'To', 'CC', 'Subject'):
1705 if header
in self
and header
not in self
._visible
:
1706 self
._visible
[header
] = self
[header
]
1708 def _explain_to(self
, message
):
1709 """Copy Babyl-specific state to message insofar as possible."""
1710 if isinstance(message
, MaildirMessage
):
1711 labels
= set(self
.get_labels())
1712 if 'unseen' in labels
:
1713 message
.set_subdir('cur')
1715 message
.set_subdir('cur')
1716 message
.add_flag('S')
1717 if 'forwarded' in labels
or 'resent' in labels
:
1718 message
.add_flag('P')
1719 if 'answered' in labels
:
1720 message
.add_flag('R')
1721 if 'deleted' in labels
:
1722 message
.add_flag('T')
1723 elif isinstance(message
, _mboxMMDFMessage
):
1724 labels
= set(self
.get_labels())
1725 if 'unseen' not in labels
:
1726 message
.add_flag('RO')
1728 message
.add_flag('O')
1729 if 'deleted' in labels
:
1730 message
.add_flag('D')
1731 if 'answered' in labels
:
1732 message
.add_flag('A')
1733 elif isinstance(message
, MHMessage
):
1734 labels
= set(self
.get_labels())
1735 if 'unseen' in labels
:
1736 message
.add_sequence('unseen')
1737 if 'answered' in labels
:
1738 message
.add_sequence('replied')
1739 elif isinstance(message
, BabylMessage
):
1740 message
.set_visible(self
.get_visible())
1741 for label
in self
.get_labels():
1742 message
.add_label(label
)
1743 elif isinstance(message
, Message
):
1746 raise TypeError('Cannot convert to specified type: %s' %
1750 class MMDFMessage(_mboxMMDFMessage
):
1751 """Message with MMDF-specific properties."""
1755 """A read-only wrapper of a file."""
1757 def __init__(self
, f
, pos
=None):
1758 """Initialize a _ProxyFile."""
1761 self
._pos
= f
.tell()
1765 def read(self
, size
=None):
1767 return self
._read
(size
, self
._file
.read
)
1769 def readline(self
, size
=None):
1771 return self
._read
(size
, self
._file
.readline
)
1773 def readlines(self
, sizehint
=None):
1774 """Read multiple lines."""
1778 if sizehint
is not None:
1779 sizehint
-= len(line
)
1785 """Iterate over lines."""
1786 return iter(self
.readline
, "")
1789 """Return the position."""
1792 def seek(self
, offset
, whence
=0):
1793 """Change position."""
1795 self
._file
.seek(self
._pos
)
1796 self
._file
.seek(offset
, whence
)
1797 self
._pos
= self
._file
.tell()
1800 """Close the file."""
1803 def _read(self
, size
, read_method
):
1804 """Read size bytes using read_method."""
1807 self
._file
.seek(self
._pos
)
1808 result
= read_method(size
)
1809 self
._pos
= self
._file
.tell()
1813 class _PartialFile(_ProxyFile
):
1814 """A read-only wrapper of part of a file."""
1816 def __init__(self
, f
, start
=None, stop
=None):
1817 """Initialize a _PartialFile."""
1818 _ProxyFile
.__init
__(self
, f
, start
)
1823 """Return the position with respect to start."""
1824 return _ProxyFile
.tell(self
) - self
._start
1826 def seek(self
, offset
, whence
=0):
1827 """Change position, possibly with respect to start or stop."""
1829 self
._pos
= self
._start
1832 self
._pos
= self
._stop
1834 _ProxyFile
.seek(self
, offset
, whence
)
1836 def _read(self
, size
, read_method
):
1837 """Read size bytes using read_method, honoring start and stop."""
1838 remaining
= self
._stop
- self
._pos
1841 if size
is None or size
< 0 or size
> remaining
:
1843 return _ProxyFile
._read
(self
, size
, read_method
)
1846 def _lock_file(f
, dotlock
=True):
1847 """Lock file f using lockf and dot locking."""
1848 dotlock_done
= False
1852 fcntl
.lockf(f
, fcntl
.LOCK_EX | fcntl
.LOCK_NB
)
1854 if e
.errno
in (errno
.EAGAIN
, errno
.EACCES
):
1855 raise ExternalClashError('lockf: lock unavailable: %s' %
1861 pre_lock
= _create_temporary(f
.name
+ '.lock')
1864 if e
.errno
== errno
.EACCES
:
1865 return # Without write access, just skip dotlocking.
1869 if hasattr(os
, 'link'):
1870 os
.link(pre_lock
.name
, f
.name
+ '.lock')
1872 os
.unlink(pre_lock
.name
)
1874 os
.rename(pre_lock
.name
, f
.name
+ '.lock')
1877 if e
.errno
== errno
.EEXIST
or \
1878 (os
.name
== 'os2' and e
.errno
== errno
.EACCES
):
1879 os
.remove(pre_lock
.name
)
1880 raise ExternalClashError('dot lock unavailable: %s' %
1886 fcntl
.lockf(f
, fcntl
.LOCK_UN
)
1888 os
.remove(f
.name
+ '.lock')
1891 def _unlock_file(f
):
1892 """Unlock file f using lockf and dot locking."""
1894 fcntl
.lockf(f
, fcntl
.LOCK_UN
)
1895 if os
.path
.exists(f
.name
+ '.lock'):
1896 os
.remove(f
.name
+ '.lock')
1898 def _create_carefully(path
):
1899 """Create a file if it doesn't exist and open for reading and writing."""
1900 fd
= os
.open(path
, os
.O_CREAT | os
.O_EXCL | os
.O_RDWR
)
1902 return open(path
, 'rb+')
1906 def _create_temporary(path
):
1907 """Create a temp file based on path and open for reading and writing."""
1908 return _create_carefully('%s.%s.%s.%s' % (path
, int(time
.time()),
1909 socket
.gethostname(),
1913 """Ensure changes to file f are physically on disk."""
1915 if hasattr(os
, 'fsync'):
1916 os
.fsync(f
.fileno())
1919 """Close file f, ensuring all changes are physically on disk."""
1923 ## Start: classes from the original module (for backward compatibility).
1925 # Note that the Maildir class, whose name is unchanged, itself offers a next()
1926 # method for backward compatibility.
1930 def __init__(self
, fp
, factory
=rfc822
.Message
):
1933 self
.factory
= factory
1936 return iter(self
.next
, None)
1940 self
.fp
.seek(self
.seekp
)
1942 self
._search
_start
()
1944 self
.seekp
= self
.fp
.tell()
1946 start
= self
.fp
.tell()
1948 self
.seekp
= stop
= self
.fp
.tell()
1951 return self
.factory(_PartialFile(self
.fp
, start
, stop
))
1953 # Recommended to use PortableUnixMailbox instead!
1954 class UnixMailbox(_Mailbox
):
1956 def _search_start(self
):
1958 pos
= self
.fp
.tell()
1959 line
= self
.fp
.readline()
1962 if line
[:5] == 'From ' and self
._isrealfromline
(line
):
1966 def _search_end(self
):
1967 self
.fp
.readline() # Throw away header line
1969 pos
= self
.fp
.tell()
1970 line
= self
.fp
.readline()
1973 if line
[:5] == 'From ' and self
._isrealfromline
(line
):
1977 # An overridable mechanism to test for From-line-ness. You can either
1978 # specify a different regular expression or define a whole new
1979 # _isrealfromline() method. Note that this only gets called for lines
1980 # starting with the 5 characters "From ".
1983 #http://home.netscape.com/eng/mozilla/2.0/relnotes/demo/content-length.html
1984 # the only portable, reliable way to find message delimiters in a BSD (i.e
1985 # Unix mailbox) style folder is to search for "\n\nFrom .*\n", or at the
1986 # beginning of the file, "^From .*\n". While _fromlinepattern below seems
1987 # like a good idea, in practice, there are too many variations for more
1988 # strict parsing of the line to be completely accurate.
1990 # _strict_isrealfromline() is the old version which tries to do stricter
1991 # parsing of the From_ line. _portable_isrealfromline() simply returns
1992 # true, since it's never called if the line doesn't already start with
1995 # This algorithm, and the way it interacts with _search_start() and
1996 # _search_end() may not be completely correct, because it doesn't check
1997 # that the two characters preceding "From " are \n\n or the beginning of
1998 # the file. Fixing this would require a more extensive rewrite than is
1999 # necessary. For convenience, we've added a PortableUnixMailbox class
2000 # which does no checking of the format of the 'From' line.
2002 _fromlinepattern
= (r
"From \s*[^\s]+\s+\w\w\w\s+\w\w\w\s+\d?\d\s+"
2003 r
"\d?\d:\d\d(:\d\d)?(\s+[^\s]+)?\s+\d\d\d\d\s*"
2008 def _strict_isrealfromline(self
, line
):
2009 if not self
._regexp
:
2011 self
._regexp
= re
.compile(self
._fromlinepattern
)
2012 return self
._regexp
.match(line
)
2014 def _portable_isrealfromline(self
, line
):
2017 _isrealfromline
= _strict_isrealfromline
2020 class PortableUnixMailbox(UnixMailbox
):
2021 _isrealfromline
= UnixMailbox
._portable
_isrealfromline
2024 class MmdfMailbox(_Mailbox
):
2026 def _search_start(self
):
2028 line
= self
.fp
.readline()
2031 if line
[:5] == '\001\001\001\001\n':
2034 def _search_end(self
):
2036 pos
= self
.fp
.tell()
2037 line
= self
.fp
.readline()
2040 if line
== '\001\001\001\001\n':
2047 def __init__(self
, dirname
, factory
=rfc822
.Message
):
2049 pat
= re
.compile('^[1-9][0-9]*$')
2050 self
.dirname
= dirname
2051 # the three following lines could be combined into:
2052 # list = map(long, filter(pat.match, os.listdir(self.dirname)))
2053 list = os
.listdir(self
.dirname
)
2054 list = filter(pat
.match
, list)
2055 list = map(long, list)
2057 # This only works in Python 1.6 or later;
2058 # before that str() added 'L':
2059 self
.boxes
= map(str, list)
2060 self
.boxes
.reverse()
2061 self
.factory
= factory
2064 return iter(self
.next
, None)
2069 fn
= self
.boxes
.pop()
2070 fp
= open(os
.path
.join(self
.dirname
, fn
))
2071 msg
= self
.factory(fp
)
2074 except (AttributeError, TypeError):
2079 class BabylMailbox(_Mailbox
):
2081 def _search_start(self
):
2083 line
= self
.fp
.readline()
2086 if line
== '*** EOOH ***\n':
2089 def _search_end(self
):
2091 pos
= self
.fp
.tell()
2092 line
= self
.fp
.readline()
2095 if line
== '\037\014\n' or line
== '\037':
2099 ## End: classes from the original module (for backward compatibility).
2102 class Error(Exception):
2103 """Raised for module-specific errors."""
2105 class NoSuchMailboxError(Error
):
2106 """The specified mailbox does not exist and won't be created."""
2108 class NotEmptyError(Error
):
2109 """The specified mailbox is not empty and deletion was requested."""
2111 class ExternalClashError(Error
):
2112 """Another process caused an action to fail."""
2114 class FormatError(Error
):
2115 """A file appears to have an invalid format."""