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')
319 msg
= self
._factory
(f
)
321 msg
= MaildirMessage(f
)
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
)))
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')
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')
345 """Return an iterator over keys."""
347 for key
in self
._toc
:
354 def has_key(self
, key
):
355 """Return True if the keyed message exists, False otherwise."""
357 return key
in self
._toc
360 """Return a count of messages in the mailbox."""
362 return len(self
._toc
)
365 """Write any pending changes to disk."""
366 return # Maildir changes are always written immediately.
369 """Lock the mailbox."""
373 """Unlock the mailbox if it is locked."""
377 """Flush and close the mailbox."""
380 def list_folders(self
):
381 """Return a list of folder names."""
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:])
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
,
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
,
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" %
417 for root
, dirs
, files
in os
.walk(path
, topdown
=False):
419 os
.remove(os
.path
.join(root
, entry
))
421 os
.rmdir(os
.path
.join(root
, entry
))
425 """Delete old files in "tmp"."""
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
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."""
437 hostname
= socket
.gethostname()
439 hostname
= hostname
.replace('/', r
'\057')
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
)
448 if e
.errno
== errno
.ENOENT
:
451 return _create_carefully(path
)
453 if e
.errno
!= errno
.EEXIST
:
458 # Fall through to here if stat succeeded or open raised EEXIST.
459 raise ExternalClashError('Name clash prevented file creation: %s' %
463 """Update table of contents mapping."""
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
)
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."""
477 if os
.path
.exists(os
.path
.join(self
._path
, self
._toc
[key
])):
478 return self
._toc
[key
]
483 return self
._toc
[key
]
485 raise KeyError('No message with key: %s' % key
)
487 # This method is for backward compatibility only.
489 """Return the next message in a one-time iteration."""
490 if not hasattr(self
, '_onetime_keys'):
491 self
._onetime
_keys
= self
.iterkeys()
494 return self
[self
._onetime
_keys
.next()]
495 except StopIteration:
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
)
508 f
= open(self
._path
, 'rb+')
510 if e
.errno
== errno
.ENOENT
:
512 f
= open(self
._path
, 'wb+')
514 raise NoSuchMailboxError(self
._path
)
515 elif e
.errno
== errno
.EACCES
:
516 f
= open(self
._path
, 'rb')
522 self
._pending
= False # No changes require rewriting the file.
524 self
._file
_length
= None # Used to record mailbox size
526 def add(self
, message
):
527 """Add message and return assigned key."""
529 self
._toc
[self
._next
_key
] = self
._append
_message
(message
)
532 return self
._next
_key
- 1
534 def remove(self
, key
):
535 """Remove the keyed message; raise KeyError if it doesn't exist."""
540 def __setitem__(self
, key
, message
):
541 """Replace the keyed message; raise KeyError if it doesn't exist."""
543 self
._toc
[key
] = self
._append
_message
(message
)
547 """Return an iterator over keys."""
549 for key
in self
._toc
.keys():
552 def has_key(self
, key
):
553 """Return True if the keyed message exists, False otherwise."""
555 return key
in self
._toc
558 """Return a count of messages in the mailbox."""
560 return len(self
._toc
)
563 """Lock the mailbox."""
565 _lock_file(self
._file
)
569 """Unlock the mailbox if it is locked."""
571 _unlock_file(self
._file
)
575 """Write any pending changes to disk."""
576 if not self
._pending
:
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
)
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()
603 buffer = self
._file
.read(min(4096,
604 stop
- self
._file
.tell()))
607 new_file
.write(buffer)
608 new_toc
[key
] = (new_start
, new_file
.tell())
609 self
._post
_message
_hook
(new_file
)
612 os
.remove(new_file
.name
)
614 _sync_close(new_file
)
615 # self._file is about to get replaced, so no need to sync.
618 os
.rename(new_file
.name
, self
._path
)
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
)
626 self
._file
= open(self
._path
, 'rb+')
628 self
._pending
= False
630 _lock_file(self
._file
, dotlock
=False)
632 def _pre_mailbox_hook(self
, f
):
633 """Called before writing the mailbox to file f."""
636 def _pre_message_hook(self
, f
):
637 """Called before writing each message to file f."""
640 def _post_message_hook(self
, f
):
641 """Called after writing each message to file f."""
645 """Flush and close the mailbox."""
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:
657 return self
._toc
[key
]
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
)
668 self
._file
_length
= self
._file
.tell() # Record current length of mailbox
673 class _mboxMMDF(_singlefileMailbox
):
674 """An mbox or MMDF mailbox."""
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:])
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
)
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
)
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."""
708 if isinstance(message
, str) and message
.startswith('From '):
709 newline
= message
.find('\n')
711 from_line
= message
[:newline
]
712 message
= message
[newline
+ 1:]
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()
729 class mbox(_mboxMMDF
):
730 """A classic mbox mailbox."""
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."""
744 def _generate_toc(self
):
745 """Generate key-to-(start, stop) table of contents."""
746 starts
, stops
= [], []
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
)
756 stops
.append(line_pos
)
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
= [], []
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
)
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
))
798 stops
.append(line_pos
)
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()
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
):
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))
820 raise NoSuchMailboxError(self
._path
)
823 def add(self
, message
):
824 """Add message and return assigned key."""
829 new_key
= max(keys
) + 1
830 new_path
= os
.path
.join(self
._path
, str(new_key
))
831 f
= _create_carefully(new_path
)
836 self
._dump
_message
(message
, f
)
837 if isinstance(message
, MHMessage
):
838 self
._dump
_sequences
(message
, 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
))
850 f
= open(path
, 'rb+')
852 if e
.errno
== errno
.ENOENT
:
853 raise KeyError('No message with key: %s' % key
)
861 os
.remove(os
.path
.join(self
._path
, str(key
)))
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
))
872 f
= open(path
, 'rb+')
874 if e
.errno
== errno
.ENOENT
:
875 raise KeyError('No message with key: %s' % key
)
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
)
892 def get_message(self
, key
):
893 """Return a Message representation or raise a KeyError."""
896 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+')
898 f
= open(os
.path
.join(self
._path
, str(key
)), 'r')
900 if e
.errno
== errno
.ENOENT
:
901 raise KeyError('No message with key: %s' % key
)
914 for name
, key_list
in self
.get_sequences():
916 msg
.add_sequence(name
)
919 def get_string(self
, key
):
920 """Return a string representation or raise a KeyError."""
923 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+')
925 f
= open(os
.path
.join(self
._path
, str(key
)), 'r')
927 if e
.errno
== errno
.ENOENT
:
928 raise KeyError('No message with key: %s' % key
)
942 def get_file(self
, key
):
943 """Return a file-like representation or raise a KeyError."""
945 f
= open(os
.path
.join(self
._path
, str(key
)), 'rb')
947 if e
.errno
== errno
.ENOENT
:
948 raise KeyError('No message with key: %s' % key
)
954 """Return an iterator over keys."""
955 return iter(sorted(int(entry
) for entry
in os
.listdir(self
._path
)
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
)))
963 """Return a count of messages in the mailbox."""
964 return len(list(self
.iterkeys()))
967 """Lock the mailbox."""
969 self
._file
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'rb+')
970 _lock_file(self
._file
)
974 """Unlock the mailbox if it is locked."""
976 _unlock_file(self
._file
)
977 _sync_close(self
._file
)
982 """Write any pending changes to the disk."""
986 """Flush and close the mailbox."""
990 def list_folders(self
):
991 """Return a list of folder names."""
993 for entry
in os
.listdir(self
._path
):
994 if os
.path
.isdir(os
.path
.join(self
._path
, entry
)):
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'))
1017 raise NotEmptyError('Folder not empty: %s' % self
._path
)
1020 def get_sequences(self
):
1021 """Return a name-to-key-list dictionary to define each sequence."""
1023 f
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'r')
1025 all_keys
= set(self
.keys())
1028 name
, contents
= line
.split(':')
1030 for spec
in contents
.split():
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
) \
1038 if len(results
[name
]) == 0:
1041 raise FormatError('Invalid sequence specification: %s' %
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+')
1051 os
.close(os
.open(f
.name
, os
.O_WRONLY | os
.O_TRUNC
))
1052 for name
, keys
in sequences
.iteritems():
1055 f
.write('%s:' % name
)
1058 for key
in sorted(set(keys
)):
1065 f
.write('%s %s' % (prev
, key
))
1067 f
.write(' %s' % key
)
1070 f
.write(str(prev
) + '\n')
1077 """Re-name messages to eliminate numbering gaps. Invalidates keys."""
1078 sequences
= self
.get_sequences()
1081 for key
in self
.iterkeys():
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
)))
1089 os
.rename(os
.path
.join(self
._path
, str(key
)),
1090 os
.path
.join(self
._path
, str(prev
+ 1)))
1092 self
._next
_key
= prev
+ 1
1093 if len(changes
) == 0:
1095 for name
, key_list
in sequences
.items():
1096 for old
, new
in changes
:
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
)
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()
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()
1153 line
= self
._file
.readline()
1154 if line
== '*** EOOH ***' + os
.linesep
or line
== '':
1156 original_headers
.write(line
.replace(os
.linesep
, '\n'))
1157 visible_headers
= StringIO
.StringIO()
1159 line
= self
._file
.readline()
1160 if line
== os
.linesep
or line
== '':
1162 visible_headers
.write(line
.replace(os
.linesep
, '\n'))
1163 body
= self
._file
.read(stop
- self
._file
.tell()).replace(os
.linesep
,
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
])
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()
1178 line
= self
._file
.readline()
1179 if line
== '*** EOOH ***' + os
.linesep
or line
== '':
1181 original_headers
.write(line
.replace(os
.linesep
, '\n'))
1183 line
= self
._file
.readline()
1184 if line
== os
.linesep
or line
== '':
1186 return original_headers
.getvalue() + \
1187 self
._file
.read(stop
- self
._file
.tell()).replace(os
.linesep
,
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',
1195 def get_labels(self
):
1196 """Return a list of user-defined labels in the mailbox."""
1199 for label_list
in self
._labels
.values():
1200 labels
.update(label_list
)
1201 labels
.difference_update(self
._special
_labels
)
1204 def _generate_toc(self
):
1205 """Generate key-to-(start, stop) table of contents."""
1206 starts
, stops
= [], []
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
))
1226 stops
.append(line_pos
- len(os
.linesep
))
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()),
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
):
1254 for label
in message
.get_labels():
1255 if label
in self
._special
_labels
:
1256 special_labels
.append(label
)
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
)
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
)
1274 line
= orig_buffer
.readline()
1275 self
._file
.write(line
.replace('\n', os
.linesep
))
1276 if line
== '\n' or line
== '':
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())
1284 line
= vis_buffer
.readline()
1285 self
._file
.write(line
.replace('\n', os
.linesep
))
1286 if line
== '\n' or line
== '':
1291 line
= orig_buffer
.readline()
1292 self
._file
.write(line
.replace('\n', os
.linesep
))
1293 if line
== '\n' or line
== '':
1296 buffer = orig_buffer
.read(4096) # Buffer size is arbitrary.
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',
1305 self
._file
.write('*** EOOH ***' + os
.linesep
)
1306 self
._file
.write(message
[:body_start
].replace('\n',
1308 self
._file
.write(message
[body_start
:].replace('\n',
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()
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
)
1323 message
.seek(original_pos
)
1327 buffer = message
.read(4096) # Buffer size is arbitrary.
1330 self
._file
.write(buffer.replace('\n', os
.linesep
))
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
)
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.
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'
1376 self
._date
= time
.time()
1377 Message
.__init
__(self
, message
)
1379 def get_subdir(self
):
1380 """Return 'new' or 'cur'."""
1383 def set_subdir(self
, subdir
):
1384 """Set subdir to 'new' or 'cur'."""
1385 if subdir
== 'new' or subdir
== 'cur':
1386 self
._subdir
= subdir
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:]
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
)))
1411 """Return delivery date of message, in seconds since the epoch."""
1414 def set_date(self
, date
):
1415 """Set delivery date of message, in seconds since the epoch."""
1417 self
._date
= float(date
)
1419 raise TypeError("can't convert to float: %s" % date
)
1422 """Get the message's "info" as a string."""
1425 def set_info(self
, info
):
1426 """Set the message's "info" string."""
1427 if isinstance(info
, str):
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())
1441 message
.add_flag('R')
1442 if self
.get_subdir() == 'cur':
1443 message
.add_flag('O')
1445 message
.add_flag('D')
1447 message
.add_flag('F')
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')
1456 message
.add_sequence('replied')
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')
1464 message
.add_label('deleted')
1466 message
.add_label('answered')
1468 message
.add_label('forwarded')
1469 elif isinstance(message
, Message
):
1472 raise TypeError('Cannot convert to specified type: %s' %
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
)
1489 """Return contents of "From " line."""
1492 def set_from(self
, from_
, time_
=None):
1493 """Set "From " line, formatting and appending time_ if specified."""
1494 if time_
is not None:
1496 time_
= time
.gmtime()
1497 from_
+= ' ' + time
.asctime(time_
)
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."""
1507 status_flags
, xstatus_flags
= '', ''
1508 for flag
in ('R', 'O'):
1510 status_flags
+= flag
1512 for flag
in ('D', 'F', 'A'):
1514 xstatus_flags
+= flag
1516 xstatus_flags
+= ''.join(sorted(flags
))
1518 self
.replace_header('Status', status_flags
)
1520 self
.add_header('Status', status_flags
)
1522 self
.replace_header('X-Status', xstatus_flags
)
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())
1540 message
.set_subdir('cur')
1542 message
.add_flag('F')
1544 message
.add_flag('R')
1546 message
.add_flag('S')
1548 message
.add_flag('T')
1549 del message
['status']
1550 del message
['x-status']
1551 maybe_date
= ' '.join(self
.get_from().split()[-5:])
1553 message
.set_date(calendar
.timegm(time
.strptime(maybe_date
,
1554 '%a %b %d %H:%M:%S %Y')))
1555 except (ValueError, OverflowError):
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')
1565 message
.add_sequence('replied')
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')
1575 message
.add_label('deleted')
1577 message
.add_label('answered')
1578 del message
['status']
1579 del message
['x-status']
1580 elif isinstance(message
, Message
):
1583 raise TypeError('Cannot convert to specified type: %s' %
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
)
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."""
1618 self
._sequences
.remove(sequence
)
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')
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')
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
):
1657 raise TypeError('Cannot convert to specified type: %s' %
1661 class BabylMessage(Message
):
1662 """Message with Babyl-specific properties."""
1664 def __init__(self
, message
=None):
1665 """Initialize an BabylMessage instance."""
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
)
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."""
1689 self
._labels
.remove(label
)
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():
1705 self
._visible
.replace_header(header
, self
[header
])
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')
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')
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
):
1750 raise TypeError('Cannot convert to specified type: %s' %
1754 class MMDFMessage(_mboxMMDFMessage
):
1755 """Message with MMDF-specific properties."""
1759 """A read-only wrapper of a file."""
1761 def __init__(self
, f
, pos
=None):
1762 """Initialize a _ProxyFile."""
1765 self
._pos
= f
.tell()
1769 def read(self
, size
=None):
1771 return self
._read
(size
, self
._file
.read
)
1773 def readline(self
, size
=None):
1775 return self
._read
(size
, self
._file
.readline
)
1777 def readlines(self
, sizehint
=None):
1778 """Read multiple lines."""
1782 if sizehint
is not None:
1783 sizehint
-= len(line
)
1789 """Iterate over lines."""
1790 return iter(self
.readline
, "")
1793 """Return the position."""
1796 def seek(self
, offset
, whence
=0):
1797 """Change position."""
1799 self
._file
.seek(self
._pos
)
1800 self
._file
.seek(offset
, whence
)
1801 self
._pos
= self
._file
.tell()
1804 """Close the file."""
1807 def _read(self
, size
, read_method
):
1808 """Read size bytes using read_method."""
1811 self
._file
.seek(self
._pos
)
1812 result
= read_method(size
)
1813 self
._pos
= self
._file
.tell()
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
)
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."""
1833 self
._pos
= self
._start
1836 self
._pos
= self
._stop
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
1845 if size
is None or size
< 0 or 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
1856 fcntl
.lockf(f
, fcntl
.LOCK_EX | fcntl
.LOCK_NB
)
1858 if e
.errno
in (errno
.EAGAIN
, errno
.EACCES
):
1859 raise ExternalClashError('lockf: lock unavailable: %s' %
1865 pre_lock
= _create_temporary(f
.name
+ '.lock')
1868 if e
.errno
== errno
.EACCES
:
1869 return # Without write access, just skip dotlocking.
1873 if hasattr(os
, 'link'):
1874 os
.link(pre_lock
.name
, f
.name
+ '.lock')
1876 os
.unlink(pre_lock
.name
)
1878 os
.rename(pre_lock
.name
, f
.name
+ '.lock')
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' %
1890 fcntl
.lockf(f
, fcntl
.LOCK_UN
)
1892 os
.remove(f
.name
+ '.lock')
1895 def _unlock_file(f
):
1896 """Unlock file f using lockf and dot locking."""
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)
1906 return open(path
, 'rb+')
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(),
1917 """Ensure changes to file f are physically on disk."""
1919 if hasattr(os
, 'fsync'):
1920 os
.fsync(f
.fileno())
1923 """Close file f, ensuring all changes are physically on disk."""
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.
1934 def __init__(self
, fp
, factory
=rfc822
.Message
):
1937 self
.factory
= factory
1940 return iter(self
.next
, None)
1944 self
.fp
.seek(self
.seekp
)
1946 self
._search
_start
()
1948 self
.seekp
= self
.fp
.tell()
1950 start
= self
.fp
.tell()
1952 self
.seekp
= stop
= self
.fp
.tell()
1955 return self
.factory(_PartialFile(self
.fp
, start
, stop
))
1957 # Recommended to use PortableUnixMailbox instead!
1958 class UnixMailbox(_Mailbox
):
1960 def _search_start(self
):
1962 pos
= self
.fp
.tell()
1963 line
= self
.fp
.readline()
1966 if line
[:5] == 'From ' and self
._isrealfromline
(line
):
1970 def _search_end(self
):
1971 self
.fp
.readline() # Throw away header line
1973 pos
= self
.fp
.tell()
1974 line
= self
.fp
.readline()
1977 if line
[:5] == 'From ' and self
._isrealfromline
(line
):
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 ".
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
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*"
2012 def _strict_isrealfromline(self
, line
):
2013 if not self
._regexp
:
2015 self
._regexp
= re
.compile(self
._fromlinepattern
)
2016 return self
._regexp
.match(line
)
2018 def _portable_isrealfromline(self
, line
):
2021 _isrealfromline
= _strict_isrealfromline
2024 class PortableUnixMailbox(UnixMailbox
):
2025 _isrealfromline
= UnixMailbox
._portable
_isrealfromline
2028 class MmdfMailbox(_Mailbox
):
2030 def _search_start(self
):
2032 line
= self
.fp
.readline()
2035 if line
[:5] == '\001\001\001\001\n':
2038 def _search_end(self
):
2040 pos
= self
.fp
.tell()
2041 line
= self
.fp
.readline()
2044 if line
== '\001\001\001\001\n':
2051 def __init__(self
, dirname
, factory
=rfc822
.Message
):
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)
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
2068 return iter(self
.next
, None)
2073 fn
= self
.boxes
.pop()
2074 fp
= open(os
.path
.join(self
.dirname
, fn
))
2075 msg
= self
.factory(fp
)
2078 except (AttributeError, TypeError):
2083 class BabylMailbox(_Mailbox
):
2085 def _search_start(self
):
2087 line
= self
.fp
.readline()
2090 if line
== '*** EOOH ***\n':
2093 def _search_end(self
):
2095 pos
= self
.fp
.tell()
2096 line
= self
.fp
.readline()
2099 if line
== '\037\014\n' or line
== '\037':
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."""