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
)
240 self
._last
_read
= None # Records last time we read cur/new
242 def add(self
, message
):
243 """Add message and return assigned key."""
244 tmp_file
= self
._create
_tmp
()
246 self
._dump
_message
(message
, tmp_file
)
248 _sync_close(tmp_file
)
249 if isinstance(message
, MaildirMessage
):
250 subdir
= message
.get_subdir()
251 suffix
= self
.colon
+ message
.get_info()
252 if suffix
== self
.colon
:
257 uniq
= os
.path
.basename(tmp_file
.name
).split(self
.colon
)[0]
258 dest
= os
.path
.join(self
._path
, subdir
, uniq
+ suffix
)
260 if hasattr(os
, 'link'):
261 os
.link(tmp_file
.name
, dest
)
262 os
.remove(tmp_file
.name
)
264 os
.rename(tmp_file
.name
, dest
)
266 os
.remove(tmp_file
.name
)
267 if e
.errno
== errno
.EEXIST
:
268 raise ExternalClashError('Name clash with existing message: %s'
272 if isinstance(message
, MaildirMessage
):
273 os
.utime(dest
, (os
.path
.getatime(dest
), message
.get_date()))
276 def remove(self
, key
):
277 """Remove the keyed message; raise KeyError if it doesn't exist."""
278 os
.remove(os
.path
.join(self
._path
, self
._lookup
(key
)))
280 def discard(self
, key
):
281 """If the keyed message exists, remove it."""
282 # This overrides an inapplicable implementation in the superclass.
288 if e
.errno
!= errno
.ENOENT
:
291 def __setitem__(self
, key
, message
):
292 """Replace the keyed message; raise KeyError if it doesn't exist."""
293 old_subpath
= self
._lookup
(key
)
294 temp_key
= self
.add(message
)
295 temp_subpath
= self
._lookup
(temp_key
)
296 if isinstance(message
, MaildirMessage
):
297 # temp's subdir and suffix were specified by message.
298 dominant_subpath
= temp_subpath
300 # temp's subdir and suffix were defaults from add().
301 dominant_subpath
= old_subpath
302 subdir
= os
.path
.dirname(dominant_subpath
)
303 if self
.colon
in dominant_subpath
:
304 suffix
= self
.colon
+ dominant_subpath
.split(self
.colon
)[-1]
308 new_path
= os
.path
.join(self
._path
, subdir
, key
+ suffix
)
309 os
.rename(os
.path
.join(self
._path
, temp_subpath
), new_path
)
310 if isinstance(message
, MaildirMessage
):
311 os
.utime(new_path
, (os
.path
.getatime(new_path
),
314 def get_message(self
, key
):
315 """Return a Message representation or raise a KeyError."""
316 subpath
= self
._lookup
(key
)
317 f
= open(os
.path
.join(self
._path
, subpath
), 'r')
320 msg
= self
._factory
(f
)
322 msg
= MaildirMessage(f
)
325 subdir
, name
= os
.path
.split(subpath
)
326 msg
.set_subdir(subdir
)
327 if self
.colon
in name
:
328 msg
.set_info(name
.split(self
.colon
)[-1])
329 msg
.set_date(os
.path
.getmtime(os
.path
.join(self
._path
, subpath
)))
332 def get_string(self
, key
):
333 """Return a string representation or raise a KeyError."""
334 f
= open(os
.path
.join(self
._path
, self
._lookup
(key
)), 'r')
340 def get_file(self
, key
):
341 """Return a file-like representation or raise a KeyError."""
342 f
= open(os
.path
.join(self
._path
, self
._lookup
(key
)), 'rb')
346 """Return an iterator over keys."""
348 for key
in self
._toc
:
355 def has_key(self
, key
):
356 """Return True if the keyed message exists, False otherwise."""
358 return key
in self
._toc
361 """Return a count of messages in the mailbox."""
363 return len(self
._toc
)
366 """Write any pending changes to disk."""
367 return # Maildir changes are always written immediately.
370 """Lock the mailbox."""
374 """Unlock the mailbox if it is locked."""
378 """Flush and close the mailbox."""
381 def list_folders(self
):
382 """Return a list of folder names."""
384 for entry
in os
.listdir(self
._path
):
385 if len(entry
) > 1 and entry
[0] == '.' and \
386 os
.path
.isdir(os
.path
.join(self
._path
, entry
)):
387 result
.append(entry
[1:])
390 def get_folder(self
, folder
):
391 """Return a Maildir instance for the named folder."""
392 return Maildir(os
.path
.join(self
._path
, '.' + folder
),
393 factory
=self
._factory
,
396 def add_folder(self
, folder
):
397 """Create a folder and return a Maildir instance representing it."""
398 path
= os
.path
.join(self
._path
, '.' + folder
)
399 result
= Maildir(path
, factory
=self
._factory
)
400 maildirfolder_path
= os
.path
.join(path
, 'maildirfolder')
401 if not os
.path
.exists(maildirfolder_path
):
402 os
.close(os
.open(maildirfolder_path
, os
.O_CREAT | os
.O_WRONLY
,
406 def remove_folder(self
, folder
):
407 """Delete the named folder, which must be empty."""
408 path
= os
.path
.join(self
._path
, '.' + folder
)
409 for entry
in os
.listdir(os
.path
.join(path
, 'new')) + \
410 os
.listdir(os
.path
.join(path
, 'cur')):
411 if len(entry
) < 1 or entry
[0] != '.':
412 raise NotEmptyError('Folder contains message(s): %s' % folder
)
413 for entry
in os
.listdir(path
):
414 if entry
!= 'new' and entry
!= 'cur' and entry
!= 'tmp' and \
415 os
.path
.isdir(os
.path
.join(path
, entry
)):
416 raise NotEmptyError("Folder contains subdirectory '%s': %s" %
418 for root
, dirs
, files
in os
.walk(path
, topdown
=False):
420 os
.remove(os
.path
.join(root
, entry
))
422 os
.rmdir(os
.path
.join(root
, entry
))
426 """Delete old files in "tmp"."""
428 for entry
in os
.listdir(os
.path
.join(self
._path
, 'tmp')):
429 path
= os
.path
.join(self
._path
, 'tmp', entry
)
430 if now
- os
.path
.getatime(path
) > 129600: # 60 * 60 * 36
433 _count
= 1 # This is used to generate unique file names.
435 def _create_tmp(self
):
436 """Create a file in the tmp subdirectory and open and return it."""
438 hostname
= socket
.gethostname()
440 hostname
= hostname
.replace('/', r
'\057')
442 hostname
= hostname
.replace(':', r
'\072')
443 uniq
= "%s.M%sP%sQ%s.%s" % (int(now
), int(now
% 1 * 1e6
), os
.getpid(),
444 Maildir
._count
, hostname
)
445 path
= os
.path
.join(self
._path
, 'tmp', uniq
)
449 if e
.errno
== errno
.ENOENT
:
452 return _create_carefully(path
)
454 if e
.errno
!= errno
.EEXIST
:
459 # Fall through to here if stat succeeded or open raised EEXIST.
460 raise ExternalClashError('Name clash prevented file creation: %s' %
464 """Update table of contents mapping."""
465 new_mtime
= os
.path
.getmtime(os
.path
.join(self
._path
, 'new'))
466 cur_mtime
= os
.path
.getmtime(os
.path
.join(self
._path
, 'cur'))
468 if (self
._last
_read
is not None and
469 new_mtime
<= self
._last
_read
and cur_mtime
<= self
._last
_read
):
473 def update_dir (subdir
):
474 path
= os
.path
.join(self
._path
, subdir
)
475 for entry
in os
.listdir(path
):
476 p
= os
.path
.join(path
, entry
)
479 uniq
= entry
.split(self
.colon
)[0]
480 self
._toc
[uniq
] = os
.path
.join(subdir
, entry
)
485 # We record the current time - 1sec so that, if _refresh() is called
486 # again in the same second, we will always re-read the mailbox
487 # just in case it's been modified. (os.path.mtime() only has
488 # 1sec resolution.) This results in a few unnecessary re-reads
489 # when _refresh() is called multiple times in the same second,
490 # but once the clock ticks over, we will only re-read as needed.
491 now
= int(time
.time() - 1)
492 self
._last
_read
= time
.time() - 1
494 def _lookup(self
, key
):
495 """Use TOC to return subpath for given key, or raise a KeyError."""
497 if os
.path
.exists(os
.path
.join(self
._path
, self
._toc
[key
])):
498 return self
._toc
[key
]
503 return self
._toc
[key
]
505 raise KeyError('No message with key: %s' % key
)
507 # This method is for backward compatibility only.
509 """Return the next message in a one-time iteration."""
510 if not hasattr(self
, '_onetime_keys'):
511 self
._onetime
_keys
= self
.iterkeys()
514 return self
[self
._onetime
_keys
.next()]
515 except StopIteration:
521 class _singlefileMailbox(Mailbox
):
522 """A single-file mailbox."""
524 def __init__(self
, path
, factory
=None, create
=True):
525 """Initialize a single-file mailbox."""
526 Mailbox
.__init
__(self
, path
, factory
, create
)
528 f
= open(self
._path
, 'rb+')
530 if e
.errno
== errno
.ENOENT
:
532 f
= open(self
._path
, 'wb+')
534 raise NoSuchMailboxError(self
._path
)
535 elif e
.errno
== errno
.EACCES
:
536 f
= open(self
._path
, 'rb')
542 self
._pending
= False # No changes require rewriting the file.
544 self
._file
_length
= None # Used to record mailbox size
546 def add(self
, message
):
547 """Add message and return assigned key."""
549 self
._toc
[self
._next
_key
] = self
._append
_message
(message
)
552 return self
._next
_key
- 1
554 def remove(self
, key
):
555 """Remove the keyed message; raise KeyError if it doesn't exist."""
560 def __setitem__(self
, key
, message
):
561 """Replace the keyed message; raise KeyError if it doesn't exist."""
563 self
._toc
[key
] = self
._append
_message
(message
)
567 """Return an iterator over keys."""
569 for key
in self
._toc
.keys():
572 def has_key(self
, key
):
573 """Return True if the keyed message exists, False otherwise."""
575 return key
in self
._toc
578 """Return a count of messages in the mailbox."""
580 return len(self
._toc
)
583 """Lock the mailbox."""
585 _lock_file(self
._file
)
589 """Unlock the mailbox if it is locked."""
591 _unlock_file(self
._file
)
595 """Write any pending changes to disk."""
596 if not self
._pending
:
599 # In order to be writing anything out at all, self._toc must
600 # already have been generated (and presumably has been modified
601 # by adding or deleting an item).
602 assert self
._toc
is not None
604 # Check length of self._file; if it's changed, some other process
605 # has modified the mailbox since we scanned it.
606 self
._file
.seek(0, 2)
607 cur_len
= self
._file
.tell()
608 if cur_len
!= self
._file
_length
:
609 raise ExternalClashError('Size of mailbox file changed '
610 '(expected %i, found %i)' %
611 (self
._file
_length
, cur_len
))
613 new_file
= _create_temporary(self
._path
)
616 self
._pre
_mailbox
_hook
(new_file
)
617 for key
in sorted(self
._toc
.keys()):
618 start
, stop
= self
._toc
[key
]
619 self
._file
.seek(start
)
620 self
._pre
_message
_hook
(new_file
)
621 new_start
= new_file
.tell()
623 buffer = self
._file
.read(min(4096,
624 stop
- self
._file
.tell()))
627 new_file
.write(buffer)
628 new_toc
[key
] = (new_start
, new_file
.tell())
629 self
._post
_message
_hook
(new_file
)
632 os
.remove(new_file
.name
)
634 _sync_close(new_file
)
635 # self._file is about to get replaced, so no need to sync.
638 os
.rename(new_file
.name
, self
._path
)
640 if e
.errno
== errno
.EEXIST
or \
641 (os
.name
== 'os2' and e
.errno
== errno
.EACCES
):
642 os
.remove(self
._path
)
643 os
.rename(new_file
.name
, self
._path
)
646 self
._file
= open(self
._path
, 'rb+')
648 self
._pending
= False
650 _lock_file(self
._file
, dotlock
=False)
652 def _pre_mailbox_hook(self
, f
):
653 """Called before writing the mailbox to file f."""
656 def _pre_message_hook(self
, f
):
657 """Called before writing each message to file f."""
660 def _post_message_hook(self
, f
):
661 """Called after writing each message to file f."""
665 """Flush and close the mailbox."""
669 self
._file
.close() # Sync has been done by self.flush() above.
671 def _lookup(self
, key
=None):
672 """Return (start, stop) or raise KeyError."""
673 if self
._toc
is None:
677 return self
._toc
[key
]
679 raise KeyError('No message with key: %s' % key
)
681 def _append_message(self
, message
):
682 """Append message to mailbox and return (start, stop) offsets."""
683 self
._file
.seek(0, 2)
684 self
._pre
_message
_hook
(self
._file
)
685 offsets
= self
._install
_message
(message
)
686 self
._post
_message
_hook
(self
._file
)
688 self
._file
_length
= self
._file
.tell() # Record current length of mailbox
693 class _mboxMMDF(_singlefileMailbox
):
694 """An mbox or MMDF mailbox."""
698 def get_message(self
, key
):
699 """Return a Message representation or raise a KeyError."""
700 start
, stop
= self
._lookup
(key
)
701 self
._file
.seek(start
)
702 from_line
= self
._file
.readline().replace(os
.linesep
, '')
703 string
= self
._file
.read(stop
- self
._file
.tell())
704 msg
= self
._message
_factory
(string
.replace(os
.linesep
, '\n'))
705 msg
.set_from(from_line
[5:])
708 def get_string(self
, key
, from_
=False):
709 """Return a string representation or raise a KeyError."""
710 start
, stop
= self
._lookup
(key
)
711 self
._file
.seek(start
)
713 self
._file
.readline()
714 string
= self
._file
.read(stop
- self
._file
.tell())
715 return string
.replace(os
.linesep
, '\n')
717 def get_file(self
, key
, from_
=False):
718 """Return a file-like representation or raise a KeyError."""
719 start
, stop
= self
._lookup
(key
)
720 self
._file
.seek(start
)
722 self
._file
.readline()
723 return _PartialFile(self
._file
, self
._file
.tell(), stop
)
725 def _install_message(self
, message
):
726 """Format a message and blindly write to self._file."""
728 if isinstance(message
, str) and message
.startswith('From '):
729 newline
= message
.find('\n')
731 from_line
= message
[:newline
]
732 message
= message
[newline
+ 1:]
736 elif isinstance(message
, _mboxMMDFMessage
):
737 from_line
= 'From ' + message
.get_from()
738 elif isinstance(message
, email
.message
.Message
):
739 from_line
= message
.get_unixfrom() # May be None.
740 if from_line
is None:
741 from_line
= 'From MAILER-DAEMON %s' % time
.asctime(time
.gmtime())
742 start
= self
._file
.tell()
743 self
._file
.write(from_line
+ os
.linesep
)
744 self
._dump
_message
(message
, self
._file
, self
._mangle
_from
_)
745 stop
= self
._file
.tell()
749 class mbox(_mboxMMDF
):
750 """A classic mbox mailbox."""
754 def __init__(self
, path
, factory
=None, create
=True):
755 """Initialize an mbox mailbox."""
756 self
._message
_factory
= mboxMessage
757 _mboxMMDF
.__init
__(self
, path
, factory
, create
)
759 def _pre_message_hook(self
, f
):
760 """Called before writing each message to file f."""
764 def _generate_toc(self
):
765 """Generate key-to-(start, stop) table of contents."""
766 starts
, stops
= [], []
769 line_pos
= self
._file
.tell()
770 line
= self
._file
.readline()
771 if line
.startswith('From '):
772 if len(stops
) < len(starts
):
773 stops
.append(line_pos
- len(os
.linesep
))
774 starts
.append(line_pos
)
776 stops
.append(line_pos
)
778 self
._toc
= dict(enumerate(zip(starts
, stops
)))
779 self
._next
_key
= len(self
._toc
)
780 self
._file
_length
= self
._file
.tell()
783 class MMDF(_mboxMMDF
):
784 """An MMDF mailbox."""
786 def __init__(self
, path
, factory
=None, create
=True):
787 """Initialize an MMDF mailbox."""
788 self
._message
_factory
= MMDFMessage
789 _mboxMMDF
.__init
__(self
, path
, factory
, create
)
791 def _pre_message_hook(self
, f
):
792 """Called before writing each message to file f."""
793 f
.write('\001\001\001\001' + os
.linesep
)
795 def _post_message_hook(self
, f
):
796 """Called after writing each message to file f."""
797 f
.write(os
.linesep
+ '\001\001\001\001' + os
.linesep
)
799 def _generate_toc(self
):
800 """Generate key-to-(start, stop) table of contents."""
801 starts
, stops
= [], []
806 line
= self
._file
.readline()
807 next_pos
= self
._file
.tell()
808 if line
.startswith('\001\001\001\001' + os
.linesep
):
809 starts
.append(next_pos
)
812 line
= self
._file
.readline()
813 next_pos
= self
._file
.tell()
814 if line
== '\001\001\001\001' + os
.linesep
:
815 stops
.append(line_pos
- len(os
.linesep
))
818 stops
.append(line_pos
)
822 self
._toc
= dict(enumerate(zip(starts
, stops
)))
823 self
._next
_key
= len(self
._toc
)
824 self
._file
.seek(0, 2)
825 self
._file
_length
= self
._file
.tell()
831 def __init__(self
, path
, factory
=None, create
=True):
832 """Initialize an MH instance."""
833 Mailbox
.__init
__(self
, path
, factory
, create
)
834 if not os
.path
.exists(self
._path
):
836 os
.mkdir(self
._path
, 0700)
837 os
.close(os
.open(os
.path
.join(self
._path
, '.mh_sequences'),
838 os
.O_CREAT | os
.O_EXCL | os
.O_WRONLY
, 0600))
840 raise NoSuchMailboxError(self
._path
)
843 def add(self
, message
):
844 """Add message and return assigned key."""
849 new_key
= max(keys
) + 1
850 new_path
= os
.path
.join(self
._path
, str(new_key
))
851 f
= _create_carefully(new_path
)
856 self
._dump
_message
(message
, f
)
857 if isinstance(message
, MHMessage
):
858 self
._dump
_sequences
(message
, new_key
)
866 def remove(self
, key
):
867 """Remove the keyed message; raise KeyError if it doesn't exist."""
868 path
= os
.path
.join(self
._path
, str(key
))
870 f
= open(path
, 'rb+')
872 if e
.errno
== errno
.ENOENT
:
873 raise KeyError('No message with key: %s' % key
)
881 os
.remove(os
.path
.join(self
._path
, str(key
)))
888 def __setitem__(self
, key
, message
):
889 """Replace the keyed message; raise KeyError if it doesn't exist."""
890 path
= os
.path
.join(self
._path
, str(key
))
892 f
= open(path
, 'rb+')
894 if e
.errno
== errno
.ENOENT
:
895 raise KeyError('No message with key: %s' % key
)
902 os
.close(os
.open(path
, os
.O_WRONLY | os
.O_TRUNC
))
903 self
._dump
_message
(message
, f
)
904 if isinstance(message
, MHMessage
):
905 self
._dump
_sequences
(message
, key
)
912 def get_message(self
, key
):
913 """Return a Message representation or raise a KeyError."""
916 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+')
918 f
= open(os
.path
.join(self
._path
, str(key
)), 'r')
920 if e
.errno
== errno
.ENOENT
:
921 raise KeyError('No message with key: %s' % key
)
934 for name
, key_list
in self
.get_sequences().iteritems():
936 msg
.add_sequence(name
)
939 def get_string(self
, key
):
940 """Return a string representation or raise a KeyError."""
943 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+')
945 f
= open(os
.path
.join(self
._path
, str(key
)), 'r')
947 if e
.errno
== errno
.ENOENT
:
948 raise KeyError('No message with key: %s' % key
)
962 def get_file(self
, key
):
963 """Return a file-like representation or raise a KeyError."""
965 f
= open(os
.path
.join(self
._path
, str(key
)), 'rb')
967 if e
.errno
== errno
.ENOENT
:
968 raise KeyError('No message with key: %s' % key
)
974 """Return an iterator over keys."""
975 return iter(sorted(int(entry
) for entry
in os
.listdir(self
._path
)
978 def has_key(self
, key
):
979 """Return True if the keyed message exists, False otherwise."""
980 return os
.path
.exists(os
.path
.join(self
._path
, str(key
)))
983 """Return a count of messages in the mailbox."""
984 return len(list(self
.iterkeys()))
987 """Lock the mailbox."""
989 self
._file
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'rb+')
990 _lock_file(self
._file
)
994 """Unlock the mailbox if it is locked."""
996 _unlock_file(self
._file
)
997 _sync_close(self
._file
)
1002 """Write any pending changes to the disk."""
1006 """Flush and close the mailbox."""
1010 def list_folders(self
):
1011 """Return a list of folder names."""
1013 for entry
in os
.listdir(self
._path
):
1014 if os
.path
.isdir(os
.path
.join(self
._path
, entry
)):
1015 result
.append(entry
)
1018 def get_folder(self
, folder
):
1019 """Return an MH instance for the named folder."""
1020 return MH(os
.path
.join(self
._path
, folder
),
1021 factory
=self
._factory
, create
=False)
1023 def add_folder(self
, folder
):
1024 """Create a folder and return an MH instance representing it."""
1025 return MH(os
.path
.join(self
._path
, folder
),
1026 factory
=self
._factory
)
1028 def remove_folder(self
, folder
):
1029 """Delete the named folder, which must be empty."""
1030 path
= os
.path
.join(self
._path
, folder
)
1031 entries
= os
.listdir(path
)
1032 if entries
== ['.mh_sequences']:
1033 os
.remove(os
.path
.join(path
, '.mh_sequences'))
1037 raise NotEmptyError('Folder not empty: %s' % self
._path
)
1040 def get_sequences(self
):
1041 """Return a name-to-key-list dictionary to define each sequence."""
1043 f
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'r')
1045 all_keys
= set(self
.keys())
1048 name
, contents
= line
.split(':')
1050 for spec
in contents
.split():
1054 start
, stop
= (int(x
) for x
in spec
.split('-'))
1055 keys
.update(range(start
, stop
+ 1))
1056 results
[name
] = [key
for key
in sorted(keys
) \
1058 if len(results
[name
]) == 0:
1061 raise FormatError('Invalid sequence specification: %s' %
1067 def set_sequences(self
, sequences
):
1068 """Set sequences using the given name-to-key-list dictionary."""
1069 f
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'r+')
1071 os
.close(os
.open(f
.name
, os
.O_WRONLY | os
.O_TRUNC
))
1072 for name
, keys
in sequences
.iteritems():
1075 f
.write('%s:' % name
)
1078 for key
in sorted(set(keys
)):
1085 f
.write('%s %s' % (prev
, key
))
1087 f
.write(' %s' % key
)
1090 f
.write(str(prev
) + '\n')
1097 """Re-name messages to eliminate numbering gaps. Invalidates keys."""
1098 sequences
= self
.get_sequences()
1101 for key
in self
.iterkeys():
1103 changes
.append((key
, prev
+ 1))
1104 if hasattr(os
, 'link'):
1105 os
.link(os
.path
.join(self
._path
, str(key
)),
1106 os
.path
.join(self
._path
, str(prev
+ 1)))
1107 os
.unlink(os
.path
.join(self
._path
, str(key
)))
1109 os
.rename(os
.path
.join(self
._path
, str(key
)),
1110 os
.path
.join(self
._path
, str(prev
+ 1)))
1112 self
._next
_key
= prev
+ 1
1113 if len(changes
) == 0:
1115 for name
, key_list
in sequences
.items():
1116 for old
, new
in changes
:
1118 key_list
[key_list
.index(old
)] = new
1119 self
.set_sequences(sequences
)
1121 def _dump_sequences(self
, message
, key
):
1122 """Inspect a new MHMessage and update sequences appropriately."""
1123 pending_sequences
= message
.get_sequences()
1124 all_sequences
= self
.get_sequences()
1125 for name
, key_list
in all_sequences
.iteritems():
1126 if name
in pending_sequences
:
1127 key_list
.append(key
)
1128 elif key
in key_list
:
1129 del key_list
[key_list
.index(key
)]
1130 for sequence
in pending_sequences
:
1131 if sequence
not in all_sequences
:
1132 all_sequences
[sequence
] = [key
]
1133 self
.set_sequences(all_sequences
)
1136 class Babyl(_singlefileMailbox
):
1137 """An Rmail-style Babyl mailbox."""
1139 _special_labels
= frozenset(('unseen', 'deleted', 'filed', 'answered',
1140 'forwarded', 'edited', 'resent'))
1142 def __init__(self
, path
, factory
=None, create
=True):
1143 """Initialize a Babyl mailbox."""
1144 _singlefileMailbox
.__init
__(self
, path
, factory
, create
)
1147 def add(self
, message
):
1148 """Add message and return assigned key."""
1149 key
= _singlefileMailbox
.add(self
, message
)
1150 if isinstance(message
, BabylMessage
):
1151 self
._labels
[key
] = message
.get_labels()
1154 def remove(self
, key
):
1155 """Remove the keyed message; raise KeyError if it doesn't exist."""
1156 _singlefileMailbox
.remove(self
, key
)
1157 if key
in self
._labels
:
1158 del self
._labels
[key
]
1160 def __setitem__(self
, key
, message
):
1161 """Replace the keyed message; raise KeyError if it doesn't exist."""
1162 _singlefileMailbox
.__setitem
__(self
, key
, message
)
1163 if isinstance(message
, BabylMessage
):
1164 self
._labels
[key
] = message
.get_labels()
1166 def get_message(self
, key
):
1167 """Return a Message representation or raise a KeyError."""
1168 start
, stop
= self
._lookup
(key
)
1169 self
._file
.seek(start
)
1170 self
._file
.readline() # Skip '1,' line specifying labels.
1171 original_headers
= StringIO
.StringIO()
1173 line
= self
._file
.readline()
1174 if line
== '*** EOOH ***' + os
.linesep
or line
== '':
1176 original_headers
.write(line
.replace(os
.linesep
, '\n'))
1177 visible_headers
= StringIO
.StringIO()
1179 line
= self
._file
.readline()
1180 if line
== os
.linesep
or line
== '':
1182 visible_headers
.write(line
.replace(os
.linesep
, '\n'))
1183 body
= self
._file
.read(stop
- self
._file
.tell()).replace(os
.linesep
,
1185 msg
= BabylMessage(original_headers
.getvalue() + body
)
1186 msg
.set_visible(visible_headers
.getvalue())
1187 if key
in self
._labels
:
1188 msg
.set_labels(self
._labels
[key
])
1191 def get_string(self
, key
):
1192 """Return a string representation or raise a KeyError."""
1193 start
, stop
= self
._lookup
(key
)
1194 self
._file
.seek(start
)
1195 self
._file
.readline() # Skip '1,' line specifying labels.
1196 original_headers
= StringIO
.StringIO()
1198 line
= self
._file
.readline()
1199 if line
== '*** EOOH ***' + os
.linesep
or line
== '':
1201 original_headers
.write(line
.replace(os
.linesep
, '\n'))
1203 line
= self
._file
.readline()
1204 if line
== os
.linesep
or line
== '':
1206 return original_headers
.getvalue() + \
1207 self
._file
.read(stop
- self
._file
.tell()).replace(os
.linesep
,
1210 def get_file(self
, key
):
1211 """Return a file-like representation or raise a KeyError."""
1212 return StringIO
.StringIO(self
.get_string(key
).replace('\n',
1215 def get_labels(self
):
1216 """Return a list of user-defined labels in the mailbox."""
1219 for label_list
in self
._labels
.values():
1220 labels
.update(label_list
)
1221 labels
.difference_update(self
._special
_labels
)
1224 def _generate_toc(self
):
1225 """Generate key-to-(start, stop) table of contents."""
1226 starts
, stops
= [], []
1232 line
= self
._file
.readline()
1233 next_pos
= self
._file
.tell()
1234 if line
== '\037\014' + os
.linesep
:
1235 if len(stops
) < len(starts
):
1236 stops
.append(line_pos
- len(os
.linesep
))
1237 starts
.append(next_pos
)
1238 labels
= [label
.strip() for label
1239 in self
._file
.readline()[1:].split(',')
1240 if label
.strip() != '']
1241 label_lists
.append(labels
)
1242 elif line
== '\037' or line
== '\037' + os
.linesep
:
1243 if len(stops
) < len(starts
):
1244 stops
.append(line_pos
- len(os
.linesep
))
1246 stops
.append(line_pos
- len(os
.linesep
))
1248 self
._toc
= dict(enumerate(zip(starts
, stops
)))
1249 self
._labels
= dict(enumerate(label_lists
))
1250 self
._next
_key
= len(self
._toc
)
1251 self
._file
.seek(0, 2)
1252 self
._file
_length
= self
._file
.tell()
1254 def _pre_mailbox_hook(self
, f
):
1255 """Called before writing the mailbox to file f."""
1256 f
.write('BABYL OPTIONS:%sVersion: 5%sLabels:%s%s\037' %
1257 (os
.linesep
, os
.linesep
, ','.join(self
.get_labels()),
1260 def _pre_message_hook(self
, f
):
1261 """Called before writing each message to file f."""
1262 f
.write('\014' + os
.linesep
)
1264 def _post_message_hook(self
, f
):
1265 """Called after writing each message to file f."""
1266 f
.write(os
.linesep
+ '\037')
1268 def _install_message(self
, message
):
1269 """Write message contents and return (start, stop)."""
1270 start
= self
._file
.tell()
1271 if isinstance(message
, BabylMessage
):
1274 for label
in message
.get_labels():
1275 if label
in self
._special
_labels
:
1276 special_labels
.append(label
)
1278 labels
.append(label
)
1279 self
._file
.write('1')
1280 for label
in special_labels
:
1281 self
._file
.write(', ' + label
)
1282 self
._file
.write(',,')
1283 for label
in labels
:
1284 self
._file
.write(' ' + label
+ ',')
1285 self
._file
.write(os
.linesep
)
1287 self
._file
.write('1,,' + os
.linesep
)
1288 if isinstance(message
, email
.message
.Message
):
1289 orig_buffer
= StringIO
.StringIO()
1290 orig_generator
= email
.generator
.Generator(orig_buffer
, False, 0)
1291 orig_generator
.flatten(message
)
1294 line
= orig_buffer
.readline()
1295 self
._file
.write(line
.replace('\n', os
.linesep
))
1296 if line
== '\n' or line
== '':
1298 self
._file
.write('*** EOOH ***' + os
.linesep
)
1299 if isinstance(message
, BabylMessage
):
1300 vis_buffer
= StringIO
.StringIO()
1301 vis_generator
= email
.generator
.Generator(vis_buffer
, False, 0)
1302 vis_generator
.flatten(message
.get_visible())
1304 line
= vis_buffer
.readline()
1305 self
._file
.write(line
.replace('\n', os
.linesep
))
1306 if line
== '\n' or line
== '':
1311 line
= orig_buffer
.readline()
1312 self
._file
.write(line
.replace('\n', os
.linesep
))
1313 if line
== '\n' or line
== '':
1316 buffer = orig_buffer
.read(4096) # Buffer size is arbitrary.
1319 self
._file
.write(buffer.replace('\n', os
.linesep
))
1320 elif isinstance(message
, str):
1321 body_start
= message
.find('\n\n') + 2
1322 if body_start
- 2 != -1:
1323 self
._file
.write(message
[:body_start
].replace('\n',
1325 self
._file
.write('*** EOOH ***' + os
.linesep
)
1326 self
._file
.write(message
[:body_start
].replace('\n',
1328 self
._file
.write(message
[body_start
:].replace('\n',
1331 self
._file
.write('*** EOOH ***' + os
.linesep
+ os
.linesep
)
1332 self
._file
.write(message
.replace('\n', os
.linesep
))
1333 elif hasattr(message
, 'readline'):
1334 original_pos
= message
.tell()
1337 line
= message
.readline()
1338 self
._file
.write(line
.replace('\n', os
.linesep
))
1339 if line
== '\n' or line
== '':
1340 self
._file
.write('*** EOOH ***' + os
.linesep
)
1343 message
.seek(original_pos
)
1347 buffer = message
.read(4096) # Buffer size is arbitrary.
1350 self
._file
.write(buffer.replace('\n', os
.linesep
))
1352 raise TypeError('Invalid message type: %s' % type(message
))
1353 stop
= self
._file
.tell()
1354 return (start
, stop
)
1357 class Message(email
.message
.Message
):
1358 """Message with mailbox-format-specific properties."""
1360 def __init__(self
, message
=None):
1361 """Initialize a Message instance."""
1362 if isinstance(message
, email
.message
.Message
):
1363 self
._become
_message
(copy
.deepcopy(message
))
1364 if isinstance(message
, Message
):
1365 message
._explain
_to
(self
)
1366 elif isinstance(message
, str):
1367 self
._become
_message
(email
.message_from_string(message
))
1368 elif hasattr(message
, "read"):
1369 self
._become
_message
(email
.message_from_file(message
))
1370 elif message
is None:
1371 email
.message
.Message
.__init
__(self
)
1373 raise TypeError('Invalid message type: %s' % type(message
))
1375 def _become_message(self
, message
):
1376 """Assume the non-format-specific state of message."""
1377 for name
in ('_headers', '_unixfrom', '_payload', '_charset',
1378 'preamble', 'epilogue', 'defects', '_default_type'):
1379 self
.__dict
__[name
] = message
.__dict
__[name
]
1381 def _explain_to(self
, message
):
1382 """Copy format-specific state to message insofar as possible."""
1383 if isinstance(message
, Message
):
1384 return # There's nothing format-specific to explain.
1386 raise TypeError('Cannot convert to specified type')
1389 class MaildirMessage(Message
):
1390 """Message with Maildir-specific properties."""
1392 def __init__(self
, message
=None):
1393 """Initialize a MaildirMessage instance."""
1394 self
._subdir
= 'new'
1396 self
._date
= time
.time()
1397 Message
.__init
__(self
, message
)
1399 def get_subdir(self
):
1400 """Return 'new' or 'cur'."""
1403 def set_subdir(self
, subdir
):
1404 """Set subdir to 'new' or 'cur'."""
1405 if subdir
== 'new' or subdir
== 'cur':
1406 self
._subdir
= subdir
1408 raise ValueError("subdir must be 'new' or 'cur': %s" % subdir
)
1410 def get_flags(self
):
1411 """Return as a string the flags that are set."""
1412 if self
._info
.startswith('2,'):
1413 return self
._info
[2:]
1417 def set_flags(self
, flags
):
1418 """Set the given flags and unset all others."""
1419 self
._info
= '2,' + ''.join(sorted(flags
))
1421 def add_flag(self
, flag
):
1422 """Set the given flag(s) without changing others."""
1423 self
.set_flags(''.join(set(self
.get_flags()) |
set(flag
)))
1425 def remove_flag(self
, flag
):
1426 """Unset the given string flag(s) without changing others."""
1427 if self
.get_flags() != '':
1428 self
.set_flags(''.join(set(self
.get_flags()) - set(flag
)))
1431 """Return delivery date of message, in seconds since the epoch."""
1434 def set_date(self
, date
):
1435 """Set delivery date of message, in seconds since the epoch."""
1437 self
._date
= float(date
)
1439 raise TypeError("can't convert to float: %s" % date
)
1442 """Get the message's "info" as a string."""
1445 def set_info(self
, info
):
1446 """Set the message's "info" string."""
1447 if isinstance(info
, str):
1450 raise TypeError('info must be a string: %s' % type(info
))
1452 def _explain_to(self
, message
):
1453 """Copy Maildir-specific state to message insofar as possible."""
1454 if isinstance(message
, MaildirMessage
):
1455 message
.set_flags(self
.get_flags())
1456 message
.set_subdir(self
.get_subdir())
1457 message
.set_date(self
.get_date())
1458 elif isinstance(message
, _mboxMMDFMessage
):
1459 flags
= set(self
.get_flags())
1461 message
.add_flag('R')
1462 if self
.get_subdir() == 'cur':
1463 message
.add_flag('O')
1465 message
.add_flag('D')
1467 message
.add_flag('F')
1469 message
.add_flag('A')
1470 message
.set_from('MAILER-DAEMON', time
.gmtime(self
.get_date()))
1471 elif isinstance(message
, MHMessage
):
1472 flags
= set(self
.get_flags())
1473 if 'S' not in flags
:
1474 message
.add_sequence('unseen')
1476 message
.add_sequence('replied')
1478 message
.add_sequence('flagged')
1479 elif isinstance(message
, BabylMessage
):
1480 flags
= set(self
.get_flags())
1481 if 'S' not in flags
:
1482 message
.add_label('unseen')
1484 message
.add_label('deleted')
1486 message
.add_label('answered')
1488 message
.add_label('forwarded')
1489 elif isinstance(message
, Message
):
1492 raise TypeError('Cannot convert to specified type: %s' %
1496 class _mboxMMDFMessage(Message
):
1497 """Message with mbox- or MMDF-specific properties."""
1499 def __init__(self
, message
=None):
1500 """Initialize an mboxMMDFMessage instance."""
1501 self
.set_from('MAILER-DAEMON', True)
1502 if isinstance(message
, email
.message
.Message
):
1503 unixfrom
= message
.get_unixfrom()
1504 if unixfrom
is not None and unixfrom
.startswith('From '):
1505 self
.set_from(unixfrom
[5:])
1506 Message
.__init
__(self
, message
)
1509 """Return contents of "From " line."""
1512 def set_from(self
, from_
, time_
=None):
1513 """Set "From " line, formatting and appending time_ if specified."""
1514 if time_
is not None:
1516 time_
= time
.gmtime()
1517 from_
+= ' ' + time
.asctime(time_
)
1520 def get_flags(self
):
1521 """Return as a string the flags that are set."""
1522 return self
.get('Status', '') + self
.get('X-Status', '')
1524 def set_flags(self
, flags
):
1525 """Set the given flags and unset all others."""
1527 status_flags
, xstatus_flags
= '', ''
1528 for flag
in ('R', 'O'):
1530 status_flags
+= flag
1532 for flag
in ('D', 'F', 'A'):
1534 xstatus_flags
+= flag
1536 xstatus_flags
+= ''.join(sorted(flags
))
1538 self
.replace_header('Status', status_flags
)
1540 self
.add_header('Status', status_flags
)
1542 self
.replace_header('X-Status', xstatus_flags
)
1544 self
.add_header('X-Status', xstatus_flags
)
1546 def add_flag(self
, flag
):
1547 """Set the given flag(s) without changing others."""
1548 self
.set_flags(''.join(set(self
.get_flags()) |
set(flag
)))
1550 def remove_flag(self
, flag
):
1551 """Unset the given string flag(s) without changing others."""
1552 if 'Status' in self
or 'X-Status' in self
:
1553 self
.set_flags(''.join(set(self
.get_flags()) - set(flag
)))
1555 def _explain_to(self
, message
):
1556 """Copy mbox- or MMDF-specific state to message insofar as possible."""
1557 if isinstance(message
, MaildirMessage
):
1558 flags
= set(self
.get_flags())
1560 message
.set_subdir('cur')
1562 message
.add_flag('F')
1564 message
.add_flag('R')
1566 message
.add_flag('S')
1568 message
.add_flag('T')
1569 del message
['status']
1570 del message
['x-status']
1571 maybe_date
= ' '.join(self
.get_from().split()[-5:])
1573 message
.set_date(calendar
.timegm(time
.strptime(maybe_date
,
1574 '%a %b %d %H:%M:%S %Y')))
1575 except (ValueError, OverflowError):
1577 elif isinstance(message
, _mboxMMDFMessage
):
1578 message
.set_flags(self
.get_flags())
1579 message
.set_from(self
.get_from())
1580 elif isinstance(message
, MHMessage
):
1581 flags
= set(self
.get_flags())
1582 if 'R' not in flags
:
1583 message
.add_sequence('unseen')
1585 message
.add_sequence('replied')
1587 message
.add_sequence('flagged')
1588 del message
['status']
1589 del message
['x-status']
1590 elif isinstance(message
, BabylMessage
):
1591 flags
= set(self
.get_flags())
1592 if 'R' not in flags
:
1593 message
.add_label('unseen')
1595 message
.add_label('deleted')
1597 message
.add_label('answered')
1598 del message
['status']
1599 del message
['x-status']
1600 elif isinstance(message
, Message
):
1603 raise TypeError('Cannot convert to specified type: %s' %
1607 class mboxMessage(_mboxMMDFMessage
):
1608 """Message with mbox-specific properties."""
1611 class MHMessage(Message
):
1612 """Message with MH-specific properties."""
1614 def __init__(self
, message
=None):
1615 """Initialize an MHMessage instance."""
1616 self
._sequences
= []
1617 Message
.__init
__(self
, message
)
1619 def get_sequences(self
):
1620 """Return a list of sequences that include the message."""
1621 return self
._sequences
[:]
1623 def set_sequences(self
, sequences
):
1624 """Set the list of sequences that include the message."""
1625 self
._sequences
= list(sequences
)
1627 def add_sequence(self
, sequence
):
1628 """Add sequence to list of sequences including the message."""
1629 if isinstance(sequence
, str):
1630 if not sequence
in self
._sequences
:
1631 self
._sequences
.append(sequence
)
1633 raise TypeError('sequence must be a string: %s' % type(sequence
))
1635 def remove_sequence(self
, sequence
):
1636 """Remove sequence from the list of sequences including the message."""
1638 self
._sequences
.remove(sequence
)
1642 def _explain_to(self
, message
):
1643 """Copy MH-specific state to message insofar as possible."""
1644 if isinstance(message
, MaildirMessage
):
1645 sequences
= set(self
.get_sequences())
1646 if 'unseen' in sequences
:
1647 message
.set_subdir('cur')
1649 message
.set_subdir('cur')
1650 message
.add_flag('S')
1651 if 'flagged' in sequences
:
1652 message
.add_flag('F')
1653 if 'replied' in sequences
:
1654 message
.add_flag('R')
1655 elif isinstance(message
, _mboxMMDFMessage
):
1656 sequences
= set(self
.get_sequences())
1657 if 'unseen' not in sequences
:
1658 message
.add_flag('RO')
1660 message
.add_flag('O')
1661 if 'flagged' in sequences
:
1662 message
.add_flag('F')
1663 if 'replied' in sequences
:
1664 message
.add_flag('A')
1665 elif isinstance(message
, MHMessage
):
1666 for sequence
in self
.get_sequences():
1667 message
.add_sequence(sequence
)
1668 elif isinstance(message
, BabylMessage
):
1669 sequences
= set(self
.get_sequences())
1670 if 'unseen' in sequences
:
1671 message
.add_label('unseen')
1672 if 'replied' in sequences
:
1673 message
.add_label('answered')
1674 elif isinstance(message
, Message
):
1677 raise TypeError('Cannot convert to specified type: %s' %
1681 class BabylMessage(Message
):
1682 """Message with Babyl-specific properties."""
1684 def __init__(self
, message
=None):
1685 """Initialize an BabylMessage instance."""
1687 self
._visible
= Message()
1688 Message
.__init
__(self
, message
)
1690 def get_labels(self
):
1691 """Return a list of labels on the message."""
1692 return self
._labels
[:]
1694 def set_labels(self
, labels
):
1695 """Set the list of labels on the message."""
1696 self
._labels
= list(labels
)
1698 def add_label(self
, label
):
1699 """Add label to list of labels on the message."""
1700 if isinstance(label
, str):
1701 if label
not in self
._labels
:
1702 self
._labels
.append(label
)
1704 raise TypeError('label must be a string: %s' % type(label
))
1706 def remove_label(self
, label
):
1707 """Remove label from the list of labels on the message."""
1709 self
._labels
.remove(label
)
1713 def get_visible(self
):
1714 """Return a Message representation of visible headers."""
1715 return Message(self
._visible
)
1717 def set_visible(self
, visible
):
1718 """Set the Message representation of visible headers."""
1719 self
._visible
= Message(visible
)
1721 def update_visible(self
):
1722 """Update and/or sensibly generate a set of visible headers."""
1723 for header
in self
._visible
.keys():
1725 self
._visible
.replace_header(header
, self
[header
])
1727 del self
._visible
[header
]
1728 for header
in ('Date', 'From', 'Reply-To', 'To', 'CC', 'Subject'):
1729 if header
in self
and header
not in self
._visible
:
1730 self
._visible
[header
] = self
[header
]
1732 def _explain_to(self
, message
):
1733 """Copy Babyl-specific state to message insofar as possible."""
1734 if isinstance(message
, MaildirMessage
):
1735 labels
= set(self
.get_labels())
1736 if 'unseen' in labels
:
1737 message
.set_subdir('cur')
1739 message
.set_subdir('cur')
1740 message
.add_flag('S')
1741 if 'forwarded' in labels
or 'resent' in labels
:
1742 message
.add_flag('P')
1743 if 'answered' in labels
:
1744 message
.add_flag('R')
1745 if 'deleted' in labels
:
1746 message
.add_flag('T')
1747 elif isinstance(message
, _mboxMMDFMessage
):
1748 labels
= set(self
.get_labels())
1749 if 'unseen' not in labels
:
1750 message
.add_flag('RO')
1752 message
.add_flag('O')
1753 if 'deleted' in labels
:
1754 message
.add_flag('D')
1755 if 'answered' in labels
:
1756 message
.add_flag('A')
1757 elif isinstance(message
, MHMessage
):
1758 labels
= set(self
.get_labels())
1759 if 'unseen' in labels
:
1760 message
.add_sequence('unseen')
1761 if 'answered' in labels
:
1762 message
.add_sequence('replied')
1763 elif isinstance(message
, BabylMessage
):
1764 message
.set_visible(self
.get_visible())
1765 for label
in self
.get_labels():
1766 message
.add_label(label
)
1767 elif isinstance(message
, Message
):
1770 raise TypeError('Cannot convert to specified type: %s' %
1774 class MMDFMessage(_mboxMMDFMessage
):
1775 """Message with MMDF-specific properties."""
1779 """A read-only wrapper of a file."""
1781 def __init__(self
, f
, pos
=None):
1782 """Initialize a _ProxyFile."""
1785 self
._pos
= f
.tell()
1789 def read(self
, size
=None):
1791 return self
._read
(size
, self
._file
.read
)
1793 def readline(self
, size
=None):
1795 return self
._read
(size
, self
._file
.readline
)
1797 def readlines(self
, sizehint
=None):
1798 """Read multiple lines."""
1802 if sizehint
is not None:
1803 sizehint
-= len(line
)
1809 """Iterate over lines."""
1810 return iter(self
.readline
, "")
1813 """Return the position."""
1816 def seek(self
, offset
, whence
=0):
1817 """Change position."""
1819 self
._file
.seek(self
._pos
)
1820 self
._file
.seek(offset
, whence
)
1821 self
._pos
= self
._file
.tell()
1824 """Close the file."""
1827 def _read(self
, size
, read_method
):
1828 """Read size bytes using read_method."""
1831 self
._file
.seek(self
._pos
)
1832 result
= read_method(size
)
1833 self
._pos
= self
._file
.tell()
1837 class _PartialFile(_ProxyFile
):
1838 """A read-only wrapper of part of a file."""
1840 def __init__(self
, f
, start
=None, stop
=None):
1841 """Initialize a _PartialFile."""
1842 _ProxyFile
.__init
__(self
, f
, start
)
1847 """Return the position with respect to start."""
1848 return _ProxyFile
.tell(self
) - self
._start
1850 def seek(self
, offset
, whence
=0):
1851 """Change position, possibly with respect to start or stop."""
1853 self
._pos
= self
._start
1856 self
._pos
= self
._stop
1858 _ProxyFile
.seek(self
, offset
, whence
)
1860 def _read(self
, size
, read_method
):
1861 """Read size bytes using read_method, honoring start and stop."""
1862 remaining
= self
._stop
- self
._pos
1865 if size
is None or size
< 0 or size
> remaining
:
1867 return _ProxyFile
._read
(self
, size
, read_method
)
1870 def _lock_file(f
, dotlock
=True):
1871 """Lock file f using lockf and dot locking."""
1872 dotlock_done
= False
1876 fcntl
.lockf(f
, fcntl
.LOCK_EX | fcntl
.LOCK_NB
)
1878 if e
.errno
in (errno
.EAGAIN
, errno
.EACCES
):
1879 raise ExternalClashError('lockf: lock unavailable: %s' %
1885 pre_lock
= _create_temporary(f
.name
+ '.lock')
1888 if e
.errno
== errno
.EACCES
:
1889 return # Without write access, just skip dotlocking.
1893 if hasattr(os
, 'link'):
1894 os
.link(pre_lock
.name
, f
.name
+ '.lock')
1896 os
.unlink(pre_lock
.name
)
1898 os
.rename(pre_lock
.name
, f
.name
+ '.lock')
1901 if e
.errno
== errno
.EEXIST
or \
1902 (os
.name
== 'os2' and e
.errno
== errno
.EACCES
):
1903 os
.remove(pre_lock
.name
)
1904 raise ExternalClashError('dot lock unavailable: %s' %
1910 fcntl
.lockf(f
, fcntl
.LOCK_UN
)
1912 os
.remove(f
.name
+ '.lock')
1915 def _unlock_file(f
):
1916 """Unlock file f using lockf and dot locking."""
1918 fcntl
.lockf(f
, fcntl
.LOCK_UN
)
1919 if os
.path
.exists(f
.name
+ '.lock'):
1920 os
.remove(f
.name
+ '.lock')
1922 def _create_carefully(path
):
1923 """Create a file if it doesn't exist and open for reading and writing."""
1924 fd
= os
.open(path
, os
.O_CREAT | os
.O_EXCL | os
.O_RDWR
, 0666)
1926 return open(path
, 'rb+')
1930 def _create_temporary(path
):
1931 """Create a temp file based on path and open for reading and writing."""
1932 return _create_carefully('%s.%s.%s.%s' % (path
, int(time
.time()),
1933 socket
.gethostname(),
1937 """Ensure changes to file f are physically on disk."""
1939 if hasattr(os
, 'fsync'):
1940 os
.fsync(f
.fileno())
1943 """Close file f, ensuring all changes are physically on disk."""
1947 ## Start: classes from the original module (for backward compatibility).
1949 # Note that the Maildir class, whose name is unchanged, itself offers a next()
1950 # method for backward compatibility.
1954 def __init__(self
, fp
, factory
=rfc822
.Message
):
1957 self
.factory
= factory
1960 return iter(self
.next
, None)
1964 self
.fp
.seek(self
.seekp
)
1966 self
._search
_start
()
1968 self
.seekp
= self
.fp
.tell()
1970 start
= self
.fp
.tell()
1972 self
.seekp
= stop
= self
.fp
.tell()
1975 return self
.factory(_PartialFile(self
.fp
, start
, stop
))
1977 # Recommended to use PortableUnixMailbox instead!
1978 class UnixMailbox(_Mailbox
):
1980 def _search_start(self
):
1982 pos
= self
.fp
.tell()
1983 line
= self
.fp
.readline()
1986 if line
[:5] == 'From ' and self
._isrealfromline
(line
):
1990 def _search_end(self
):
1991 self
.fp
.readline() # Throw away header line
1993 pos
= self
.fp
.tell()
1994 line
= self
.fp
.readline()
1997 if line
[:5] == 'From ' and self
._isrealfromline
(line
):
2001 # An overridable mechanism to test for From-line-ness. You can either
2002 # specify a different regular expression or define a whole new
2003 # _isrealfromline() method. Note that this only gets called for lines
2004 # starting with the 5 characters "From ".
2007 #http://home.netscape.com/eng/mozilla/2.0/relnotes/demo/content-length.html
2008 # the only portable, reliable way to find message delimiters in a BSD (i.e
2009 # Unix mailbox) style folder is to search for "\n\nFrom .*\n", or at the
2010 # beginning of the file, "^From .*\n". While _fromlinepattern below seems
2011 # like a good idea, in practice, there are too many variations for more
2012 # strict parsing of the line to be completely accurate.
2014 # _strict_isrealfromline() is the old version which tries to do stricter
2015 # parsing of the From_ line. _portable_isrealfromline() simply returns
2016 # true, since it's never called if the line doesn't already start with
2019 # This algorithm, and the way it interacts with _search_start() and
2020 # _search_end() may not be completely correct, because it doesn't check
2021 # that the two characters preceding "From " are \n\n or the beginning of
2022 # the file. Fixing this would require a more extensive rewrite than is
2023 # necessary. For convenience, we've added a PortableUnixMailbox class
2024 # which does no checking of the format of the 'From' line.
2026 _fromlinepattern
= (r
"From \s*[^\s]+\s+\w\w\w\s+\w\w\w\s+\d?\d\s+"
2027 r
"\d?\d:\d\d(:\d\d)?(\s+[^\s]+)?\s+\d\d\d\d\s*"
2032 def _strict_isrealfromline(self
, line
):
2033 if not self
._regexp
:
2035 self
._regexp
= re
.compile(self
._fromlinepattern
)
2036 return self
._regexp
.match(line
)
2038 def _portable_isrealfromline(self
, line
):
2041 _isrealfromline
= _strict_isrealfromline
2044 class PortableUnixMailbox(UnixMailbox
):
2045 _isrealfromline
= UnixMailbox
._portable
_isrealfromline
2048 class MmdfMailbox(_Mailbox
):
2050 def _search_start(self
):
2052 line
= self
.fp
.readline()
2055 if line
[:5] == '\001\001\001\001\n':
2058 def _search_end(self
):
2060 pos
= self
.fp
.tell()
2061 line
= self
.fp
.readline()
2064 if line
== '\001\001\001\001\n':
2071 def __init__(self
, dirname
, factory
=rfc822
.Message
):
2073 pat
= re
.compile('^[1-9][0-9]*$')
2074 self
.dirname
= dirname
2075 # the three following lines could be combined into:
2076 # list = map(long, filter(pat.match, os.listdir(self.dirname)))
2077 list = os
.listdir(self
.dirname
)
2078 list = filter(pat
.match
, list)
2079 list = map(long, list)
2081 # This only works in Python 1.6 or later;
2082 # before that str() added 'L':
2083 self
.boxes
= map(str, list)
2084 self
.boxes
.reverse()
2085 self
.factory
= factory
2088 return iter(self
.next
, None)
2093 fn
= self
.boxes
.pop()
2094 fp
= open(os
.path
.join(self
.dirname
, fn
))
2095 msg
= self
.factory(fp
)
2098 except (AttributeError, TypeError):
2103 class BabylMailbox(_Mailbox
):
2105 def _search_start(self
):
2107 line
= self
.fp
.readline()
2110 if line
== '*** EOOH ***\n':
2113 def _search_end(self
):
2115 pos
= self
.fp
.tell()
2116 line
= self
.fp
.readline()
2119 if line
== '\037\014\n' or line
== '\037':
2123 ## End: classes from the original module (for backward compatibility).
2126 class Error(Exception):
2127 """Raised for module-specific errors."""
2129 class NoSuchMailboxError(Error
):
2130 """The specified mailbox does not exist and won't be created."""
2132 class NotEmptyError(Error
):
2133 """The specified mailbox is not empty and deletion was requested."""
2135 class ExternalClashError(Error
):
2136 """Another process caused an action to fail."""
2138 class FormatError(Error
):
2139 """A file appears to have an invalid format."""