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
))
404 def remove_folder(self
, folder
):
405 """Delete the named folder, which must be empty."""
406 path
= os
.path
.join(self
._path
, '.' + folder
)
407 for entry
in os
.listdir(os
.path
.join(path
, 'new')) + \
408 os
.listdir(os
.path
.join(path
, 'cur')):
409 if len(entry
) < 1 or entry
[0] != '.':
410 raise NotEmptyError('Folder contains message(s): %s' % folder
)
411 for entry
in os
.listdir(path
):
412 if entry
!= 'new' and entry
!= 'cur' and entry
!= 'tmp' and \
413 os
.path
.isdir(os
.path
.join(path
, entry
)):
414 raise NotEmptyError("Folder contains subdirectory '%s': %s" %
416 for root
, dirs
, files
in os
.walk(path
, topdown
=False):
418 os
.remove(os
.path
.join(root
, entry
))
420 os
.rmdir(os
.path
.join(root
, entry
))
424 """Delete old files in "tmp"."""
426 for entry
in os
.listdir(os
.path
.join(self
._path
, 'tmp')):
427 path
= os
.path
.join(self
._path
, 'tmp', entry
)
428 if now
- os
.path
.getatime(path
) > 129600: # 60 * 60 * 36
431 _count
= 1 # This is used to generate unique file names.
433 def _create_tmp(self
):
434 """Create a file in the tmp subdirectory and open and return it."""
436 hostname
= socket
.gethostname()
438 hostname
= hostname
.replace('/', r
'\057')
440 hostname
= hostname
.replace(':', r
'\072')
441 uniq
= "%s.M%sP%sQ%s.%s" % (int(now
), int(now
% 1 * 1e6
), os
.getpid(),
442 Maildir
._count
, hostname
)
443 path
= os
.path
.join(self
._path
, 'tmp', uniq
)
447 if e
.errno
== errno
.ENOENT
:
450 return _create_carefully(path
)
452 if e
.errno
!= errno
.EEXIST
:
457 # Fall through to here if stat succeeded or open raised EEXIST.
458 raise ExternalClashError('Name clash prevented file creation: %s' %
462 """Update table of contents mapping."""
464 for subdir
in ('new', 'cur'):
465 subdir_path
= os
.path
.join(self
._path
, subdir
)
466 for entry
in os
.listdir(subdir_path
):
467 p
= os
.path
.join(subdir_path
, entry
)
470 uniq
= entry
.split(self
.colon
)[0]
471 self
._toc
[uniq
] = os
.path
.join(subdir
, entry
)
473 def _lookup(self
, key
):
474 """Use TOC to return subpath for given key, or raise a KeyError."""
476 if os
.path
.exists(os
.path
.join(self
._path
, self
._toc
[key
])):
477 return self
._toc
[key
]
482 return self
._toc
[key
]
484 raise KeyError('No message with key: %s' % key
)
486 # This method is for backward compatibility only.
488 """Return the next message in a one-time iteration."""
489 if not hasattr(self
, '_onetime_keys'):
490 self
._onetime
_keys
= self
.iterkeys()
493 return self
[self
._onetime
_keys
.next()]
494 except StopIteration:
500 class _singlefileMailbox(Mailbox
):
501 """A single-file mailbox."""
503 def __init__(self
, path
, factory
=None, create
=True):
504 """Initialize a single-file mailbox."""
505 Mailbox
.__init
__(self
, path
, factory
, create
)
507 f
= open(self
._path
, 'rb+')
509 if e
.errno
== errno
.ENOENT
:
511 f
= open(self
._path
, 'wb+')
513 raise NoSuchMailboxError(self
._path
)
514 elif e
.errno
== errno
.EACCES
:
515 f
= open(self
._path
, 'rb')
521 self
._pending
= False # No changes require rewriting the file.
523 self
._file
_length
= None # Used to record mailbox size
525 def add(self
, message
):
526 """Add message and return assigned key."""
528 self
._toc
[self
._next
_key
] = self
._append
_message
(message
)
531 return self
._next
_key
- 1
533 def remove(self
, key
):
534 """Remove the keyed message; raise KeyError if it doesn't exist."""
539 def __setitem__(self
, key
, message
):
540 """Replace the keyed message; raise KeyError if it doesn't exist."""
542 self
._toc
[key
] = self
._append
_message
(message
)
546 """Return an iterator over keys."""
548 for key
in self
._toc
.keys():
551 def has_key(self
, key
):
552 """Return True if the keyed message exists, False otherwise."""
554 return key
in self
._toc
557 """Return a count of messages in the mailbox."""
559 return len(self
._toc
)
562 """Lock the mailbox."""
564 _lock_file(self
._file
)
568 """Unlock the mailbox if it is locked."""
570 _unlock_file(self
._file
)
574 """Write any pending changes to disk."""
575 if not self
._pending
:
578 # In order to be writing anything out at all, self._toc must
579 # already have been generated (and presumably has been modified
580 # by adding or deleting an item).
581 assert self
._toc
is not None
583 # Check length of self._file; if it's changed, some other process
584 # has modified the mailbox since we scanned it.
585 self
._file
.seek(0, 2)
586 cur_len
= self
._file
.tell()
587 if cur_len
!= self
._file
_length
:
588 raise ExternalClashError('Size of mailbox file changed '
589 '(expected %i, found %i)' %
590 (self
._file
_length
, cur_len
))
592 new_file
= _create_temporary(self
._path
)
595 self
._pre
_mailbox
_hook
(new_file
)
596 for key
in sorted(self
._toc
.keys()):
597 start
, stop
= self
._toc
[key
]
598 self
._file
.seek(start
)
599 self
._pre
_message
_hook
(new_file
)
600 new_start
= new_file
.tell()
602 buffer = self
._file
.read(min(4096,
603 stop
- self
._file
.tell()))
606 new_file
.write(buffer)
607 new_toc
[key
] = (new_start
, new_file
.tell())
608 self
._post
_message
_hook
(new_file
)
611 os
.remove(new_file
.name
)
613 _sync_close(new_file
)
614 # self._file is about to get replaced, so no need to sync.
617 os
.rename(new_file
.name
, self
._path
)
619 if e
.errno
== errno
.EEXIST
or \
620 (os
.name
== 'os2' and e
.errno
== errno
.EACCES
):
621 os
.remove(self
._path
)
622 os
.rename(new_file
.name
, self
._path
)
625 self
._file
= open(self
._path
, 'rb+')
627 self
._pending
= False
629 _lock_file(self
._file
, dotlock
=False)
631 def _pre_mailbox_hook(self
, f
):
632 """Called before writing the mailbox to file f."""
635 def _pre_message_hook(self
, f
):
636 """Called before writing each message to file f."""
639 def _post_message_hook(self
, f
):
640 """Called after writing each message to file f."""
644 """Flush and close the mailbox."""
648 self
._file
.close() # Sync has been done by self.flush() above.
650 def _lookup(self
, key
=None):
651 """Return (start, stop) or raise KeyError."""
652 if self
._toc
is None:
656 return self
._toc
[key
]
658 raise KeyError('No message with key: %s' % key
)
660 def _append_message(self
, message
):
661 """Append message to mailbox and return (start, stop) offsets."""
662 self
._file
.seek(0, 2)
663 self
._pre
_message
_hook
(self
._file
)
664 offsets
= self
._install
_message
(message
)
665 self
._post
_message
_hook
(self
._file
)
667 self
._file
_length
= self
._file
.tell() # Record current length of mailbox
672 class _mboxMMDF(_singlefileMailbox
):
673 """An mbox or MMDF mailbox."""
677 def get_message(self
, key
):
678 """Return a Message representation or raise a KeyError."""
679 start
, stop
= self
._lookup
(key
)
680 self
._file
.seek(start
)
681 from_line
= self
._file
.readline().replace(os
.linesep
, '')
682 string
= self
._file
.read(stop
- self
._file
.tell())
683 msg
= self
._message
_factory
(string
.replace(os
.linesep
, '\n'))
684 msg
.set_from(from_line
[5:])
687 def get_string(self
, key
, from_
=False):
688 """Return a string representation or raise a KeyError."""
689 start
, stop
= self
._lookup
(key
)
690 self
._file
.seek(start
)
692 self
._file
.readline()
693 string
= self
._file
.read(stop
- self
._file
.tell())
694 return string
.replace(os
.linesep
, '\n')
696 def get_file(self
, key
, from_
=False):
697 """Return a file-like representation or raise a KeyError."""
698 start
, stop
= self
._lookup
(key
)
699 self
._file
.seek(start
)
701 self
._file
.readline()
702 return _PartialFile(self
._file
, self
._file
.tell(), stop
)
704 def _install_message(self
, message
):
705 """Format a message and blindly write to self._file."""
707 if isinstance(message
, str) and message
.startswith('From '):
708 newline
= message
.find('\n')
710 from_line
= message
[:newline
]
711 message
= message
[newline
+ 1:]
715 elif isinstance(message
, _mboxMMDFMessage
):
716 from_line
= 'From ' + message
.get_from()
717 elif isinstance(message
, email
.message
.Message
):
718 from_line
= message
.get_unixfrom() # May be None.
719 if from_line
is None:
720 from_line
= 'From MAILER-DAEMON %s' % time
.asctime(time
.gmtime())
721 start
= self
._file
.tell()
722 self
._file
.write(from_line
+ os
.linesep
)
723 self
._dump
_message
(message
, self
._file
, self
._mangle
_from
_)
724 stop
= self
._file
.tell()
728 class mbox(_mboxMMDF
):
729 """A classic mbox mailbox."""
733 def __init__(self
, path
, factory
=None, create
=True):
734 """Initialize an mbox mailbox."""
735 self
._message
_factory
= mboxMessage
736 _mboxMMDF
.__init
__(self
, path
, factory
, create
)
738 def _pre_message_hook(self
, f
):
739 """Called before writing each message to file f."""
743 def _generate_toc(self
):
744 """Generate key-to-(start, stop) table of contents."""
745 starts
, stops
= [], []
748 line_pos
= self
._file
.tell()
749 line
= self
._file
.readline()
750 if line
.startswith('From '):
751 if len(stops
) < len(starts
):
752 stops
.append(line_pos
- len(os
.linesep
))
753 starts
.append(line_pos
)
755 stops
.append(line_pos
)
757 self
._toc
= dict(enumerate(zip(starts
, stops
)))
758 self
._next
_key
= len(self
._toc
)
759 self
._file
_length
= self
._file
.tell()
762 class MMDF(_mboxMMDF
):
763 """An MMDF mailbox."""
765 def __init__(self
, path
, factory
=None, create
=True):
766 """Initialize an MMDF mailbox."""
767 self
._message
_factory
= MMDFMessage
768 _mboxMMDF
.__init
__(self
, path
, factory
, create
)
770 def _pre_message_hook(self
, f
):
771 """Called before writing each message to file f."""
772 f
.write('\001\001\001\001' + os
.linesep
)
774 def _post_message_hook(self
, f
):
775 """Called after writing each message to file f."""
776 f
.write(os
.linesep
+ '\001\001\001\001' + os
.linesep
)
778 def _generate_toc(self
):
779 """Generate key-to-(start, stop) table of contents."""
780 starts
, stops
= [], []
785 line
= self
._file
.readline()
786 next_pos
= self
._file
.tell()
787 if line
.startswith('\001\001\001\001' + os
.linesep
):
788 starts
.append(next_pos
)
791 line
= self
._file
.readline()
792 next_pos
= self
._file
.tell()
793 if line
== '\001\001\001\001' + os
.linesep
:
794 stops
.append(line_pos
- len(os
.linesep
))
797 stops
.append(line_pos
)
801 self
._toc
= dict(enumerate(zip(starts
, stops
)))
802 self
._next
_key
= len(self
._toc
)
803 self
._file
.seek(0, 2)
804 self
._file
_length
= self
._file
.tell()
810 def __init__(self
, path
, factory
=None, create
=True):
811 """Initialize an MH instance."""
812 Mailbox
.__init
__(self
, path
, factory
, create
)
813 if not os
.path
.exists(self
._path
):
815 os
.mkdir(self
._path
, 0700)
816 os
.close(os
.open(os
.path
.join(self
._path
, '.mh_sequences'),
817 os
.O_CREAT | os
.O_EXCL | os
.O_WRONLY
, 0600))
819 raise NoSuchMailboxError(self
._path
)
822 def add(self
, message
):
823 """Add message and return assigned key."""
828 new_key
= max(keys
) + 1
829 new_path
= os
.path
.join(self
._path
, str(new_key
))
830 f
= _create_carefully(new_path
)
835 self
._dump
_message
(message
, f
)
836 if isinstance(message
, MHMessage
):
837 self
._dump
_sequences
(message
, new_key
)
845 def remove(self
, key
):
846 """Remove the keyed message; raise KeyError if it doesn't exist."""
847 path
= os
.path
.join(self
._path
, str(key
))
849 f
= open(path
, 'rb+')
851 if e
.errno
== errno
.ENOENT
:
852 raise KeyError('No message with key: %s' % key
)
860 os
.remove(os
.path
.join(self
._path
, str(key
)))
867 def __setitem__(self
, key
, message
):
868 """Replace the keyed message; raise KeyError if it doesn't exist."""
869 path
= os
.path
.join(self
._path
, str(key
))
871 f
= open(path
, 'rb+')
873 if e
.errno
== errno
.ENOENT
:
874 raise KeyError('No message with key: %s' % key
)
881 os
.close(os
.open(path
, os
.O_WRONLY | os
.O_TRUNC
))
882 self
._dump
_message
(message
, f
)
883 if isinstance(message
, MHMessage
):
884 self
._dump
_sequences
(message
, key
)
891 def get_message(self
, key
):
892 """Return a Message representation or raise a KeyError."""
895 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+')
897 f
= open(os
.path
.join(self
._path
, str(key
)), 'r')
899 if e
.errno
== errno
.ENOENT
:
900 raise KeyError('No message with key: %s' % key
)
913 for name
, key_list
in self
.get_sequences():
915 msg
.add_sequence(name
)
918 def get_string(self
, key
):
919 """Return a string representation or raise a KeyError."""
922 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+')
924 f
= open(os
.path
.join(self
._path
, str(key
)), 'r')
926 if e
.errno
== errno
.ENOENT
:
927 raise KeyError('No message with key: %s' % key
)
941 def get_file(self
, key
):
942 """Return a file-like representation or raise a KeyError."""
944 f
= open(os
.path
.join(self
._path
, str(key
)), 'rb')
946 if e
.errno
== errno
.ENOENT
:
947 raise KeyError('No message with key: %s' % key
)
953 """Return an iterator over keys."""
954 return iter(sorted(int(entry
) for entry
in os
.listdir(self
._path
)
957 def has_key(self
, key
):
958 """Return True if the keyed message exists, False otherwise."""
959 return os
.path
.exists(os
.path
.join(self
._path
, str(key
)))
962 """Return a count of messages in the mailbox."""
963 return len(list(self
.iterkeys()))
966 """Lock the mailbox."""
968 self
._file
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'rb+')
969 _lock_file(self
._file
)
973 """Unlock the mailbox if it is locked."""
975 _unlock_file(self
._file
)
976 _sync_close(self
._file
)
981 """Write any pending changes to the disk."""
985 """Flush and close the mailbox."""
989 def list_folders(self
):
990 """Return a list of folder names."""
992 for entry
in os
.listdir(self
._path
):
993 if os
.path
.isdir(os
.path
.join(self
._path
, entry
)):
997 def get_folder(self
, folder
):
998 """Return an MH instance for the named folder."""
999 return MH(os
.path
.join(self
._path
, folder
),
1000 factory
=self
._factory
, create
=False)
1002 def add_folder(self
, folder
):
1003 """Create a folder and return an MH instance representing it."""
1004 return MH(os
.path
.join(self
._path
, folder
),
1005 factory
=self
._factory
)
1007 def remove_folder(self
, folder
):
1008 """Delete the named folder, which must be empty."""
1009 path
= os
.path
.join(self
._path
, folder
)
1010 entries
= os
.listdir(path
)
1011 if entries
== ['.mh_sequences']:
1012 os
.remove(os
.path
.join(path
, '.mh_sequences'))
1016 raise NotEmptyError('Folder not empty: %s' % self
._path
)
1019 def get_sequences(self
):
1020 """Return a name-to-key-list dictionary to define each sequence."""
1022 f
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'r')
1024 all_keys
= set(self
.keys())
1027 name
, contents
= line
.split(':')
1029 for spec
in contents
.split():
1033 start
, stop
= (int(x
) for x
in spec
.split('-'))
1034 keys
.update(range(start
, stop
+ 1))
1035 results
[name
] = [key
for key
in sorted(keys
) \
1037 if len(results
[name
]) == 0:
1040 raise FormatError('Invalid sequence specification: %s' %
1046 def set_sequences(self
, sequences
):
1047 """Set sequences using the given name-to-key-list dictionary."""
1048 f
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'r+')
1050 os
.close(os
.open(f
.name
, os
.O_WRONLY | os
.O_TRUNC
))
1051 for name
, keys
in sequences
.iteritems():
1054 f
.write('%s:' % name
)
1057 for key
in sorted(set(keys
)):
1064 f
.write('%s %s' % (prev
, key
))
1066 f
.write(' %s' % key
)
1069 f
.write(str(prev
) + '\n')
1076 """Re-name messages to eliminate numbering gaps. Invalidates keys."""
1077 sequences
= self
.get_sequences()
1080 for key
in self
.iterkeys():
1082 changes
.append((key
, prev
+ 1))
1083 if hasattr(os
, 'link'):
1084 os
.link(os
.path
.join(self
._path
, str(key
)),
1085 os
.path
.join(self
._path
, str(prev
+ 1)))
1086 os
.unlink(os
.path
.join(self
._path
, str(key
)))
1088 os
.rename(os
.path
.join(self
._path
, str(key
)),
1089 os
.path
.join(self
._path
, str(prev
+ 1)))
1091 self
._next
_key
= prev
+ 1
1092 if len(changes
) == 0:
1094 for name
, key_list
in sequences
.items():
1095 for old
, new
in changes
:
1097 key_list
[key_list
.index(old
)] = new
1098 self
.set_sequences(sequences
)
1100 def _dump_sequences(self
, message
, key
):
1101 """Inspect a new MHMessage and update sequences appropriately."""
1102 pending_sequences
= message
.get_sequences()
1103 all_sequences
= self
.get_sequences()
1104 for name
, key_list
in all_sequences
.iteritems():
1105 if name
in pending_sequences
:
1106 key_list
.append(key
)
1107 elif key
in key_list
:
1108 del key_list
[key_list
.index(key
)]
1109 for sequence
in pending_sequences
:
1110 if sequence
not in all_sequences
:
1111 all_sequences
[sequence
] = [key
]
1112 self
.set_sequences(all_sequences
)
1115 class Babyl(_singlefileMailbox
):
1116 """An Rmail-style Babyl mailbox."""
1118 _special_labels
= frozenset(('unseen', 'deleted', 'filed', 'answered',
1119 'forwarded', 'edited', 'resent'))
1121 def __init__(self
, path
, factory
=None, create
=True):
1122 """Initialize a Babyl mailbox."""
1123 _singlefileMailbox
.__init
__(self
, path
, factory
, create
)
1126 def add(self
, message
):
1127 """Add message and return assigned key."""
1128 key
= _singlefileMailbox
.add(self
, message
)
1129 if isinstance(message
, BabylMessage
):
1130 self
._labels
[key
] = message
.get_labels()
1133 def remove(self
, key
):
1134 """Remove the keyed message; raise KeyError if it doesn't exist."""
1135 _singlefileMailbox
.remove(self
, key
)
1136 if key
in self
._labels
:
1137 del self
._labels
[key
]
1139 def __setitem__(self
, key
, message
):
1140 """Replace the keyed message; raise KeyError if it doesn't exist."""
1141 _singlefileMailbox
.__setitem
__(self
, key
, message
)
1142 if isinstance(message
, BabylMessage
):
1143 self
._labels
[key
] = message
.get_labels()
1145 def get_message(self
, key
):
1146 """Return a Message representation or raise a KeyError."""
1147 start
, stop
= self
._lookup
(key
)
1148 self
._file
.seek(start
)
1149 self
._file
.readline() # Skip '1,' line specifying labels.
1150 original_headers
= StringIO
.StringIO()
1152 line
= self
._file
.readline()
1153 if line
== '*** EOOH ***' + os
.linesep
or line
== '':
1155 original_headers
.write(line
.replace(os
.linesep
, '\n'))
1156 visible_headers
= StringIO
.StringIO()
1158 line
= self
._file
.readline()
1159 if line
== os
.linesep
or line
== '':
1161 visible_headers
.write(line
.replace(os
.linesep
, '\n'))
1162 body
= self
._file
.read(stop
- self
._file
.tell()).replace(os
.linesep
,
1164 msg
= BabylMessage(original_headers
.getvalue() + body
)
1165 msg
.set_visible(visible_headers
.getvalue())
1166 if key
in self
._labels
:
1167 msg
.set_labels(self
._labels
[key
])
1170 def get_string(self
, key
):
1171 """Return a string representation or raise a KeyError."""
1172 start
, stop
= self
._lookup
(key
)
1173 self
._file
.seek(start
)
1174 self
._file
.readline() # Skip '1,' line specifying labels.
1175 original_headers
= StringIO
.StringIO()
1177 line
= self
._file
.readline()
1178 if line
== '*** EOOH ***' + os
.linesep
or line
== '':
1180 original_headers
.write(line
.replace(os
.linesep
, '\n'))
1182 line
= self
._file
.readline()
1183 if line
== os
.linesep
or line
== '':
1185 return original_headers
.getvalue() + \
1186 self
._file
.read(stop
- self
._file
.tell()).replace(os
.linesep
,
1189 def get_file(self
, key
):
1190 """Return a file-like representation or raise a KeyError."""
1191 return StringIO
.StringIO(self
.get_string(key
).replace('\n',
1194 def get_labels(self
):
1195 """Return a list of user-defined labels in the mailbox."""
1198 for label_list
in self
._labels
.values():
1199 labels
.update(label_list
)
1200 labels
.difference_update(self
._special
_labels
)
1203 def _generate_toc(self
):
1204 """Generate key-to-(start, stop) table of contents."""
1205 starts
, stops
= [], []
1211 line
= self
._file
.readline()
1212 next_pos
= self
._file
.tell()
1213 if line
== '\037\014' + os
.linesep
:
1214 if len(stops
) < len(starts
):
1215 stops
.append(line_pos
- len(os
.linesep
))
1216 starts
.append(next_pos
)
1217 labels
= [label
.strip() for label
1218 in self
._file
.readline()[1:].split(',')
1219 if label
.strip() != '']
1220 label_lists
.append(labels
)
1221 elif line
== '\037' or line
== '\037' + os
.linesep
:
1222 if len(stops
) < len(starts
):
1223 stops
.append(line_pos
- len(os
.linesep
))
1225 stops
.append(line_pos
- len(os
.linesep
))
1227 self
._toc
= dict(enumerate(zip(starts
, stops
)))
1228 self
._labels
= dict(enumerate(label_lists
))
1229 self
._next
_key
= len(self
._toc
)
1230 self
._file
.seek(0, 2)
1231 self
._file
_length
= self
._file
.tell()
1233 def _pre_mailbox_hook(self
, f
):
1234 """Called before writing the mailbox to file f."""
1235 f
.write('BABYL OPTIONS:%sVersion: 5%sLabels:%s%s\037' %
1236 (os
.linesep
, os
.linesep
, ','.join(self
.get_labels()),
1239 def _pre_message_hook(self
, f
):
1240 """Called before writing each message to file f."""
1241 f
.write('\014' + os
.linesep
)
1243 def _post_message_hook(self
, f
):
1244 """Called after writing each message to file f."""
1245 f
.write(os
.linesep
+ '\037')
1247 def _install_message(self
, message
):
1248 """Write message contents and return (start, stop)."""
1249 start
= self
._file
.tell()
1250 if isinstance(message
, BabylMessage
):
1253 for label
in message
.get_labels():
1254 if label
in self
._special
_labels
:
1255 special_labels
.append(label
)
1257 labels
.append(label
)
1258 self
._file
.write('1')
1259 for label
in special_labels
:
1260 self
._file
.write(', ' + label
)
1261 self
._file
.write(',,')
1262 for label
in labels
:
1263 self
._file
.write(' ' + label
+ ',')
1264 self
._file
.write(os
.linesep
)
1266 self
._file
.write('1,,' + os
.linesep
)
1267 if isinstance(message
, email
.message
.Message
):
1268 orig_buffer
= StringIO
.StringIO()
1269 orig_generator
= email
.generator
.Generator(orig_buffer
, False, 0)
1270 orig_generator
.flatten(message
)
1273 line
= orig_buffer
.readline()
1274 self
._file
.write(line
.replace('\n', os
.linesep
))
1275 if line
== '\n' or line
== '':
1277 self
._file
.write('*** EOOH ***' + os
.linesep
)
1278 if isinstance(message
, BabylMessage
):
1279 vis_buffer
= StringIO
.StringIO()
1280 vis_generator
= email
.generator
.Generator(vis_buffer
, False, 0)
1281 vis_generator
.flatten(message
.get_visible())
1283 line
= vis_buffer
.readline()
1284 self
._file
.write(line
.replace('\n', os
.linesep
))
1285 if line
== '\n' or line
== '':
1290 line
= orig_buffer
.readline()
1291 self
._file
.write(line
.replace('\n', os
.linesep
))
1292 if line
== '\n' or line
== '':
1295 buffer = orig_buffer
.read(4096) # Buffer size is arbitrary.
1298 self
._file
.write(buffer.replace('\n', os
.linesep
))
1299 elif isinstance(message
, str):
1300 body_start
= message
.find('\n\n') + 2
1301 if body_start
- 2 != -1:
1302 self
._file
.write(message
[:body_start
].replace('\n',
1304 self
._file
.write('*** EOOH ***' + os
.linesep
)
1305 self
._file
.write(message
[:body_start
].replace('\n',
1307 self
._file
.write(message
[body_start
:].replace('\n',
1310 self
._file
.write('*** EOOH ***' + os
.linesep
+ os
.linesep
)
1311 self
._file
.write(message
.replace('\n', os
.linesep
))
1312 elif hasattr(message
, 'readline'):
1313 original_pos
= message
.tell()
1316 line
= message
.readline()
1317 self
._file
.write(line
.replace('\n', os
.linesep
))
1318 if line
== '\n' or line
== '':
1319 self
._file
.write('*** EOOH ***' + os
.linesep
)
1322 message
.seek(original_pos
)
1326 buffer = message
.read(4096) # Buffer size is arbitrary.
1329 self
._file
.write(buffer.replace('\n', os
.linesep
))
1331 raise TypeError('Invalid message type: %s' % type(message
))
1332 stop
= self
._file
.tell()
1333 return (start
, stop
)
1336 class Message(email
.message
.Message
):
1337 """Message with mailbox-format-specific properties."""
1339 def __init__(self
, message
=None):
1340 """Initialize a Message instance."""
1341 if isinstance(message
, email
.message
.Message
):
1342 self
._become
_message
(copy
.deepcopy(message
))
1343 if isinstance(message
, Message
):
1344 message
._explain
_to
(self
)
1345 elif isinstance(message
, str):
1346 self
._become
_message
(email
.message_from_string(message
))
1347 elif hasattr(message
, "read"):
1348 self
._become
_message
(email
.message_from_file(message
))
1349 elif message
is None:
1350 email
.message
.Message
.__init
__(self
)
1352 raise TypeError('Invalid message type: %s' % type(message
))
1354 def _become_message(self
, message
):
1355 """Assume the non-format-specific state of message."""
1356 for name
in ('_headers', '_unixfrom', '_payload', '_charset',
1357 'preamble', 'epilogue', 'defects', '_default_type'):
1358 self
.__dict
__[name
] = message
.__dict
__[name
]
1360 def _explain_to(self
, message
):
1361 """Copy format-specific state to message insofar as possible."""
1362 if isinstance(message
, Message
):
1363 return # There's nothing format-specific to explain.
1365 raise TypeError('Cannot convert to specified type')
1368 class MaildirMessage(Message
):
1369 """Message with Maildir-specific properties."""
1371 def __init__(self
, message
=None):
1372 """Initialize a MaildirMessage instance."""
1373 self
._subdir
= 'new'
1375 self
._date
= time
.time()
1376 Message
.__init
__(self
, message
)
1378 def get_subdir(self
):
1379 """Return 'new' or 'cur'."""
1382 def set_subdir(self
, subdir
):
1383 """Set subdir to 'new' or 'cur'."""
1384 if subdir
== 'new' or subdir
== 'cur':
1385 self
._subdir
= subdir
1387 raise ValueError("subdir must be 'new' or 'cur': %s" % subdir
)
1389 def get_flags(self
):
1390 """Return as a string the flags that are set."""
1391 if self
._info
.startswith('2,'):
1392 return self
._info
[2:]
1396 def set_flags(self
, flags
):
1397 """Set the given flags and unset all others."""
1398 self
._info
= '2,' + ''.join(sorted(flags
))
1400 def add_flag(self
, flag
):
1401 """Set the given flag(s) without changing others."""
1402 self
.set_flags(''.join(set(self
.get_flags()) |
set(flag
)))
1404 def remove_flag(self
, flag
):
1405 """Unset the given string flag(s) without changing others."""
1406 if self
.get_flags() != '':
1407 self
.set_flags(''.join(set(self
.get_flags()) - set(flag
)))
1410 """Return delivery date of message, in seconds since the epoch."""
1413 def set_date(self
, date
):
1414 """Set delivery date of message, in seconds since the epoch."""
1416 self
._date
= float(date
)
1418 raise TypeError("can't convert to float: %s" % date
)
1421 """Get the message's "info" as a string."""
1424 def set_info(self
, info
):
1425 """Set the message's "info" string."""
1426 if isinstance(info
, str):
1429 raise TypeError('info must be a string: %s' % type(info
))
1431 def _explain_to(self
, message
):
1432 """Copy Maildir-specific state to message insofar as possible."""
1433 if isinstance(message
, MaildirMessage
):
1434 message
.set_flags(self
.get_flags())
1435 message
.set_subdir(self
.get_subdir())
1436 message
.set_date(self
.get_date())
1437 elif isinstance(message
, _mboxMMDFMessage
):
1438 flags
= set(self
.get_flags())
1440 message
.add_flag('R')
1441 if self
.get_subdir() == 'cur':
1442 message
.add_flag('O')
1444 message
.add_flag('D')
1446 message
.add_flag('F')
1448 message
.add_flag('A')
1449 message
.set_from('MAILER-DAEMON', time
.gmtime(self
.get_date()))
1450 elif isinstance(message
, MHMessage
):
1451 flags
= set(self
.get_flags())
1452 if 'S' not in flags
:
1453 message
.add_sequence('unseen')
1455 message
.add_sequence('replied')
1457 message
.add_sequence('flagged')
1458 elif isinstance(message
, BabylMessage
):
1459 flags
= set(self
.get_flags())
1460 if 'S' not in flags
:
1461 message
.add_label('unseen')
1463 message
.add_label('deleted')
1465 message
.add_label('answered')
1467 message
.add_label('forwarded')
1468 elif isinstance(message
, Message
):
1471 raise TypeError('Cannot convert to specified type: %s' %
1475 class _mboxMMDFMessage(Message
):
1476 """Message with mbox- or MMDF-specific properties."""
1478 def __init__(self
, message
=None):
1479 """Initialize an mboxMMDFMessage instance."""
1480 self
.set_from('MAILER-DAEMON', True)
1481 if isinstance(message
, email
.message
.Message
):
1482 unixfrom
= message
.get_unixfrom()
1483 if unixfrom
is not None and unixfrom
.startswith('From '):
1484 self
.set_from(unixfrom
[5:])
1485 Message
.__init
__(self
, message
)
1488 """Return contents of "From " line."""
1491 def set_from(self
, from_
, time_
=None):
1492 """Set "From " line, formatting and appending time_ if specified."""
1493 if time_
is not None:
1495 time_
= time
.gmtime()
1496 from_
+= ' ' + time
.asctime(time_
)
1499 def get_flags(self
):
1500 """Return as a string the flags that are set."""
1501 return self
.get('Status', '') + self
.get('X-Status', '')
1503 def set_flags(self
, flags
):
1504 """Set the given flags and unset all others."""
1506 status_flags
, xstatus_flags
= '', ''
1507 for flag
in ('R', 'O'):
1509 status_flags
+= flag
1511 for flag
in ('D', 'F', 'A'):
1513 xstatus_flags
+= flag
1515 xstatus_flags
+= ''.join(sorted(flags
))
1517 self
.replace_header('Status', status_flags
)
1519 self
.add_header('Status', status_flags
)
1521 self
.replace_header('X-Status', xstatus_flags
)
1523 self
.add_header('X-Status', xstatus_flags
)
1525 def add_flag(self
, flag
):
1526 """Set the given flag(s) without changing others."""
1527 self
.set_flags(''.join(set(self
.get_flags()) |
set(flag
)))
1529 def remove_flag(self
, flag
):
1530 """Unset the given string flag(s) without changing others."""
1531 if 'Status' in self
or 'X-Status' in self
:
1532 self
.set_flags(''.join(set(self
.get_flags()) - set(flag
)))
1534 def _explain_to(self
, message
):
1535 """Copy mbox- or MMDF-specific state to message insofar as possible."""
1536 if isinstance(message
, MaildirMessage
):
1537 flags
= set(self
.get_flags())
1539 message
.set_subdir('cur')
1541 message
.add_flag('F')
1543 message
.add_flag('R')
1545 message
.add_flag('S')
1547 message
.add_flag('T')
1548 del message
['status']
1549 del message
['x-status']
1550 maybe_date
= ' '.join(self
.get_from().split()[-5:])
1552 message
.set_date(calendar
.timegm(time
.strptime(maybe_date
,
1553 '%a %b %d %H:%M:%S %Y')))
1554 except (ValueError, OverflowError):
1556 elif isinstance(message
, _mboxMMDFMessage
):
1557 message
.set_flags(self
.get_flags())
1558 message
.set_from(self
.get_from())
1559 elif isinstance(message
, MHMessage
):
1560 flags
= set(self
.get_flags())
1561 if 'R' not in flags
:
1562 message
.add_sequence('unseen')
1564 message
.add_sequence('replied')
1566 message
.add_sequence('flagged')
1567 del message
['status']
1568 del message
['x-status']
1569 elif isinstance(message
, BabylMessage
):
1570 flags
= set(self
.get_flags())
1571 if 'R' not in flags
:
1572 message
.add_label('unseen')
1574 message
.add_label('deleted')
1576 message
.add_label('answered')
1577 del message
['status']
1578 del message
['x-status']
1579 elif isinstance(message
, Message
):
1582 raise TypeError('Cannot convert to specified type: %s' %
1586 class mboxMessage(_mboxMMDFMessage
):
1587 """Message with mbox-specific properties."""
1590 class MHMessage(Message
):
1591 """Message with MH-specific properties."""
1593 def __init__(self
, message
=None):
1594 """Initialize an MHMessage instance."""
1595 self
._sequences
= []
1596 Message
.__init
__(self
, message
)
1598 def get_sequences(self
):
1599 """Return a list of sequences that include the message."""
1600 return self
._sequences
[:]
1602 def set_sequences(self
, sequences
):
1603 """Set the list of sequences that include the message."""
1604 self
._sequences
= list(sequences
)
1606 def add_sequence(self
, sequence
):
1607 """Add sequence to list of sequences including the message."""
1608 if isinstance(sequence
, str):
1609 if not sequence
in self
._sequences
:
1610 self
._sequences
.append(sequence
)
1612 raise TypeError('sequence must be a string: %s' % type(sequence
))
1614 def remove_sequence(self
, sequence
):
1615 """Remove sequence from the list of sequences including the message."""
1617 self
._sequences
.remove(sequence
)
1621 def _explain_to(self
, message
):
1622 """Copy MH-specific state to message insofar as possible."""
1623 if isinstance(message
, MaildirMessage
):
1624 sequences
= set(self
.get_sequences())
1625 if 'unseen' in sequences
:
1626 message
.set_subdir('cur')
1628 message
.set_subdir('cur')
1629 message
.add_flag('S')
1630 if 'flagged' in sequences
:
1631 message
.add_flag('F')
1632 if 'replied' in sequences
:
1633 message
.add_flag('R')
1634 elif isinstance(message
, _mboxMMDFMessage
):
1635 sequences
= set(self
.get_sequences())
1636 if 'unseen' not in sequences
:
1637 message
.add_flag('RO')
1639 message
.add_flag('O')
1640 if 'flagged' in sequences
:
1641 message
.add_flag('F')
1642 if 'replied' in sequences
:
1643 message
.add_flag('A')
1644 elif isinstance(message
, MHMessage
):
1645 for sequence
in self
.get_sequences():
1646 message
.add_sequence(sequence
)
1647 elif isinstance(message
, BabylMessage
):
1648 sequences
= set(self
.get_sequences())
1649 if 'unseen' in sequences
:
1650 message
.add_label('unseen')
1651 if 'replied' in sequences
:
1652 message
.add_label('answered')
1653 elif isinstance(message
, Message
):
1656 raise TypeError('Cannot convert to specified type: %s' %
1660 class BabylMessage(Message
):
1661 """Message with Babyl-specific properties."""
1663 def __init__(self
, message
=None):
1664 """Initialize an BabylMessage instance."""
1666 self
._visible
= Message()
1667 Message
.__init
__(self
, message
)
1669 def get_labels(self
):
1670 """Return a list of labels on the message."""
1671 return self
._labels
[:]
1673 def set_labels(self
, labels
):
1674 """Set the list of labels on the message."""
1675 self
._labels
= list(labels
)
1677 def add_label(self
, label
):
1678 """Add label to list of labels on the message."""
1679 if isinstance(label
, str):
1680 if label
not in self
._labels
:
1681 self
._labels
.append(label
)
1683 raise TypeError('label must be a string: %s' % type(label
))
1685 def remove_label(self
, label
):
1686 """Remove label from the list of labels on the message."""
1688 self
._labels
.remove(label
)
1692 def get_visible(self
):
1693 """Return a Message representation of visible headers."""
1694 return Message(self
._visible
)
1696 def set_visible(self
, visible
):
1697 """Set the Message representation of visible headers."""
1698 self
._visible
= Message(visible
)
1700 def update_visible(self
):
1701 """Update and/or sensibly generate a set of visible headers."""
1702 for header
in self
._visible
.keys():
1704 self
._visible
.replace_header(header
, self
[header
])
1706 del self
._visible
[header
]
1707 for header
in ('Date', 'From', 'Reply-To', 'To', 'CC', 'Subject'):
1708 if header
in self
and header
not in self
._visible
:
1709 self
._visible
[header
] = self
[header
]
1711 def _explain_to(self
, message
):
1712 """Copy Babyl-specific state to message insofar as possible."""
1713 if isinstance(message
, MaildirMessage
):
1714 labels
= set(self
.get_labels())
1715 if 'unseen' in labels
:
1716 message
.set_subdir('cur')
1718 message
.set_subdir('cur')
1719 message
.add_flag('S')
1720 if 'forwarded' in labels
or 'resent' in labels
:
1721 message
.add_flag('P')
1722 if 'answered' in labels
:
1723 message
.add_flag('R')
1724 if 'deleted' in labels
:
1725 message
.add_flag('T')
1726 elif isinstance(message
, _mboxMMDFMessage
):
1727 labels
= set(self
.get_labels())
1728 if 'unseen' not in labels
:
1729 message
.add_flag('RO')
1731 message
.add_flag('O')
1732 if 'deleted' in labels
:
1733 message
.add_flag('D')
1734 if 'answered' in labels
:
1735 message
.add_flag('A')
1736 elif isinstance(message
, MHMessage
):
1737 labels
= set(self
.get_labels())
1738 if 'unseen' in labels
:
1739 message
.add_sequence('unseen')
1740 if 'answered' in labels
:
1741 message
.add_sequence('replied')
1742 elif isinstance(message
, BabylMessage
):
1743 message
.set_visible(self
.get_visible())
1744 for label
in self
.get_labels():
1745 message
.add_label(label
)
1746 elif isinstance(message
, Message
):
1749 raise TypeError('Cannot convert to specified type: %s' %
1753 class MMDFMessage(_mboxMMDFMessage
):
1754 """Message with MMDF-specific properties."""
1758 """A read-only wrapper of a file."""
1760 def __init__(self
, f
, pos
=None):
1761 """Initialize a _ProxyFile."""
1764 self
._pos
= f
.tell()
1768 def read(self
, size
=None):
1770 return self
._read
(size
, self
._file
.read
)
1772 def readline(self
, size
=None):
1774 return self
._read
(size
, self
._file
.readline
)
1776 def readlines(self
, sizehint
=None):
1777 """Read multiple lines."""
1781 if sizehint
is not None:
1782 sizehint
-= len(line
)
1788 """Iterate over lines."""
1789 return iter(self
.readline
, "")
1792 """Return the position."""
1795 def seek(self
, offset
, whence
=0):
1796 """Change position."""
1798 self
._file
.seek(self
._pos
)
1799 self
._file
.seek(offset
, whence
)
1800 self
._pos
= self
._file
.tell()
1803 """Close the file."""
1806 def _read(self
, size
, read_method
):
1807 """Read size bytes using read_method."""
1810 self
._file
.seek(self
._pos
)
1811 result
= read_method(size
)
1812 self
._pos
= self
._file
.tell()
1816 class _PartialFile(_ProxyFile
):
1817 """A read-only wrapper of part of a file."""
1819 def __init__(self
, f
, start
=None, stop
=None):
1820 """Initialize a _PartialFile."""
1821 _ProxyFile
.__init
__(self
, f
, start
)
1826 """Return the position with respect to start."""
1827 return _ProxyFile
.tell(self
) - self
._start
1829 def seek(self
, offset
, whence
=0):
1830 """Change position, possibly with respect to start or stop."""
1832 self
._pos
= self
._start
1835 self
._pos
= self
._stop
1837 _ProxyFile
.seek(self
, offset
, whence
)
1839 def _read(self
, size
, read_method
):
1840 """Read size bytes using read_method, honoring start and stop."""
1841 remaining
= self
._stop
- self
._pos
1844 if size
is None or size
< 0 or size
> remaining
:
1846 return _ProxyFile
._read
(self
, size
, read_method
)
1849 def _lock_file(f
, dotlock
=True):
1850 """Lock file f using lockf and dot locking."""
1851 dotlock_done
= False
1855 fcntl
.lockf(f
, fcntl
.LOCK_EX | fcntl
.LOCK_NB
)
1857 if e
.errno
in (errno
.EAGAIN
, errno
.EACCES
):
1858 raise ExternalClashError('lockf: lock unavailable: %s' %
1864 pre_lock
= _create_temporary(f
.name
+ '.lock')
1867 if e
.errno
== errno
.EACCES
:
1868 return # Without write access, just skip dotlocking.
1872 if hasattr(os
, 'link'):
1873 os
.link(pre_lock
.name
, f
.name
+ '.lock')
1875 os
.unlink(pre_lock
.name
)
1877 os
.rename(pre_lock
.name
, f
.name
+ '.lock')
1880 if e
.errno
== errno
.EEXIST
or \
1881 (os
.name
== 'os2' and e
.errno
== errno
.EACCES
):
1882 os
.remove(pre_lock
.name
)
1883 raise ExternalClashError('dot lock unavailable: %s' %
1889 fcntl
.lockf(f
, fcntl
.LOCK_UN
)
1891 os
.remove(f
.name
+ '.lock')
1894 def _unlock_file(f
):
1895 """Unlock file f using lockf and dot locking."""
1897 fcntl
.lockf(f
, fcntl
.LOCK_UN
)
1898 if os
.path
.exists(f
.name
+ '.lock'):
1899 os
.remove(f
.name
+ '.lock')
1901 def _create_carefully(path
):
1902 """Create a file if it doesn't exist and open for reading and writing."""
1903 fd
= os
.open(path
, os
.O_CREAT | os
.O_EXCL | os
.O_RDWR
)
1905 return open(path
, 'rb+')
1909 def _create_temporary(path
):
1910 """Create a temp file based on path and open for reading and writing."""
1911 return _create_carefully('%s.%s.%s.%s' % (path
, int(time
.time()),
1912 socket
.gethostname(),
1916 """Ensure changes to file f are physically on disk."""
1918 if hasattr(os
, 'fsync'):
1919 os
.fsync(f
.fileno())
1922 """Close file f, ensuring all changes are physically on disk."""
1926 ## Start: classes from the original module (for backward compatibility).
1928 # Note that the Maildir class, whose name is unchanged, itself offers a next()
1929 # method for backward compatibility.
1933 def __init__(self
, fp
, factory
=rfc822
.Message
):
1936 self
.factory
= factory
1939 return iter(self
.next
, None)
1943 self
.fp
.seek(self
.seekp
)
1945 self
._search
_start
()
1947 self
.seekp
= self
.fp
.tell()
1949 start
= self
.fp
.tell()
1951 self
.seekp
= stop
= self
.fp
.tell()
1954 return self
.factory(_PartialFile(self
.fp
, start
, stop
))
1956 # Recommended to use PortableUnixMailbox instead!
1957 class UnixMailbox(_Mailbox
):
1959 def _search_start(self
):
1961 pos
= self
.fp
.tell()
1962 line
= self
.fp
.readline()
1965 if line
[:5] == 'From ' and self
._isrealfromline
(line
):
1969 def _search_end(self
):
1970 self
.fp
.readline() # Throw away header line
1972 pos
= self
.fp
.tell()
1973 line
= self
.fp
.readline()
1976 if line
[:5] == 'From ' and self
._isrealfromline
(line
):
1980 # An overridable mechanism to test for From-line-ness. You can either
1981 # specify a different regular expression or define a whole new
1982 # _isrealfromline() method. Note that this only gets called for lines
1983 # starting with the 5 characters "From ".
1986 #http://home.netscape.com/eng/mozilla/2.0/relnotes/demo/content-length.html
1987 # the only portable, reliable way to find message delimiters in a BSD (i.e
1988 # Unix mailbox) style folder is to search for "\n\nFrom .*\n", or at the
1989 # beginning of the file, "^From .*\n". While _fromlinepattern below seems
1990 # like a good idea, in practice, there are too many variations for more
1991 # strict parsing of the line to be completely accurate.
1993 # _strict_isrealfromline() is the old version which tries to do stricter
1994 # parsing of the From_ line. _portable_isrealfromline() simply returns
1995 # true, since it's never called if the line doesn't already start with
1998 # This algorithm, and the way it interacts with _search_start() and
1999 # _search_end() may not be completely correct, because it doesn't check
2000 # that the two characters preceding "From " are \n\n or the beginning of
2001 # the file. Fixing this would require a more extensive rewrite than is
2002 # necessary. For convenience, we've added a PortableUnixMailbox class
2003 # which does no checking of the format of the 'From' line.
2005 _fromlinepattern
= (r
"From \s*[^\s]+\s+\w\w\w\s+\w\w\w\s+\d?\d\s+"
2006 r
"\d?\d:\d\d(:\d\d)?(\s+[^\s]+)?\s+\d\d\d\d\s*"
2011 def _strict_isrealfromline(self
, line
):
2012 if not self
._regexp
:
2014 self
._regexp
= re
.compile(self
._fromlinepattern
)
2015 return self
._regexp
.match(line
)
2017 def _portable_isrealfromline(self
, line
):
2020 _isrealfromline
= _strict_isrealfromline
2023 class PortableUnixMailbox(UnixMailbox
):
2024 _isrealfromline
= UnixMailbox
._portable
_isrealfromline
2027 class MmdfMailbox(_Mailbox
):
2029 def _search_start(self
):
2031 line
= self
.fp
.readline()
2034 if line
[:5] == '\001\001\001\001\n':
2037 def _search_end(self
):
2039 pos
= self
.fp
.tell()
2040 line
= self
.fp
.readline()
2043 if line
== '\001\001\001\001\n':
2050 def __init__(self
, dirname
, factory
=rfc822
.Message
):
2052 pat
= re
.compile('^[1-9][0-9]*$')
2053 self
.dirname
= dirname
2054 # the three following lines could be combined into:
2055 # list = map(long, filter(pat.match, os.listdir(self.dirname)))
2056 list = os
.listdir(self
.dirname
)
2057 list = filter(pat
.match
, list)
2058 list = map(long, list)
2060 # This only works in Python 1.6 or later;
2061 # before that str() added 'L':
2062 self
.boxes
= map(str, list)
2063 self
.boxes
.reverse()
2064 self
.factory
= factory
2067 return iter(self
.next
, None)
2072 fn
= self
.boxes
.pop()
2073 fp
= open(os
.path
.join(self
.dirname
, fn
))
2074 msg
= self
.factory(fp
)
2077 except (AttributeError, TypeError):
2082 class BabylMailbox(_Mailbox
):
2084 def _search_start(self
):
2086 line
= self
.fp
.readline()
2089 if line
== '*** EOOH ***\n':
2092 def _search_end(self
):
2094 pos
= self
.fp
.tell()
2095 line
= self
.fp
.readline()
2098 if line
== '\037\014\n' or line
== '\037':
2102 ## End: classes from the original module (for backward compatibility).
2105 class Error(Exception):
2106 """Raised for module-specific errors."""
2108 class NoSuchMailboxError(Error
):
2109 """The specified mailbox does not exist and won't be created."""
2111 class NotEmptyError(Error
):
2112 """The specified mailbox is not empty and deletion was requested."""
2114 class ExternalClashError(Error
):
2115 """Another process caused an action to fail."""
2117 class FormatError(Error
):
2118 """A file appears to have an invalid format."""